
snapshotFlow는 Compose를 조금 깊게 보기 시작하면 자주 마주치는 API입니다.
그런데 이름만 보면 막연합니다.

snapshotFlow는 Compose state 변화를 Flow 파이프라인으로 보내고 싶을 때 쓰는 bridge입니다. 즉 UI를 직접 그리는 도구라기보다, Compose 안의 변화를 Flow operator와 side effect 처리로 연결하는 쪽에 가깝습니다.
즉 화면에 state를 그리기 위한 도구라기보다, Compose 안의 변화를 Flow operator와 side effect 처리로 연결하는 도구에 가깝습니다.
언제 떠올리면 될까
가장 쉬운 판단은 방향을 보는 것입니다.
Flow -> Compose UI로 들어오는 쪽이면collectAsStateWithLifecycle같은 패턴을 먼저 봅니다.Compose state -> Flow pipeline으로 나가는 쪽이면snapshotFlow를 검토합니다.
이 구분만 잡혀도 많이 쉬워집니다.
snapshotFlow가 하는 일
snapshotFlow는 block 안에서 읽은 Compose snapshot state를 관찰합니다.
그리고 그 state가 바뀌면 block을 다시 실행해서 새 값을 emit합니다.
val listState = rememberLazyListState()
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex }
.collect { index ->
println("visible index = $index")
}
}이 코드는 firstVisibleItemIndex를 읽습니다.
이후 collection이 살아 있는 동안 그 값이 바뀌면 flow가 새 값을 흘려보냅니다.
즉 snapshotFlow의 핵심은 Compose state를 읽고, 그 변화를 Flow operator 체인으로 넘길 수 있게 만드는 것입니다.
왜 그냥 LaunchedEffect 안에서 직접 읽으면 안 될까
직접 한 번 읽는 것은 가능합니다.
하지만 그건 한 시점의 값만 보는 것입니다.
LaunchedEffect(listState) {
val index = listState.firstVisibleItemIndex
analytics.log(index)
}이 코드는 effect 시작 시점의 값만 읽습니다.
반면 우리는 종종 스크롤이 바뀔 때마다, 혹은 특정 조건이 달라질 때마다 Flow operator를 연결해서 처리하고 싶습니다.
이럴 때 snapshotFlow가 잘 맞습니다.
대표 예시: 스크롤 analytics
@Composable
fun FeedScreen(
analytics: Analytics,
) {
val listState = rememberLazyListState()
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex }
.map { index -> index > 0 }
.distinctUntilChanged()
.collect { hasScrolled ->
if (hasScrolled) {
analytics.log("feed_scrolled")
}
}
}
LazyColumn(state = listState) {
// ...
}
}여기서 핵심은 세 가지입니다.
- Compose state를 읽는다
- Flow operator로 가공한다
- 최종적으로 analytics 같은 side effect로 보낸다
즉 snapshotFlow는 관찰된 state를 Flow 생태계로 연결하는 첫 관문입니다.
distinctUntilChanged 비슷한 감각이 있다
레퍼런스 설명에 따르면 snapshotFlow는 새 결과가 이전 결과와 equals로 다를 때 emit합니다.
그래서 동작 감각이 distinctUntilChanged()와 비슷하게 느껴질 수 있습니다.
다만 실무에서는 의도를 더 분명히 하려고 operator를 추가로 붙이는 경우가 많습니다.
예를 들어 Boolean으로 바꾼 뒤 distinctUntilChanged()를 붙이면, 팀원이 코드를 읽을 때도 “중복 방지 의도”가 더 잘 드러납니다.
block 안에서 state를 수정하면 안 되는 이유
이 부분은 꼭 조심해야 합니다.
snapshotFlow block은 read-only snapshot에서 실행됩니다.
즉 block 안에서는 snapshot state를 읽는 쪽으로 생각해야 합니다.
snapshotFlow {
count++
count
}이런 식으로 block 안에서 state를 수정하면 실패할 수 있습니다.
공식 레퍼런스도 이런 경우 IllegalStateException 가능성을 명시합니다.
그래서 block은 가능하면 아래처럼 생각하면 좋습니다.
- 읽기 전용
- 부작용 없음
- 같은 입력이면 같은 결과를 내는 쪽
state와 event를 구분해서 보면 더 이해가 쉽다
snapshotFlow 설명에서 중요한 포인트 하나는 state와 event를 구분하라는 점입니다.
예를 들어 현재 스크롤 인덱스는 state에 가깝습니다.
반면 버튼 클릭 한 번, 토스트 한 번, 네비게이션 한 번은 event에 가깝습니다.
snapshotFlow는 state 변화를 바탕으로 Flow를 만드는 도구입니다.
그래서 일회성 event 자체를 만들기 위한 기본 도구로 이해하면 자꾸 헷갈립니다.
언제 잘 맞을까
다음 상황이면 snapshotFlow를 검토할 가치가 큽니다.
- Compose state 변화를 Flow operator와 연결하고 싶다
- debounce, map, filter, distinctUntilChanged를 쓰고 싶다
- analytics, logging, paging trigger 같은 side effect에 연결하고 싶다
- UI state를 coroutine 수집 문맥으로 옮기고 싶다
언제 다른 도구가 더 자연스러울까
반대로 아래 상황이면 다른 도구가 더 맞을 수 있습니다.
- Flow를 화면에 그리려는 목적이다 ->
collectAsStateWithLifecycle - cleanup이 필요한 listener 등록/해제다 ->
DisposableEffect - 한 번성 suspend 작업 시작이 핵심이다 ->
LaunchedEffect - 단순히 파생 UI state가 필요하다 ->
derivedStateOf검토
즉 snapshotFlow는 만능 도구가 아니라 Compose state를 Flow 문맥으로 옮기는 브리지입니다.
빠른 판단 기준
아래 질문으로 고르면 쉽습니다.
- 지금 필요한 것이 화면 렌더링용 state인가, Flow operator 체인인가?
- Compose state 변화에 debounce/filter/distinct를 붙이고 싶은가?
- 결과를 UI 그리기보다 analytics나 후속 side effect에 쓰는가?
이 질문에 예가 많으면 snapshotFlow가 잘 맞을 가능성이 큽니다.
정리
snapshotFlow를 어렵게 느끼는 이유는 Compose와 Flow의 경계에 서 있기 때문입니다.
하지만 역할만 정확히 잡으면 오히려 단순합니다.
Compose 안의 state 변화를 Flow 바깥 처리로 연결하는 도구라고 이해하면 됩니다.
그러면 collectAsStateWithLifecycle, LaunchedEffect, derivedStateOf와의 경계도 훨씬 선명해집니다.
관련해서 같이 보면 좋은 글은 collectAsStateWithLifecycle 정리, LaunchedEffect와 SideEffect 차이, Jetpack Compose 성능은 어디서 느려질까입니다.
함께 보면 좋은 글로는 collectAsStateWithLifecycle 정리, DisposableEffect 정리가 있습니다.
정확한 제약은 Android Developers snapshotFlow API reference를 같이 보는 편이 좋습니다.