|

registerForActivityResult 쉽게 이해하기

registerForActivityResult 대표 이미지
결과 처리 API는 호출 문법보다 생명주기와 계약 구조가 더 중요하다

registerForActivityResult는 단순히 새 문법이 아닙니다. 결론부터 말하면 결과 처리 규칙을 생명주기와 계약 기반으로 묶어 주기 때문에 onActivityResult보다 훨씬 안전합니다.

예전 onActivityResult는 requestCode 분기, nullable Intent 처리, Fragment 전달 경로, 재생성 이후 callback 위치를 사람이 계속 맞춰야 했습니다. 반면 Activity Result API는 launcher 하나에 입력 계약과 결과 callback이 같이 묶여 있어서 코드가 더 짧고, 더 지역적이고, 더 테스트하기 쉬워집니다.

이번 글에서는 왜 새 API가 권장되는지부터 시작해서, 권한 요청사진 선택 예시까지 단계적으로 보겠습니다. 마지막에는 언제 어떤 패턴을 쓰면 좋은지도 짧은 체크리스트로 정리하겠습니다.


왜 onActivityResult 방식이 자주 꼬였을까

onActivityResult의 가장 큰 문제는 결과 처리 로직이 호출 위치와 멀어지기 쉽다는 점입니다. 화면 A에서 사진 선택을 시작했는데, 결과는 Activity나 Fragment의 큰 switch 문에서 다시 requestCode로 분기해야 했습니다.

  • requestCode 상수를 여러 군데에서 맞춰야 한다
  • Intent data가 null일 수 있어 방어 코드가 늘어난다
  • 호출한 코드와 결과 처리 코드가 멀어져 읽기 흐름이 끊긴다
  • Fragment 중첩 구조에서는 누가 결과를 받는지 헷갈리기 쉽다
  • 생명주기 재생성 이후에도 같은 등록 순서와 전달 경로를 사람이 계속 의식해야 한다

즉, 문제의 본질은 deprecated 딱지보다 결과를 사람이 수동으로 라우팅해야 했던 구조에 있습니다.

onActivityResult와 registerForActivityResult 차이를 한눈에 비교한 카드
핵심 차이는 requestCode 수동 분기에서 계약 기반 launcher로 바뀌었다는 점이다

registerForActivityResult는 무엇이 다를까

Android Developers 문서가 설명하는 핵심은 단순합니다. Activity Result API는 registerForActivityResult()로 callback을 등록하고, launcher.launch()로 실행한다는 흐름입니다. 이 구조 덕분에 호출과 결과 처리가 한 덩어리로 보입니다.

private val pickImageLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
    if (uri != null) {
        viewModel.onImagePicked(uri)
    }
}

fun openGallery() {
    pickImageLauncher.launch("image/*")
}

이 코드는 읽는 순간 의도가 바로 보입니다. 어떤 계약으로 실행하는지, 결과 타입이 무엇인지, 결과를 받으면 어디로 보내는지가 한곳에 모여 있습니다.

예전 방식이 requestCode 중심이었다면, 새 방식은 contract와 launcher 중심입니다. 그래서 실수의 단위가 숫자 분기가 아니라 타입 있는 호출 단위로 바뀝니다.


왜 더 안전하다고 말할 수 있을까

안전하다는 말을 구체적으로 풀면 크게 네 가지입니다.

  1. 호출과 결과가 붙어 있다. 읽는 사람이 코드 흐름을 따라가기 쉽다.
  2. 계약 기반이라 타입 의도가 선명하다. 문자열 MIME type, permission, intent input이 API 이름과 함께 드러난다.
  3. 생명주기 규칙이 명시적이다. 아무 때나 늦게 붙이는 구조보다 훨씬 예측 가능하다.
  4. Fragment와 Activity 모두 같은 패턴으로 볼 수 있다. 결과 처리 방식이 통일된다.

