Developing Myself Everyday
article thumbnail

사진: UnsplashDavid Libeert

 

 

이번 게시글에서는  Compose에서 빼놓을 수 없는, 모든 곳에 사용되는 Remember에 대하여 정복하고자 합니다.

 

 

 

Remember가 필요한 이유


Remember에 대해 자세히 알아보기 전에, 먼저 Remember가 왜 필요한지 알아보려고 합니다.

 

아래의 Composable 함수를 한번 보겠습니다.

@Composable
fun Counter() {

    var counter by mutableStateOf(0)

    Column(
        modifier = Modifier.fillMaxSize()
    ) {
        Text(
            text = "Counter: $counter",
        )
        Button(
            onClick = {
                counter++   
            }) {
            Text(
                text = "더하기",
            )
        }
    }
}

 

이 함수는 정수형을 가지고 있는 counter 변수와 이를 나타내는 Text 그리고 counter의 값을 + 1하는 Button이 있습니다.

 

Compose에 대해서 이전에 접해보지 않으신 분들이라면 해당 코드를 보면, 문제가 없다고 생각하실 수 있을것 같습니다.

 

 

하지만 코드를 직접 작성해 보시면 아래와 같은 에러를 확인하실 수 있을 겁니다.

 

 

에러는 다음과 같이 말합니다.

 

State objects created during composition need to be `remember`ed, otherwise they will be recreated during recomposition, and lose their state. Either hoist the state to an object that is not created during composition, or wrap the state in a call to remember.

