ActivityResultLauncher: 개념과 사용 방법

저자: IT Sectr 게시일: 2026-06-10 읽는 시간: 9 분

ActivityResultLauncher는 Android Activity Result API의 구성 요소로, androidx.activity 라이브러리의 Activity 1.2.0 버전에서 도입되었습니다. 이는 Android SDK가 처음 만들어진 이후부터 있었던 더 이상 사용되지 않는 startActivityForResult 및 onActivityResult 메서드를 대체합니다. Android Developers (2024)에 따르면, 새 API는 Activity와의 강한 결합 및 타입 안전성 부족 문제를 해결합니다. ActivityResultLauncher는 미리 등록되며 Contract를 사용하여 입력 및 출력 데이터의 엄격한 타입 지정을 수행합니다.

주요 사항

  • ActivityResultLauncher — 더 이상 사용되지 않는 startActivityForResult 대신 Activity에서 결과를 얻기 위한 새 API
  • Contract — 특정 시나리오에 대한 입력 및 출력 데이터 타입을 정의하는 객체
  • 등록은 launch를 호출하기 전에 registerForActivityResult를 통해 수행됩니다
  • Callback은 대상 Activity가 결과와 함께 종료된 후 호출됩니다
  • API 사용 가능 Activity 1.2.0부터 Activity, Fragment 및 Compose에서

ActivityResultLauncher란

ActivityResultLauncher는 androidx.activity.result 패키지의 클래스로, Activity를 시작하고 결과를 받기 위한 타입 안전 메커니즘을 제공합니다. Launcher는 registerForActivityResult 메서드를 통해 생성되며, Contract(입출력 타입 설명)와 ActivityResultCallback(결과 핸들러)의 두 매개변수를 받습니다. 등록 후 launcher는 launch 메서드를 통해 호출할 준비가 됩니다.

이전 API와의 주요 차이점은 등록과 실행의 분리입니다. 등록은 초기화 단계(Activity.onCreate 또는 Fragment.onCreate)에서 수행되며, 콜백은 launcher에 한 번 바인딩되고 결과가 반환될 때 실행이 보장됩니다. 이는 onActivityResult가 예상치 못한 순서로 호출되거나 소멸된 Activity에서 호출되는 문제를 해결합니다.

ActivityResultLauncher는 이전에 onActivityResult를 통해 처리되던 모든 시나리오를 지원합니다: 카메라 실행, 갤러리, 연락처 요청, 권한 및 사용자 정의 Activity. 또한 API는 확장 가능하여 개발자는 Activity 간의 특정 데이터 교환 시나리오를 위한 사용자 정의 Contract를 만들 수 있습니다.

Activity Result API가 startActivityForResult를 대체한 이유

startActivityForResult는 API Level 1(2008)부터 Android SDK의 일부였으며 12년 이상 Activity에서 결과를 얻는 주요 방법으로 남아 있었습니다. 그러나 이 메서드에는 Google이 Activity Result API에서 해결한 근본적인 결함이 있었습니다. 주요 문제와 새 API가 이를 해결하는 방법을 살펴보겠습니다.

문제 1: Activity와의 강한 결합

startActivityForResult 메서드는 requestCode(onActivityResult에 전달되는 임의의 정수)를 통해 Activity 및 Fragment에 연결됩니다. 개발자는 수동으로 코드를 실행된 작업과 일치시켜야 했으며, 이로 인해 코드 재사용 및 상속에서 오류가 발생했습니다. ActivityResultLauncher는 requestCode를 완전히 제거합니다. 콜백은 등록 시 특정 launcher에 바인딩되며 해당 launcher에 대해서만 호출됩니다.

문제 2: 화면 회전 시 결과 손실

구성 변경(화면 회전, 언어 변경) 중에 Activity가 다시 생성되고 onActivityResult가 실행되지 않을 수 있었습니다 — 콜백이 손실되었습니다. Activity Result API는 SavedStateRegistry를 통해 launcher 상태를 자동으로 저장하고 복원하여 Activity 재생성 후에도 결과가 수신되도록 보장합니다.