실무에서는 세 번째 이유가 특히 큽니다. registerForActivityResult는 등록 시점을 분명히 요구합니다. 문서도 callback은 화면이 생성될 때마다 무조건 등록되어야 하고, launcher는 lifecycle이 CREATED 되기 전에 등록해야 한다고 설명합니다.

registerForActivityResult 등록과 실행 시점을 보여주는 흐름도
등록은 미리, 실행은 화면이 준비된 뒤라는 두 단계 규칙이 핵심이다

등록 시점이 왜 중요할까

많이 하는 실수 중 하나가 버튼을 눌렀을 때 launcher를 만들려고 하는 것입니다. 하지만 launcher는 그때 즉석에서 만드는 객체가 아니라, 화면이 살아나는 동안 같은 순서로 재등록되어야 하는 콜백 계약에 가깝습니다.

class ProfileFragment : Fragment(R.layout.fragment_profile) {

    private val requestCameraPermission =
        registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
            if (granted) {
                openCamera()
            } else {
                showPermissionDeniedMessage()
            }
        }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        binding.cameraButton.setOnClickListener {
            requestCameraPermission.launch(Manifest.permission.CAMERA)
        }
    }
}

이 구조가 좋은 이유는 등록과 실행 책임이 분리되기 때문입니다. 등록은 화면 초기화 단계에서 끝내고, 실행은 사용자 액션이 왔을 때만 합니다. 이 두 단계를 섞지 않으면 코드가 훨씬 덜 꼬입니다.


예시 1: 권한 요청에서는 무엇이 좋아질까

권한 요청은 새 API의 장점이 가장 잘 드러나는 예시입니다. 예전에는 requestCode를 나중에 다시 해석해야 했지만, 지금은 RequestPermission 계약 자체가 역할을 다 말해 줍니다.

private val requestLocationPermission =
    registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
        if (granted) {
            viewModel.loadCurrentLocation()
        } else {
            showLocationPermissionGuide()
        }
    }

fun onLocationButtonClick() {
    requestLocationPermission.launch(Manifest.permission.ACCESS_FINE_LOCATION)
}

여기서는 결과도 간단합니다. Boolean 하나만 보면 됩니다. 즉, 계약이 결과 타입까지 정리해 준다는 뜻입니다. requestCode 분기와 extras 해석이 사라지니 읽는 부담도 줄어듭니다.

실무에서는 여기서 한 단계 더 나가서, permission 결과를 바로 UI 로직에 섞지 말고 ViewModel 이벤트로 넘기는 편이 좋습니다. 결과를 받은 화면은 입력 경계만 맡고, 실제 상태 변화는 ViewModel이 처리하게 두면 테스트가 쉬워집니다.


예시 2: 사진 선택에서는 왜 더 읽기 쉬워질까

사진 선택도 비슷합니다. GetContent 계약을 쓰면 input은 MIME type, output은 Uri?라는 점이 코드에 바로 드러납니다.

private val imagePicker =
    registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
        if (uri == null) return@registerForActivityResult
        viewModel.updateProfileImage(uri)
    }

fun onPickProfileImageClick() {
    imagePicker.launch("image/*")
}

이 예시는 초보자에게도 장점이 분명합니다. 무슨 작업을 시작하는지와 어떤 결과를 기대하는지가 함수 이름에 그대로 나온다는 점입니다. 나중에 코드를 다시 봐도 추적 비용이 낮습니다.

반대로 onActivityResult에서는 보통 requestCode와 resultCode를 먼저 보고, 그 안에서 다시 data?.data를 확인하고, 어디서 이 requestCode를 날렸는지 거슬러 올라가야 했습니다.

권한 요청과 사진 선택 예시를 요약한 카드
권한 요청과 사진 선택은 새 API 장점이 바로 보이는 대표 예시다

Fragment와 Compose에서는 어떻게 보면 좋을까

Fragment에서는 지금까지 본 패턴 그대로 보면 됩니다. 핵심은 launcher를 필드 수준에서 미리 등록하고, 버튼 클릭 같은 사용자 액션에서는 launch만 호출하는 것입니다.

