#1 개요
State가 가지는 위치 제약 그리고 해당 제약을 해소하는 방법에 대해 살펴본다.
#2 코드
#2-1 에러가 발생하는 코드 (State object의 위치 제약)
@Composable
fun ButtonExample(modifierParam: Modifier = Modifier) {
val count = mutableStateOf(0)
Button(
onClick = {
Log.i("interfacer_han", "Current count value: ${++count.value}")
}, modifier = modifierParam
) {
Text(
text = "Count: ${count.value}", fontSize = 40.sp
)
}
}
State를 가지고 있는 Composable이다. 하지만 이 코드는 "Creating a state object during composition without using remember" 라는 에러 메시지를 뱉어낸다. 'remember'가 무엇인지는 아래에서 설명하고, 먼저 왜 이런 에러 메시지가 나왔는 지를 알아야 한다. State에 대해 다룬 이 게시글에서 보듯, State가 변하면 해당 State에 연관된 Composable을 다시 로드(재구성, recomposition)한다. Composable이 다시 로드되면 그 속의 State도 다시 초기화된다. 무한 루프에 빠지고 만다. 즉, State는 Composable 내부에 위치할 수 없다는 일종의 위치 제약을 가진다.
여담으로, 애초에 count를 Composable에서 빼 전역 변수로 두면 그만이지 않냐는 생각도 들 수 있다. 하지만, 이는 Jetpack Compose에서 권장되지 않는 방식이다. 왜냐하면 Composable들은 독립적으로 재구성(recomposition)되기 때문이다. 위의 코드에서는 ButtonExample() 단 하나의 Composable만 존재하지만, 실제 프로젝트에서는 수 많은 Composable이 존재할 것이다. 따라서 그 상황에서 전역 변수로 State를 사용하면 나중에 (UI 유지보수 면에서) 감당하기 힘든 코드가 만들어질 여지를 낳게 된다. 하지만 State가 아래에 곧 나올 해결법에 의해 Composable에 에러 없이 들어갈 수있게 된다고 해도 이 또한 여전히 유지보수하기 어려운 코드다. 그 이유는 이어지는 게시글(State Hoisting) 참조.
#2-2 remember를 통한 해결 (State object의 위치 제약 해소)
@Composable
fun ButtonExample(modifierParam: Modifier = Modifier) {
val count = remember {
mutableStateOf(0)
}
Button(
onClick = {
Log.i("interfacer_han", "Current count value: ${++count.value}")
}, modifier = modifierParam
) {
Text(
text = "Count: ${count.value}", fontSize = 40.sp
)
}
}
해결법은 #2-1의 에러 메시지에서 보듯, 그냥 remember { ... }로 State를 감싸버리면 된다. remember는 Composable 내에서만 사용할 수 있는 함수인데, State를 별도 메모리에 저장함으로써, recomposition이 일어나도 그 값이 초기화되지 않게 만든다. 한 마디로 remember는 재구성(recomposition)에 대한 보호막이다.
기억해야할 것은 remember { ... }로 감싸는 대상이 State 프로퍼티가 아니라, State 객체(Object)라는 당연한 사실이다. 즉, 프로퍼티인 count가 아니라 인스턴스인 mutableStateOf(0)를 remember { ... }로 감싼다. State 프로퍼티는 Jetpack Compose 런타임에 의해 어차피 항상 그 value값이 기억되고 모니터링된다. 그래야 State 프로퍼티의 value 속성이 변할 때, 암시적으로 Recomposition을 수행할 수 있기 때문이다. 그러나 State object는 그렇지 않다. 따라서 명시적으로 remember { ... }로 감싸는 처리가 요구되는 것이다.
#2-3 rememberSaveable
@Composable
fun ButtonExample(modifierParam: Modifier = Modifier) {
val count = rememberSaveable {
mutableStateOf(0)
}
Button(
onClick = {
Log.i("interfacer_han", "Current count value: ${++count.value}")
}, modifier = modifierParam
) {
Text(
text = "Count: ${count.value}", fontSize = 40.sp
)
}
}
rememberSaveable은 remember의 심화판이라고 할 수 있다. remember는 recomposition은 막아내지만 화면 회전 등으로 onCreate()가 다시 실행되는 경우에는 State의 초기화를 막을 수 없다. 다시 말하자면, View 자체가 파괴되는 경우는 막을 수 없다. rememberSaveable은 이 경우(View의 파괴 및 재생성)에도 State가 초기화되지 않게 방어한다. 하지만, 남발해서는 안 된다. 짧은 코드라면 모를까, 코드가 길어질 땐 그냥 ViewModel을 쓰는 게 최적의 방식이기 때문이다.
#3 작동 확인 (rememberSaveable을 사용한 경우)
rememberSaveable가 아니라 remember를 썼다면, 화면 회전 전까지는 잘 동작하겠지만, 화면 회전이 되고나서는 count.value가 0으로 초기화된다.
#4 요약
remember는 재구성(recomposition)에 대한 보호막이다.
#5 완성된 앱
'깨알 개념 > Android' 카테고리의 다른 글
[Android] Jetpack Compose - ViewModel에서 State 사용하기 (0) | 2024.07.23 |
---|---|
[Android] Jetpack Compose - State Hoisting (0) | 2024.07.23 |
[Android] Jetpack Compose - State 기초 (0) | 2024.07.22 |
[Android] Jetpack Compose - Lazy Column (0) | 2024.07.22 |
[Android] Jetpack Compose - Button (0) | 2024.07.22 |