|

repeatOnLifecycle 없이 Flow를 collect하면 왜 문제가 생길까

repeatOnLifecycle 없이 Flow collect 문제 대표 이미지
Flow collect는 UI lifecycle에 맞춰 시작하고 멈추도록 설계해야 한다.

repeatOnLifecycle은 Android에서 Flow를 UI와 연결할 때 자주 등장합니다. 그런데 왜 그냥 lifecycleScope.launch 안에서 collect하면 안 되는지 헷갈릴 수 있습니다.

핵심은 Flow collect의 수명과 화면의 lifecycle 수명이 자동으로 같지 않다는 점입니다. UI가 멈췄는데도 collect가 계속되거나, 화면이 다시 시작되면서 collect가 중복될 수 있습니다.


Flow collect lifecycle 판단 기준

repeatOnLifecycle Flow collect 판단 카드
Flow는 데이터 흐름이고, UI는 lifecycle을 가진다. 둘 사이를 맞추는 장치가 필요하다.

그냥 launch로 collect하면 무엇이 문제일까

아래 코드는 보기에는 단순합니다. 화면이 만들어질 때 coroutine을 시작하고 Flow를 collect합니다.

lifecycleScope.launch {
    viewModel.uiState.collect { state ->
        render(state)
    }
}

하지만 이 coroutine이 언제 멈추는지 확인해야 합니다. Fragment view lifecycle보다 오래 살아 있거나, 화면이 STOPPED 상태인데도 계속 collect하면 UI 업데이트 타이밍과 수명이 어긋날 수 있습니다.

repeatOnLifecycle은 어떤 기준으로 동작할까

Android Developers의 lifecycle coroutine 문서는 lifecycle-aware coroutine 사용을 안내합니다. repeatOnLifecycle은 지정한 lifecycle state에 들어왔을 때 block을 실행하고, 그 상태보다 낮아지면 실행 중인 작업을 취소했다가 다시 시작하는 패턴으로 사용됩니다.

viewLifecycleOwner.lifecycleScope.launch {
    viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.uiState.collect { state ->
            render(state)
        }
    }
}

이 구조에서는 화면이 STARTED 이상일 때만 collect하고, STOPPED로 내려가면 collect가 멈춥니다. 다시 STARTED가 되면 새로 collect합니다.

Compose에서는 collectAsStateWithLifecycle을 먼저 본다

Android Developers 문서는 Compose에서 Flow를 안전하게 collect하려면 collectAsStateWithLifecycle을 사용할 수 있다고 설명합니다. 이 API는 Flow를 Compose State로 바꾸면서 lifecycle subscription을 관리합니다.

@Composable
fun ConversationRoute(viewModel: ConversationViewModel = viewModel()) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    ConversationScreen(uiState)
}

Compose 화면에서는 직접 lifecycleScope를 꺼내 collect하기보다 UI state로 변환하는 쪽이 더 자연스럽습니다. 화면은 state를 읽고, ViewModel은 StateFlow를 노출하는 구조가 명확합니다.

화면 상태 보존과 navigation 경계는 Compose Navigation 상태 보존 글과도 연결됩니다. Flow collect는 화면 상태가 어디에서 살아야 하는지와 함께 봐야 합니다.

중복 collect는 어떻게 생길까

Fragment의 view가 다시 만들어질 때마다 collect coroutine이 새로 시작되는데 이전 collect가 제대로 취소되지 않으면 같은 Flow를 여러 번 듣는 상태가 됩니다. 그러면 같은 이벤트가 두 번 처리되거나, API 호출이 중복으로 이어질 수 있습니다.

특히 callbackFlow, shared flow event, navigation event처럼 부작용이 있는 흐름은 중복 collect가 더 크게 보입니다.

실무 체크리스트

  • Fragment에서는 viewLifecycleOwner 기준으로 collect하는가?
  • UI가 STARTED 또는 RESUMED일 때만 collect하면 되는 흐름인가?
  • Compose에서는 collectAsStateWithLifecycle을 쓸 수 있는가?
  • 이 Flow는 state인가, 단발 event인가?
  • 화면 재진입 때 collect가 중복으로 시작되지 않는가?

정리

repeatOnLifecycle은 Flow를 더 멋있게 collect하는 문법이 아닙니다. UI lifecycle과 Flow collection의 수명을 맞추는 안전장치입니다. 화면이 보일 때만 필요한 데이터라면 lifecycle-aware collection을 기본값으로 두는 편이 좋습니다.

함께보면 좋은 글