[Android] Jetpack Compose란 - State와 remember

Ju-unn·2026년 8월 16일

들어가며

Jetpack 라이브러리들을 정리하고 나니, 그중 UI를 담당하는 Compose를 좀 더 깊게 이해하고 싶어졌다. 특히 State와 remember는 이름만 봐서는 감이 잘 안 왔던 개념이라, 직접 코드를 짜서 동작을 눈으로 확인해보기로 했다.

Compose는 선언형이다

Jetpack Compose는 안드로이드 네이티브 UI를 만들기 위한 선언형 UI 툴킷이다. XML 없이 Kotlin 함수만으로 화면을 정의한다.

선언형이라는 게 처음엔 낯설었는데, "화면을 어떻게 바꿀지"가 아니라 "지금 상태가 주어졌을 때 화면이 어떤 모습이어야 하는지"만 코드로 적는다는 뜻이었다. 상태가 바뀌었을 때 화면을 실제로 어떻게 다시 그릴지는 Compose가 대신 계산한다.

기존 View 방식과 비교하면 차이가 뚜렷하다.

<!-- activity_main.xml -->
<TextView
    android:id="@+id/countText"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />
<Button
    android:id="@+id/increaseButton"
    android:text="증가" />
// MainActivity.kt
val countText = findViewById<TextView>(R.id.countText)
val button = findViewById<Button>(R.id.increaseButton)
var count = 0

button.setOnClickListener {
    count++
    countText.text = count.toString() // 화면 갱신을 직접 호출
}

기존 방식은 "count가 바뀌었으니 countText.text를 바꿔라"라고 매번 직접 지시한다. Compose는 다르다.

@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }

    Column {
        Text(text = count.toString())
        Button(onClick = { count++ }) {
            Text("증가")
        }
    }
}

"count 값이 이것이면 화면에 이 값을 보여줘라"라고 선언만 하고, count가 바뀌면 화면을 다시 그리는 일은 Compose가 알아서 처리한다.

remember가 왜 필요한지 헷갈렸다

Compose에서 화면이 자동으로 갱신되려면 그 값이 Compose가 추적할 수 있는 "State"여야 한다. 그냥 var count = 0처럼 선언하면 값은 바뀌어도 Compose는 그 변화를 알지 못해서 화면이 갱신되지 않는다.

그래서 mutableStateOf로 값을 감싸서 State로 만들고, remember로 값을 기억하게 만든다.

var count by remember { mutableStateOf(0) }

문제는 여기서 "remember 없이 mutableStateOf만 쓰면 어떻게 되는지"가 글로만 봐서는 잘 와닿지 않았다는 점이다. "리컴포지션마다 초기화된다"는 설명은 이해했는데, 실제로 화면에서 어떻게 보이는지 감이 안 왔다.

세 단계로 나눠서 직접 실험해봤다

그래서 세 가지 버전을 나란히 만들어서 비교해보기로 했다.

첫 번째는 State 자체를 안 쓰는 경우다.

@Composable
fun PlainVarCounter(modifier: Modifier = Modifier) {
    var count = 0
    Log.d(TAG, "PlainVarCounter 재구성됨")

    Column(modifier) {
        Text("count: $count (절대 안 바뀜)")
        Button(onClick = { count++ }) { Text("+1") }
    }
}

두 번째는 State는 쓰지만 remember가 없는 경우다.

@Suppress("UnrememberedMutableState")
@Composable
fun NoRememberCounter(modifier: Modifier = Modifier) {
    var count by mutableStateOf(0)
    val recomposeCount = remember { intArrayOf(0) }
    recomposeCount[0]++
    Log.d(TAG, "NoRememberCounter 재구성 #${recomposeCount[0]} count=$count")

    Column(modifier) {
        Text("count: $count (재구성 ${recomposeCount[0]}회, 매번 리셋됨)")
        Button(onClick = { count++ }) { Text("+1") }
    }
}

세 번째는 정석대로 State + remember다.

@Composable
fun RememberCounter(modifier: Modifier = Modifier) {
    var count by remember { mutableStateOf(0) }
    val recomposeCount = remember { intArrayOf(0) }
    recomposeCount[0]++
    Log.d(TAG, "RememberCounter 재구성 #${recomposeCount[0]} count=$count")

    Column(modifier) {
        Text("count: $count (재구성 ${recomposeCount[0]}회, 값 유지됨)")
        Button(onClick = { count++ }) { Text("+1") }
    }
}

두 번째와 세 번째에는 recomposeCount라는 값을 하나 더 넣었다. 이 값 자체는 State가 아니라 remember로 감싼 배열(intArrayOf)이라서 화면 갱신을 직접 트리거하지는 않지만, 함수가 몇 번 재실행됐는지는 remember 덕분에 계속 누적된다. 이걸 넣어두면 "리컴포지션이 일어나긴 했는지"와 "값이 유지되는지"를 동시에 눈으로 구분할 수 있다.

실제로 눌러보니

PlainVarCounter는 버튼을 아무리 눌러도 화면 숫자가 절대 바뀌지 않았다. Logcat에도 최초 composition 시점 로그 한 줄만 찍히고, 버튼을 눌러도 추가로 찍히지 않았다. Compose가 이 값의 변화를 아예 감지하지 못한다는 뜻이다.

NoRememberCounter는 버튼을 누르면 Logcat에 재구성 로그가 계속 찍혔다. 즉 리컴포지션 자체는 매번 일어나고 있었다. 그런데 화면의 숫자는 항상 0에 머물러 있었다. 클릭할 때마다 mutableStateOf(0)이 새로 생성되면서 값이 다시 0으로 시작하기 때문이었다. 로그 상의 재구성 횟수는 계속 올라가는데 count는 그대로 0인 걸 보고서야 "아, remember가 없으면 값 자체가 초기화되는구나"가 확실히 이해됐다.

RememberCounter는 버튼을 누른 만큼 숫자가 그대로 누적됐다. 재구성 횟수도 같이 올라가는 걸 보면, remember가 있다고 리컴포지션 자체가 안 일어나는 게 아니라 "리컴포지션이 일어나도 이전 값을 그대로 들고 있는다"는 게 핵심이라는 걸 알 수 있었다.

정리

세 버전을 나란히 두고 비교해보니 개념이 훨씬 명확해졌다.

  • State가 아니면: 값이 바뀌어도 리컴포지션 자체가 안 일어난다.
  • State지만 remember가 없으면: 리컴포지션은 일어나지만 값이 매번 초기화된다.
  • State + remember면: 리컴포지션이 일어나도 값이 유지된다.

글로만 읽었을 때는 "remember는 값을 기억하는 것"이라는 정도로만 이해했는데, 직접 세 가지를 나란히 실행해보니 remember가 정확히 어느 시점에 무엇을 지켜주는지가 훨씬 뚜렷해졌다. 다음 글에서는 이 리컴포지션이 내부적으로 어떻게 "바뀐 부분만" 골라서 다시 그리는지, Slot Table과 Gap Buffer까지 정리해보려고 한다.

profile
한줄한줄 기록해보자

0개의 댓글