[Jetpack Compose] ProduceState 에 대해 알아보자

오규성·2025년 11월 9일

Jetpack Compose 기초

목록 보기
8/13

# ProduceState<T> 란?

Non-Compose 상태를 Compose 상태로 변환하여 주는 함수로 람다 스코프 내는 CoroutineScope 이기 때문에 이를 활용할 수 있다.

@Composable
fun <T> produceState(
    initialValue: T,
    producer: suspend ProduceStateScope<T>.() -> Unit
): State<T> {
    val result = remember { mutableStateOf(initialValue) }
    LaunchedEffect(Unit) {
        ProduceStateScopeImpl(result, coroutineContext).producer()
    }
    return result
}

내부 코드는 위와 같은데, remember { mutableStateOf(initialValue) } 로 MutableState 를 우선 구현하고 LaunchedEffect 에서 이 값을 업데이트하도록 한다.

LaunchedEffect 의 경우 모든 컴포지션이 완료된 후 실행되므로, initialValue 가 우선 표기된 후 Coroutine 이 실행된다.

produceState 사용처

1초마다 count state 를 늘려주고, 이를 텍스트로 표기하는 코드가 있다고 해보자.
보통 이는 다음과 같이 나타낼 것이다.

@Composable
fun ProduceStateTest(){
    var count by remember { mutableIntStateOf(0) }
    
    Text(text = count.toString())
    
    LaunchedEffect(true) {
        while (true){
            delay(1000)
            count++
        }
    }
}

위의 코드가 틀린 코드는 아니다.

하지만 count 의 경우 var + MutableState 로 되어있기에 새롭게 수정이 가능 하며 코드가 길어진다면 이 count 객체에 대한 변경이 어디서 이루어지는지 헷갈릴 수 있다.

이 때, produceState 를 통해 State 생산자로 만들어주면 다음과 같이 만들 수 있다.

@Composable
fun ProduceStateTest(){
    val count by produceState(initialValue = 0, key1 = true) {
        while (true){
            delay(1000)
            value++
        }
    }
}

이렇게 설정해준다면 count value 를 변경하는 코드를 바로 확인하기도 쉽고, val + State 로 설정되어 있기에 다른 코드에서 값 수정이 불가능해진다.

다른 사용 이유가 또 있을까?

BroadCastReciever 를 사용하여 안드로이드 시스템과 상호작용한다고 생각해보자.

@Composable
fun ProduceStateTest(){
    val context = LocalContext.current
    val battery by produceState<Int?>(initialValue = null) {
        val receiver = object : BroadcastReceiver() {
            override fun onReceive(context: Context, intent: Intent) {
                if (intent.action == Intent.ACTION_BATTERY_CHANGED) {
                    val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
                    val scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
                    value = (level * 100 / scale.toFloat()).toInt()
                }
            }
        }

        context.registerReceiver(receiver, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
    }

    Text(text = battery.toString())
}

위처럼 구현되는 경우 ProduceStateTest() Composable 이 종료된 이후에도 브로드캐스트리시버에 대한 참조가 남아있어 메모리 누수가 발생할 수 있다.

이런 상황들을 대비하기 위해 produceState 에서는 awaitDispose 라는 함수를 제공한다.
이를 사용하면 Composition leave 상태에서 참조 상태를 해제하여 메모리 누수를 방지할 수 있다.

코드를 올바르게 수정하면 다음과 같이 된다.

@Composable
fun ProduceStateTest(){
    val context = LocalContext.current
    val battery by produceState<Int?>(initialValue = null) {
        val receiver = object : BroadcastReceiver() {
            override fun onReceive(context: Context, intent: Intent) {
                if (intent.action == Intent.ACTION_BATTERY_CHANGED) {
                    val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
                    val scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
                    value = (level * 100 / scale.toFloat()).toInt()
                }
            }
        }

        context.registerReceiver(receiver, IntentFilter(Intent.ACTION_BATTERY_CHANGED))

        awaitDispose { context.unregisterReceiver(receiver) }
    }

    Text(text = battery.toString())
}
profile
안드로이드 개발자 Gyu 의 개발 블로그 !

0개의 댓글