|

collectAsStateWithLifecycle 정리

collectAsStateWithLifecycle 대표 이미지
Compose 화면 수집은 값 자체보다 lifecycle에 맞는 시점이 더 중요하다

collectAsStateWithLifecycle은 Compose에서 Flow를 화면 상태로 바꿔 읽을 때 생명주기를 같이 고려하게 도와주는 API입니다. 즉 화면이 보일 때만 수집하고, 보이지 않을 때는 불필요한 수집을 줄이는 쪽에 더 가깝습니다.

Compose를 처음 쓰면 ViewModel의 StateFlow를 그냥 collectAsState()로 연결해도 일단 돌아가는 경우가 많습니다. 문제는 화면이 백스택에 들어가거나, 다른 화면 위로 가려지거나, 다시 돌아오는 순간부터 보이기 시작합니다.

공식 API 문서도 collectAsStateWithLifecycle이 Flow 값을 lifecycle-aware하게 State로 노출하고, 기본 최소 활성 상태를 STARTED로 둔다고 설명합니다. 이 글에서는 그 의미를 실무 예시 위주로 다시 풀어보겠습니다.


왜 그냥 collect하면 문제가 될까

문제의 핵심은 “값을 받는다”가 아니라 “화면이 어떤 상태일 때 값을 받느냐”입니다. 화면이 안 보이는데도 계속 수집하면 불필요한 계산, 중복 로그, 쓸데없는 네트워크 요청, 화면 복귀 시 어색한 재동기화가 생길 수 있습니다.

Flow 수집과 lifecycle 상태 관계를 보여주는 다이어그램
보이는 동안만 UI 수집을 붙잡는 것이 핵심이다
  • 화면이 백그라운드인데도 UI 수집이 계속 돈다
  • 다시 돌아왔을 때 이미 오래된 side effect가 한꺼번에 반영된다
  • 화면은 사라졌는데 collector 안에서 불필요한 작업이 이어진다
  • 화면 수집과 비즈니스 작업의 경계가 흐려진다

특히 UI가 해야 할 일은 “상태를 보이는 동안 그리기”인데, 개발자가 collector 안에 너무 많은 일을 넣으면 화면 수집이 비즈니스 작업까지 끌고 다니게 됩니다.


collectAsStateWithLifecycle이 실제로 하는 일

이 API는 Flow나 StateFlow를 Compose의 State로 바꾸는 역할은 유지하면서, 그 수집을 lifecycle-aware하게 붙입니다. 쉽게 말해 Compose에서 읽기 좋은 상태 형태Android 화면 생명주기를 같이 맞춰 주는 중간 다리입니다.

@Composable
fun UserScreen(viewModel: UserViewModel) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()

    when {
        uiState.isLoading -> LoadingView()
        uiState.error != null -> ErrorView(uiState.error)
        else -> UserContent(uiState.user)
    }
}

겉보기에는 collectAsState()와 비슷하지만, 차이는 “언제 수집을 붙들고 있을 것인가”에 있습니다. 이 한 줄 덕분에 화면이 STARTED 이상일 때만 UI 쪽 수집이 이어집니다.

collectAsStateWithLifecycle 선택 기준 요약 카드
UI 수집은 화면 수명과 같이 움직여야 덜 꼬인다

collectAsState와는 무엇이 다를까

collectAsState()가 틀렸다는 뜻은 아닙니다. 다만 Android 화면에서 lifecycle-aware 수집이 필요한 경우가 훨씬 많기 때문에, 화면 상태를 직접 그리는 용도라면 collectAsStateWithLifecycle()가 더 안전한 기본값에 가깝습니다.

  1. 플랫폼 중립 Compose 샘플이나 아주 단순한 경우에는 collectAsState도 쓸 수 있다
  2. 안드로이드 화면에서 ViewModel의 StateFlow를 UI에 연결하는 기본 상황에서는 collectAsStateWithLifecycle이 더 자연스럽다
  3. UI 수집과 별개로 실제 비즈니스 작업은 ViewModel 쪽에서 관리해야 한다

화면에서 읽는 방식을 바꾼다고 해서 비즈니스 로직 위치까지 바뀌는 것은 아닙니다. 이 점을 놓치면 collectAsStateWithLifecycle만 붙이고도 여전히 collector 안에 무거운 일을 넣게 됩니다.


실전 예시 1: 검색 화면에서 어떤 경계가 생길까

class SearchViewModel : ViewModel() {
    private val _uiState = MutableStateFlow(SearchUiState())
    val uiState: StateFlow<SearchUiState> = _uiState