composition 중에 생성된 객체는 상태를 `remember`하지 않으면, Recomposition 중에 다시 생성되기 때문에 값이 손실됩니다. 상태를 composition 중에 생성되지 않는 객체로 hoist하거나 상태를 `remember 호출로 래핑하세요.

 

이 짧은 문구에는 우리가 알아야하고 이해해야 하는 많은 것들이 담겨있습니다. 이제부터 이 말을 쉽게 이해할 수 있게 차근차근히 정리해나갈 것입니다.

 

 

 

 

Recomposition으로 인해 재할당


Recomposition에 대해 잘 모르신다면, Compose의 Lifecycle에 대한 아래의 그림을 보시면 좋을 것 같습니다.

 

Compose는 Composable을 함수에 들어가면, Composition이 시작됩니다. 이렇게 한번 Composition이 이루어진 Composable은 가지고 있는 상태가 바뀌기 전까지는 화면을 다시 그리지 않습니다. 

 

만약 내부의 상태가 바뀐다면, 바뀐 상태를 기반으로 Composable 함수는 다시 그려집니다. 그럼 이제 다시 아까의 Counter 예제를 보겠습니다.

 

@Composable
fun Counter() {

    var counter by mutableStateOf(0)

    Column(
        modifier = Modifier.fillMaxSize()
    ) {
        Text(
            text = "Counter: $counter",
        )
        Button(
            onClick = {
                counter++   
            }) {
            Text(
                text = "더하기",
            )
        }
    }
}

 

위의 카운터는 `counter`라는 상태를 가지고 있습니다. 그렇기 때문에 Button의 클릭 이벤트로 상태가 바뀌면 해당 Composable은 Recomposition이 되게 됩니다. 

 

하지만, 지금은 Recomposition이 되더라도 counter는 초기값 0으로 다시 할당되게 됩니다. 이런 이유 때문에 우리는 위의 방식으로는 아무리 Button을 클릭하더라도 다른 값을 볼 수 없습니다.

 

 

 

 

그럼 Remember는??


Remember는 이전의 상태를 기억합니다. 아래의 코드를 살펴보겠습니다.

@Composable
inline fun <T> remember(crossinline calculation: @DisallowComposableCalls () -> T): T =
    currentComposer.cache(false, calculation)

 

`@DisallowComposableCalls`는 말 그대로 remember 안에 Composable 함수를 넣지 못하게 하겠다는 말입니다. 만약 내부에 Composable 함수가 들어갈 수 있다면, 예측할 수 없는 행위가 반복될 수 있고, 무한 루프에 빠질 수 있습니다.

 

currentComposer이 부분이 remember의 핵심 부분입니다. 사실 이를 설명하기 위해서는 Compose의 메모리 구조에 대해 설명해야할 필요가 있습니다.

 

간단하게 설명하자면, Compose의 메모리 구조는 Gap Buffer 구조로 되어있습니다. 이는 아래의 그림처럼 `Gap` 이라는 빈 공간을 가지고 있고, Composable의 함수는 이러한 Gap BufferGap에 들어가게 됩니다.

 

 

 

 

위의 그림의 빨간색 화살표를 잘 보시길 바랍니다. 저 빨간색 화살표는 Cursor로 새로운 Composable을 삽입하거나, Recomposition될 때, 해당 위치에서 삽입이 이뤄진다는 것을 의미합니다.

 

이러한 삽입과 메모리 저장, 그리고 다양한 작업은 바로 `Composer`에 의해 이뤄집니다.

 

Composer는 코틀린 컴파일러 플러그인의 타켓이 되어 코드 생성 도우미가 사용하는 인터페이스라고 설명되어 집니다. 다만, 말이 조금 어렵기에 그냥 Composition 해주는 도구라고 생각하겠습니다.

 

 

이제 아래와 같이 remember로 상태를 감싼 다음,  Recomposition이 발생했다고 생각해 보겠습니다.

@Composable
fun Counter() {

    var counter by remember { mutableStateOf(0) }

    Column(
        modifier = Modifier.fillMaxSize()
    ) {
        Text(
            text = "Counter: $counter",
        )
        Button(
            onClick = {
                counter++
            }) {
            Text(
                text = "더하기",
            )
        }
    }
}

 

 

Recomposition간에 기존에 저장된 상태는 remember에 의해 유지됩니다. 만약 새로운 값이 `key1`로 들어온다면 `calculation` 함수는 값이 변경됨을 감지하여 값을 변경할 수 있게 합니다.

@Composable
inline fun <T> remember(
    key1: Any?,
    crossinline calculation: @DisallowComposableCalls () -> T
): T {
    return currentComposer.cache(currentComposer.changed(key1), calculation)
}

 

 

 

만약, 왼쪽의 Gap에 `Group(123)`이라는 데이터를 넣은 다음, Recomposition을 통해 새로운 값 `Group(456)`이 들어온다고 생각해 보겠습니다.

 

 

그렇다면, remember는 기존의 위치를 기억하고, 해당 위치로 Cursor를 이동시킵니다. 그리고 해당 위치에 값을 새롭게 바꾸게 됩니다.

 

 

 

 

Remember의 한계


이렇게 만능같은 Remember에도 한계는 있습니다. 바로 화면 전환과 같은 액티비티가 재구성될 때에는 기존의 값을 기억하지 못한다는 것입니다.

 

그럼 우리는 어떻게 해야할까요?? 바로 `rememberSaveable`을 사용하면 됩니다.

@Composable
fun Counter() {

    var counter by rememberSaveable { mutableStateOf(0) }

    Column(
        modifier = Modifier.fillMaxSize()
    ) {
        Text(
            text = "Counter: $counter",
        )
        Button(
            onClick = {
                counter++
            }) {
            Text(
                text = "더하기",
            )
        }
    }
}

 

 

rememberSaveable의 원리는 생각보다 간단합니다. 기존의 xml의 방식에서 액티비티 재구성에 값을 유지하기 위해서 사용했던 `savedInstanceState` 사용하는 겁니다. 그렇기 때문에 동일하게 저장할 수 있는 값은 Bundle이 지원하는 데이터 유형만 가능합니다.

 

rememberSaveable은 Bundle로 된 savedInstatanceState를 구현하여 액티비티 재구성간에 상태를 유지합니다.

 

 

 

 

Custom RememberSaverable


위에서 말했듯이 rememberSaverable은 원시 데이터 유형만 지원합니다. 그렇기 때문에 우리가 원하는 형태의 데이터를 저장하기 위해서는 Saver를 Custom하게 정의해줘야 합니다.

Saver
Saver는 Original 클래스의 객체를 단순화하고 Saveable로 변환하는 방법을 설명하는 인터페이스 입니다.

 

 

아래에 다음과 같은 데이터 클래스가 있습니다.

data class Person(val name: String, val age: Int, val isMan: Boolean)

 

 

 

사실 이러한 데이터 클래스를 가장 쉽게 rememberSaverable로 저장할 수 있는 방법은 `@Parcelize` 어노테이션을 사용하는 방법입니다.

@Parcelize
data class Person(val name: String, val age: Int, val isMan: Boolean): Parcelable

 

 

다만 이는 외부 라이브러리 이므로, 새롭게 추가해줘야 합니다. 그러니 우리는 Custom하게 Saver를 구현하는 방법을 간단하게 알아보겠습니다.

 

먼저 Saver은 아래와 같이 2개의 메서드가 존재합니다. 

interface Saver<Original, Saveable : Any> {
    /**
     * Convert the value into a saveable one. If null is returned the value will not be saved.
     */
    fun SaverScope.save(value: Original): Saveable?

    /**
     * Convert the restored value back to the original Class. If null is returned the value will
     * not be restored and would be initialized again instead.
     */
    fun restore(value: Saveable): Original?
}

 

먼저 `save()`는 값을 저장 가능한 형태로 변환합니다. `restore()`은 변환한 값을 다시 원래의 값으로 변환합니다.

 

그렇기에 saver를 구현하기 위해서는 이 2개의 메서드를 구현해야 합니다.

 

 

mapSaver

첫번째 방법은 mapSaver를 사용하는 방법입니다. 이는 map의 형태로 데이터를 저장하고, 다시 찾아옵니다.

@Composable
fun Person() {
    val person = rememberSaveable(saver = PersonSaver.saver) {
        Person(
            "준형",
            27,
            true
        )
    }
}

object PersonSaver {
    val saver = mapSaver(
        save = { person: Person ->
            mapOf(
                "name" to person.name,
                "age" to person.age,
                "isMan" to person.isMan,
            )
        },
        restore = {
            Person(
                it["name"] as String,
                it["age"] as Int,
                it["isMan"] as Boolean,
            )
        }
    )
}

 

 

 

listSaver

두번째 방법은 listSaver를 사용하는 방법입니다. list의 형태로 데이터를 저장하고 index를 통해 다시 찾아옵니다.

object PersonSaver {
    val saver = listSaver(
        save = { person: Person ->
            listOf(
                person.name,
                person.age,
                person.isMan,
            )
        },
        restore = {
            Person(
                it[0] as String,
                it[1] as Int,
                it[2] as Boolean,
            )
        }
    )
}

 

 

 

 

 

마무리하며


사실 xml에서도 화면의 재구성간에 데이터를 유지하기 위해서 사용했던 방법들은 여러가지 였습니다. 그 중, 저는 ViewModel을 데이터 홀더로 사용하여 데이터를 유지해 왔습니다.

 

Compose도 이와 마찬가지입니다. 다양한 방법 중 하나를 채택해서 상태를 유지하면 됩니다.

 

이렇게 해서 remember가 무엇인지 알아보고 내부 구조도 간단하게 알아보았습니다. 이젠 누군가 remember에 대해 질문한다면, 어렵지 않게 대답할 수 있을 것 같습니다.

 

 

 

 

 

Reference

 

Everything You Need To Know About Remember In Android Jetpack Compose

In this blog, we are going to talk about the importance of using remember in our state.

medium.com

 

Compose에 UI 상태 저장  |  Jetpack Compose  |  Android Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. Compose에 UI 상태 저장 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 상태가 호이스팅된 위치와 필요

developer.android.com

 

'Android > Compose' 카테고리의 다른 글

Compose의 WindowInsets  (0) 2024.04.23
Compose의 SnapShot 시스템  (0) 2024.03.29
Compose의 버전 관리: BOM!!  (0) 2024.02.19
내가 Compose로 만든 앱의 화면이 버벅거린다면?  (1) 2024.01.14
Compose의 Layout 단계  (0) 2023.10.24
profile

Developing Myself Everyday

@배준형

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!