문제 3: 타입 안전성 부족

이전 API는 키와 데이터 타입이 컴파일러에 의해 검사되지 않는 Bundle이 포함된 Intent를 통해 결과를 전달했습니다. Activity Result API는 Contract(입력 데이터 타입(I)과 결과 타입(O)을 정의하는 제네릭 인터페이스)를 사용합니다. 타입 불일치 오류는 런타임이 아닌 컴파일 타임에 발견됩니다.

특징startActivityForResultActivityResultLauncher
RequestCode수동 관리 필요자동, 필요 없음
타입 안전성없음제네릭 Contract
회전 시 저장손실됨SavedStateRegistry
최소 APIAPI Level 1Activity 1.2.0
Compose에서 사용지원 안 함rememberLauncherForActivityResult

Activity Result API의 주요 계약

Contract는 ActivityResultContract<I, O> 인터페이스로, Activity를 시작하는 방법과 결과를 해석하는 방법을 정의합니다. Google은 대부분의 개발자 요구를 충족하는 일반적인 시나리오를 위한 기본 제공 계약 세트를 제공합니다.

StartIntentSenderForResult

StartIntentSenderForResult — IntentSender를 시작하기 위한 기본 계약입니다. 시스템 시나리오에서 사용되며, 예를 들어 Google Sign-In을 통한 인증이나 Google Pay를 통한 결제 시 사용됩니다. 입력 매개변수는 PendingIntent이고, 출력은 코드와 Intent가 포함된 ActivityResult입니다.

RequestMultiplePermissions

RequestMultiplePermissions — Android 6.0+에서 여러 권한을 동시에 요청하기 위한 계약입니다. 입력 매개변수는 권한 이름의 String 배열이고, 출력은 각 요청의 결과가 포함된 Map<String, Boolean>입니다. 이전에는 요청 코드를 일치시키면서 onRequestPermissionsResult에서 수동으로 파싱해야 했습니다.

TakePicture 및 TakeVideo

TakePicture — 시스템 카메라를 통해 사진을 촬영하기 위한 계약입니다. 입력은 이미지를 저장할 Uri이고, 출력은 Boolean(성공)입니다. TakeVideo는 비디오에서도 유사하게 작동합니다. 이 계약들은 기기마다 불안정한 동작을 보이는 더 이상 사용되지 않는 MediaStore.ACTION_IMAGE_CAPTURE를 대체합니다.

GetContent 및 OpenDocument