    fun onQueryChanged(query: String) {
        _uiState.update { it.copy(query = query) }
    }
}

@Composable
fun SearchScreen(viewModel: SearchViewModel) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()

    SearchContent(
        query = uiState.query,
        onQueryChanged = viewModel::onQueryChanged,
        items = uiState.items
    )
}

이 예시에서 중요한 점은 검색 요청을 UI가 직접 수집해서 처리하지 않는다는 것입니다. UI는 상태를 읽고, 입력 이벤트를 ViewModel로 보내는 데 집중합니다. 검색 실행과 취소 정책은 ViewModel 쪽의 Flow 연산에서 다루는 편이 더 낫습니다.

그래서 collectLatest vs collect 글과 이 글은 역할이 다릅니다. 그 글은 “이전 작업을 언제 취소할까”를 다루고, 이 글은 “UI 수집을 언제 붙잡고 있을까”를 다룹니다.


실전 예시 2: 화면 복귀와 백스택에서 왜 차이가 체감될까

예를 들어 목록 화면 A가 있고, 상세 화면 B로 이동했다가 다시 돌아오는 상황을 생각해 보겠습니다. A 화면이 보이지 않는 동안에도 UI 수집이 계속 돌아가면, A는 사용자가 보지 않는 상태 업데이트까지 계속 소비할 수 있습니다.

반대로 lifecycle-aware 수집을 쓰면 A는 보일 때 다시 최신 상태를 그리는 데 집중합니다. 화면이 안 보이는 동안의 모든 중간 과정을 UI가 굳이 다 소화할 필요는 없기 때문입니다.

collectAsStateWithLifecycle 관련 자주 하는 실수 카드
UI 수집과 작업 수행을 한 블록으로 섞기 시작하면 금방 꼬인다

repeatOnLifecycle과는 어떤 관계일까

Compose 밖에서는 lifecycle-aware collection을 위해 repeatOnLifecycle 패턴을 자주 봅니다. Compose에서는 그 감각을 좀 더 자연스럽게 가져오도록 collectAsStateWithLifecycle() 같은 API가 제공된다고 이해하면 편합니다.

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

즉 View 시스템 쪽에서 하던 lifecycle-aware 수집 감각을 Compose 쪽에서 더 짧고 명확하게 가져오는 셈입니다.


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

  • collector 안에서 네트워크 호출, 저장, 로그 같은 side effect를 직접 처리할 때
  • 최신 값만 남길지 모든 값을 처리할지 취소 정책 자체가 중요한 경우
  • 화면 수집이 아니라 Repository나 ViewModel 계층의 장기 작업을 다루는 경우

이런 경우에는 단순히 collectAsStateWithLifecycle()를 붙이는 것보다, 상태 생산 위치와 side effect 위치를 다시 나누는 편이 더 중요합니다.


빠르게 결정하는 기준

  1. Compose 화면에서 ViewModel의 상태를 읽어 그리는가? → collectAsStateWithLifecycle을 먼저 떠올린다
  2. 지금 하고 싶은 일이 UI 그리기인지, 실제 작업 수행인지 구분한다
  3. 취소 정책이 중요하면 collectLatest 등 Flow 연산을 ViewModel 쪽에서 설계한다
  4. 일회성 이벤트는 상태와 섞지 말고 별도 event 흐름으로 본다

상태와 이벤트를 어디서 나눌지 감이 흐리다면 안드로이드 UDF 글과 함께 보는 것이 좋습니다.



StateFlow를 화면에서 읽는다는 말의 뜻

많은 입문자가 StateFlow를 화면에서 읽는 일을 단순 값 구독으로 생각합니다. 하지만 실제로는 어떤 lifecycle 상태에서 UI가 값을 소비할지까지 같이 설계해야 합니다.

즉 collectAsStateWithLifecycle은 Compose 문법 편의가 아니라, UI가 상태를 읽는 타이밍을 안드로이드 생명주기에 맞추는 쪽에 더 가깝습니다.

마무리

collectAsStateWithLifecycle이 필요한 이유는 Compose가 부족해서가 아니라, 안드로이드 화면에는 원래 lifecycle이라는 변수가 있기 때문입니다. 화면이 보이는 동안만 상태를 읽는다는 기준을 붙여야 UI가 덜 꼬입니다.

짧게 정리하면 이렇습니다. 화면 상태 수집은 단순 collect 문제가 아니라 lifecycle 문제이고, Compose에서는 그 기본 해법으로 collectAsStateWithLifecycle을 먼저 떠올리는 편이 안전합니다.

함께보면 좋은 글