
produceState는 언제 써야 할까를 알면 Compose 상태 관리가 훨씬 덜 헷갈립니다. produceState는 단순히 비동기 코드를 실행하는 도구가 아니라, 외부 값을 Compose가 읽을 수 있는 State로 바꾸는 브리지에 가깝습니다.
이 글에서는 produceState를 LaunchedEffect, remember, collectAsStateWithLifecycle과 비교하면서 설명하겠습니다. 상태를 어디에 둘지 고민된다면 rememberSaveable과 SavedStateHandle 차이 글과 함께 보면 흐름이 더 잘 잡힙니다.

produceState는 언제 써야 할까, 먼저 한 줄로 정리
외부 비동기 소스에서 값을 가져와 composable이 읽을 수 있는 State로 노출해야 할 때 produceState가 잘 맞습니다. suspend 함수 호출, callback 기반 API, 직접 만든 비동기 데이터 소스를 Compose State로 감싸야 하는 경우가 대표적입니다.
- 이미 Flow라면 collectAsStateWithLifecycle을 먼저 검토한다
- 단순 side effect 실행이면 LaunchedEffect가 더 명확할 수 있다
- 외부 비동기 값을 State
로 바꿔 UI가 읽어야 한다면 produceState가 후보가 된다
LaunchedEffect와 무엇이 다를까
LaunchedEffect는 특정 key가 바뀔 때 coroutine을 실행하기 좋은 도구입니다. 하지만 그 결과를 State로 직접 제공해 주지는 않습니다. 그래서 보통 remember로 state를 만들고 LaunchedEffect 안에서 값을 바꾸는 형태가 됩니다.
@Composable
fun UserName(userId: String, repository: UserRepository) {
var name by remember { mutableStateOf<String?>(null) }
LaunchedEffect(userId) {
name = repository.loadUserName(userId)
}
Text(name ?: "불러오는 중")
}이 코드는 틀린 코드는 아니지만, 외부 값을 State로 바꾸는 의도가 여러 줄에 흩어집니다. 같은 일을 produceState로 쓰면 “이 값은 비동기 생산자가 만든 State다”라는 의도가 더 선명해집니다.
@Composable
fun UserName(userId: String, repository: UserRepository) {
val name by produceState<String?>(initialValue = null, userId) {
value = repository.loadUserName(userId)
}
Text(name ?: "불러오는 중")
}collectAsStateWithLifecycle과의 경계
Flow를 UI에서 수집한다면 대부분은 collectAsStateWithLifecycle이 더 자연스럽습니다. lifecycle을 고려한 수집 흐름이 이미 잘 정리되어 있기 때문입니다.
반면 produceState는 Flow 전용 API가 아닙니다. suspend 함수, callback, 직접 만든 listener처럼 아직 Compose State 형태가 아닌 외부 값을 감싸는 데 더 넓게 쓸 수 있습니다. Android 공식 문서의 Compose side-effects 설명에서도 produceState를 외부 subscription을 Compose State로 변환하는 도구로 다룹니다.
produceState가 특히 자연스러운 경우
1. suspend 함수 결과를 화면 상태로 보여줄 때
상세 화면에 들어왔을 때 ID로 데이터를 한 번 가져와 보여주는 흐름이 있습니다. ViewModel로 올릴 정도의 화면 상태가 아니라면 produceState로 composable 근처에서 짧게 표현할 수 있습니다.
2. callback API를 State로 바꿀 때
위치, 센서, 간단한 listener처럼 callback으로 값이 들어오는 API를 UI에서 읽어야 할 때도 produceState가 후보가 됩니다. 이때 awaitDispose로 listener 해제를 처리해야 합니다.
@Composable
fun NetworkStatusText(monitor: NetworkMonitor) {
val isOnline by produceState(initialValue = false, monitor) {
val listener = NetworkListener { online ->
value = online
}
monitor.addListener(listener)
awaitDispose {
monitor.removeListener(listener)
}
}
Text(if (isOnline) "온라인" else "오프라인")
}주의할 점
- 화면 전체 상태와 비즈니스 로직까지 composable 안에 넣는 용도로 쓰지 않는다
- key를 빠뜨리면 입력이 바뀌어도 producer가 다시 실행되지 않을 수 있다
- callback을 등록했다면 awaitDispose로 반드시 해제한다
- Flow 수집은 collectAsStateWithLifecycle이 더 맞는지 먼저 확인한다
produceState는 편하지만, 모든 비동기 상태를 composable 안으로 끌고 오는 명분이 되면 안 됩니다. 화면 전체 의미를 설명하는 상태는 여전히 ViewModel의 uiState로 두는 편이 더 안정적입니다.
마무리
produceState는 외부 비동기 값을 Compose State로 바꿔 UI가 읽게 만드는 도구입니다. LaunchedEffect가 side effect 실행에 가깝다면, produceState는 그 실행 결과를 State 형태로 정리해 주는 역할까지 함께 합니다.
정리하면 Flow는 collectAsStateWithLifecycle, 단순 실행은 LaunchedEffect, 외부 비동기 값을 State로 감싸야 하는 경우는 produceState를 먼저 떠올리면 됩니다.