깨알 개념/Android

[Android] Jetpack Compose - State Remembering

interfacer_han 2024. 7. 23. 00:17

#1 개요

 

상태 및 Jetpack Compose  |  Android Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. 상태 및 Jetpack Compose 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 앱의 상태는 시간이 지남에 따라

developer.android.com

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-practice/jetpack-compose/StateRemembering at master · Kanmanemone/android-practice

Contribute to Kanmanemone/android-practice development by creating an account on GitHub.

github.com