Compose에서는 보통 rememberLauncherForActivityResult를 같이 보게 됩니다. 개념은 같고, 등록 위치만 Compose 스타일로 옮겨온 것에 가깝습니다. 다만 이번 글의 핵심은 특정 UI toolkit보다 등록은 미리, 실행은 나중이라는 규칙입니다.

@Composable
fun AvatarPicker(onImagePicked: (Uri) -> Unit) {
    val launcher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.GetContent()
    ) { uri ->
        if (uri != null) onImagePicked(uri)
    }

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

즉, API 이름이 달라도 사고방식은 같습니다. 결과 처리 callback을 실행 시점 옆에 두고, 그 launcher를 화면 생명주기 안에서 안정적으로 유지하는 것입니다.


자주 하는 실수 5가지

  1. 버튼 클릭 시점에 launcher를 만들려고 하는 것
  2. 조건문 안에서 registerForActivityResult를 호출해 등록 순서를 흔드는 것
  3. callback 안에서 너무 많은 UI 로직을 직접 처리하는 것
  4. 결과 데이터를 화면 상태에만 두고 회전/재생성 후 흐름을 고려하지 않는 것
  5. 새 API를 쓰면서도 여전히 requestCode 사고방식으로 코드를 설계하는 것

특히 두 번째 실수는 꼭 피해야 합니다. 문서가 “화면이 생성될 때마다 무조건 등록”을 강조하는 이유가 여기 있습니다. 등록 순서가 흔들리면 나중에 결과 전달 흐름을 예측하기가 어려워집니다.


빠르게 고르는 기준

  • 새 안드로이드 코드라면 onActivityResult 대신 registerForActivityResult를 기본값으로 본다
  • 권한 요청, 문서 선택, 사진 선택처럼 표준 contract가 있는 작업은 새 API가 특히 유리하다
  • 결과를 받은 뒤 상태를 오래 유지해야 하면 ViewModel로 넘긴다
  • 등록 시점과 실행 시점을 분리해서 설계한다
  • callback은 가볍게 두고 실제 상태 변경은 별도 로직으로 위임한다


언제는 이것만으로 부족할까

이 API를 쓴다고 해서 결과 처리 설계가 자동으로 좋아지는 것은 아닙니다. callback 안에 화면 로직, 저장 로직, 분석 로그, 네트워크 재호출까지 다 넣기 시작하면 결국 구조는 다시 무거워집니다.

즉 registerForActivityResult는 결과를 안전하게 받는 통로를 주는 것이지, 받은 뒤 책임 분리를 대신해 주는 도구는 아닙니다. 이 경계를 같이 잡아야 실무 코드가 오래 버팁니다.

정리

registerForActivityResult가 더 안전한 이유는 단순히 새 API라서가 아닙니다. 결과 처리 흐름을 contract와 lifecycle 규칙으로 묶어서, 사람이 수동으로 맞춰야 할 조각을 줄여 주기 때문입니다.

권한 요청과 사진 선택 같은 대표 예시를 보면 차이가 더 분명합니다. 예전에는 requestCode와 분기 흐름을 기억해야 했다면, 지금은 launcher 하나만 보면 입력과 결과가 함께 보입니다.

관련해서 같이 보면 좋은 글은 Navigation Component와 FragmentManager 차이, 안드로이드에서 ViewModel이 왜 필요한가, SavedStateHandle과 rememberSaveable 차이입니다.

외부 기준으로는 Android Developers의 Activity Result API 문서ViewModel overview를 같이 보면 흐름이 더 단단하게 잡힙니다.

한 문장으로 줄이면 이렇습니다. onActivityResult는 결과를 사람이 수동으로 분기하던 방식이고, registerForActivityResult는 결과를 계약 단위로 묶어 실수를 줄이는 방식입니다.

함께보면 좋은 글