GetContent — 시스템 피커를 통해 콘텐츠를 선택하기 위한 계약입니다. 입력은 MIME 타입(예: image/*)이고, 출력은 선택된 파일의 Uri입니다. OpenDocument는 다중 선택 및 문서 타입별 필터링을 지원한다는 점에서 다릅니다. 두 계약 모두 SAF(Storage Access Framework)를 통해 작동합니다.

CreateDocument 및 OpenDocumentTree

CreateDocument — 시스템 대화상자를 통해 새 문서를 만들기 위한 계약입니다. 사용자가 이름과 폴더를 선택하면 시스템이 쓰기용 Uri를 반환합니다. OpenDocumentTree는 전체 디렉토리에 대한 액세스를 제공합니다 — 사용자가 폴더를 선택하면 앱이 내부의 모든 파일을 읽고 쓰기 위한 tree-uri를 받습니다.

Activity 및 Fragment에서의 사용

클래식 Android에서 ActivityResultLauncher를 사용하는 기본 패턴은 두 단계로 구성됩니다: 초기화 시 registerForActivityResult를 통한 등록과 사용자 작업에 응답한 launch 호출입니다. 갤러리에서 이미지를 선택하는 일반적인 예를 살펴보겠습니다.

Activity에서 등록 및 실행

Activity의 onCreate에서 launcher를 등록하세요 — 이렇게 하면 가능한 호출 전에 콜백이 준비됩니다. 실행 직전에 launcher를 등록하지 마세요 — API 계약을 위반하며 Activity 재생성 시 결과가 손실될 수 있습니다.

kotlin
class MainActivity : AppCompatActivity() {
    private val pickImageLauncher =
        registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
            uri?.let { binding.imageView.setImageURI(it) }
        }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        pickImageLauncher.launch("image/*")
    }
}

Fragment에서 사용

Fragment에서는 onCreate, onAttach 또는 onCreateView에서 초기화 시 등록이 수행됩니다. FragmentActivity는 부모 Activity를 통해 launcher를 전달하므로 결과는 Activity가 아닌 Fragment 내에서 처리됩니다. 이는 모든 Fragment의 모든 결과가 하나의 Activity 메서드에 모였던 onActivityResult에 비해 캡슐화가 향상됩니다.

kotlin
class ProfileFragment : Fragment() {
    private val cameraLauncher =
        registerForActivityResult(ActivityResultContracts.TakePicture()) { success ->
            if (success) { updateProfilePhoto() }
        }

    fun takePhoto(photoUri: Uri) {
        cameraLauncher.launch(photoUri)
    }
}

Jetpack Compose에서의 ActivityResultLauncher

Jetpack Compose는 Activity Result API를 위한 특별한 composable 함수 — rememberLauncherForActivityResult를 제공합니다. 클래식 접근 방식과 달리, Compose에서 launcher는 remember를 통해 composable 수명 주기에 바인딩된 객체로 생성됩니다. 이를 통해 Activity나 Fragment에 직접 접근하지 않고도 선언적 스타일로 Activity Result API를 완전히 사용할 수 있습니다.

rememberLauncherForActivityResult

rememberLauncherForActivityResult는 Contract와 콜백을 받아 ActivityResultLauncher를 반환합니다. Launcher는 재구성 중에 유지되며 구성을 벗어날 때 자동으로 정리됩니다. launch 호출은 이벤트(예: 버튼 클릭 또는 상태 변경)에 응답하여 발생합니다.

kotlin
@Composable
fun PhotoPicker() {
    val context = LocalContext.current
    val launcher = rememberLauncherForActivityResult(
        ActivityResultContracts.GetContent()
    ) { uri -> handleImage(uri) }

    Button(onClick = { launcher.launch("image/*") }) {
        Text("사진 선택")
    }
}

Compose에서 권한 처리

Compose에서 권한 요청도 RequestPermission 또는 RequestMultiplePermissions 계약과 함께 rememberLauncherForActivityResult를 통해 수행됩니다. Google은 accompanist-permissions 사용을 권장하지만, 내부적으로도 Activity Result API를 사용합니다. 권한 상태를 추적하려면 remember 또는 ViewModel에 상태를 저장하는 것이 편리합니다.

일반적인 실수와 모범 사례

Activity Result API는 이전 접근 방식의 많은 문제를 해결했지만, 부적절한 사용은 새로운 유형의 오류로 이어질 수 있습니다. 가장 일반적인 문제와 이를 피하는 방법을 살펴보겠습니다.

실수: 람다 또는 코루틴 내에서 등록

Launcher의 등록은 컴포넌트 초기화 중(Activity의 onCreate 또는 Fragment 초기화자)에 수행되어야 합니다. 람다, 콜백 또는 코루틴 내에서 launcher를 등록하면 Activity 재생성 시 등록이 다시 수행되어 이전 launcher가 결과와의 연결을 잃을 수 있습니다.

실수: 동일한 키로 여러 launcher 등록

각 launcher는 상태 저장을 위한 고유 키를 받습니다. 하나의 컴포넌트에 동일한 Contract를 가진 두 개의 launcher를 등록하면 SavedStateRegistry가 한쪽의 상태를 다른 쪽으로 덮어쓸 수 있습니다. Android Studio는 lint 규칙 UnnecessaryRegisterForActivityResult를 통해 이에 대해 경고하지만, 고유성을 수동으로 제어하는 것이 좋습니다.

모범 사례: 항상 null 결과 처리

사용자는 작업을 취소할 수 있습니다 — 시스템 뒤로 가기 버튼을 누르거나, 앱을 최소화하거나, 다른 앱으로 전환할 수 있습니다. 이 경우 콜백은 null 또는 RESULT_CANCELED가 포함된 ActivityResult를 받습니다. NullPointerException을 방지하려면 사용하기 전에 항상 결과가 null인지 확인하세요.

모범 사례: 재사용 가능한 로직을 위한 사용자 정의 Contract

앱이 자주 유사한 시나리오를 시작하는 경우 — 예: 연락처를 선택하고 이름과 전화번호를 반환하는 경우 — 사용자 정의 Contract를 만드세요. 이렇게 하면 코드 가독성이 향상되고 실행 및 결과 처리 로직을 중앙에서 변경할 수 있습니다.

kotlin
class PickContactContract : ActivityResultContract<Void, ContactData?>() {
    override fun createIntent(context: Context, input: Void?) =
        Intent(Intent.ACTION_PICK).setType(ContactsContract.Contacts.CONTENT_TYPE)

    override fun parseResult(resultCode: Int, intent: Intent?) =
        intent?.data?.let { queryContact(it) }
}

자주 묻는 질문

ViewModel에서 ActivityResultLauncher를 사용할 수 있나요?

아니요 — ActivityResultLauncher는 등록을 위해 Activity 또는 Fragment 컨텍스트가 필요합니다. ViewModel은 상태 저장에만 사용하고, launcher는 Activity 또는 Fragment에서 생성하여 결과를 ViewModel에 전달하세요.

Activity Result API에 필요한 최소 SDK는 무엇인가요?

Activity Result API는 라이브러리 activity-ktx 1.2.0부터 사용할 수 있습니다. 최소 SDK는 API Level 14(Android 4.0)이지만, 대부분의 계약은 API Level 19+에서만 작동합니다.

결과를 받기 전에 launch를 두 번 호출하면 어떻게 되나요?

첫 번째 작업이 완료되기 전의 반복된 launch 호출은 무시됩니다. Activity Result API는 병렬 실행을 지원하지 않습니다 — 새 호출 전에 첫 번째 작업의 콜백을 기다리세요.

이전 코드에서 onActivityResult를 어떻게 대체하나요?

마이그레이션은 적절한 Contract와 함께 startActivityForResult 호출을 registerForActivityResult로 대체하여 수행됩니다. onActivityResult를 제거하고 launcher 콜백에서 결과를 처리하세요. Google은 Android Developers 문서에서 마이그레이션 가이드를 제공합니다.

ActivityResultLauncher는 ML Kit 또는 Barcode Scanner와 같은 라이브러리와 작동하나요?

, 많은 라이브러리가 ActivityResultContracts를 통한 통합을 지원합니다. 예를 들어, ML Kit Barcode Scanner는 스캐너를 시작하기 위해 StartIntentSenderForResult를 사용합니다. 특정 라이브러리의 문서를 확인하세요.

요약

  • ActivityResultLauncher — 더 이상 사용되지 않는 startActivityForResult를 대체하는 최신 타입 안전 API
  • Contract는 입출력 데이터 타입을 정의하여 수동 requestCode 일치를 제거
  • 등록은 초기화 시 수행되며, 결과는 콜백을 통해 안정적으로 전달
  • 기본 제공 계약은 카메라, 갤러리, 권한, 문서 및 연락처를 포함
  • Jetpack Compose는 API 작업을 위해 rememberLauncherForActivityResult 사용
  • 사용자 정의 Contract는 컴포넌트 간 실행 로직 재사용 허용
  • API는 SavedStateRegistry를 통해 구성 변경 중 상태 유지

턴키 방식의 모바일 애플리케이션을 개발해 드립니다

IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.

프로젝트 논의

더 읽어보기