지난 글: [Android] Compose의 모든 것(4) - Recomposition의 동작 원리와 Compose UI 트리 구성 파헤치기
https://yujinius45.tistory.com/142
[Android] Compose의 모든 것(4) - Recomposition의 동작 원리와 Compose UI 트리 구성 파헤치기
지난 글: [Android] Compose의 모든 것(3) - State, MutableState, remember, rememberSaveable, by, = 선언 방식 차이https://yujinius45.tistory.com/141 [Android] Compose의 모든 것(3) - State, MutableState, remember, rememberSaveable, by, = 선
yujinius45.tistory.com
그렇다면 이러한 ComposeNode로 UI 트리가 구성된다는 것은 알았는데 State, key 같은 것은 어디에 저장되어 관리될까?
이번 글에서는 Slot Table과 Stability, Key를 중심으로 Jetpack Compose의 Recomposition 최적화 원리를 살펴보겠다.
오늘의 주요 포인트 ⭐
- Slot Table: 상태와 구조 관리, remember와 rememberSaveable 비교.
- Key 사용법: Key의 역할과 Recomposition 최적화.
- Stability와 Skipping: Stability의 정의, Stable/Immutable 객체 사용.
- Recomposition 범위 제어: Stable 객체, State Hoisting, Composable 분리.
Slot Table과 상태 관리
Jetpack Compose는 UI 상태를 효율적으로 관리하기 위해 Slot Table을 사용한다. Slot Table은 UI 트리의 상태와 구조를 저장하는 데이터 구조로, Recomposition 시 이전 상태와 새로운 상태를 비교하고 필요한 부분만 업데이트할 수 있도록 돕는다.

Slot Table이란?
Slot Table은 UI 트리를 효율적으로 저장하기 위한 내부 데이터 구조로, 다음을 관리한다:
- Composable 함수 호출 위치: 이전 Composition에서 호출된 함수 위치를 추적하여, 이를 통해 Recomposition 시 변경된 위치만 다시 실행할 수 있도록 한다.
- 상태(State): 각 Composable에서 사용된 상태 정보를 저장.
- Key와 Group: UI 요소를 그룹화하고 고유 키를 통해 트리 구조를 관리.
우선은 Slot Table에 함수 블록들과 상태, key 등을 관리하고 있다고 생각하면 된다.
내부적으로 동작하는 자세한 순서와 과정을 확인하고 싶다면 아래의 영상을 추천한다.
https://youtu.be/Q9MtlmmN4Q0?si=IEsUuBU5zTNpOV5K
State와 Slot Table의 관계
Slot Table과 remember
- remember는 내부적으로 currentComposer.cache를 호출하여 Slot Table에 상태를 저장한다.
- Slot Table은 Composition 과정에서 Composable 함수 호출 위치와 상태 정보를 관리하며, 이를 기반으로 상태를 재사용하며, currentComposer.cache는 Slot Table에 상태를 저장하거나, 기존 상태를 반환한다.
- 이로 인해 동일한 상태가 Recomposition 중에도 유지된다.
State가 변경되었을 때
- Slot Table은 state를 읽는 시점을 기록하고 추적한다.
- state가 변경되면, 이를 감지하여 관련 Composable 함수가 Recomposition된다.
var count by remember { mutableStateOf(0) }
- 위의 예로 살펴보면 mutableStateOf는 Snapshot 시스템을 통해 상태 변경을 추적하고 Slot Table과 연결된다. 그리고 상태가 변경되면 Slot Table에서 이를 감지하여 관련된 UI만 다시 그리게 된다. 이를 Recomposition이라고 한다.
그렇다면 rememberSaveable는??
- rememberSaveable은 Jetpack Compose에서 구성 변경(Configuration Change)에도 상태를 유지할 수 있도록 돕는 메커니즘이다.
- remember와 rememberSaveable의 차이는 다음과 같다.
- remember: Recomposition이 발생해도 상태를 유지하지만, Configuration Change 시 상태가 사라진다.
- rememberSaveable: 구성 변경 시에도 상태를 유지하며, 저장 가능한 값(Bundle)을 사용한다.
rememberSaveable은 어떻게 동작하는가?
- Bundle 저장:
- rememberSaveable은 상태 값을 Bundle에 저장할 수 있는 형태로 변환한다.
- Parcelable 또는 Serializable 객체만 저장 가능하다.
- Slot Table과 병행:
- Recomposition에서는 Slot Table을 활용해 상태를 관리한다.
- Configuration Change 시에는 저장된 Bundle에서 값을 복원한다.
예제:
@Composable
fun RememberSaveableExample() {
val count = rememberSaveable { mutableStateOf(0) }
Button(onClick = { count.value++ }) {
Text("Count: ${count.value}")
}
}
동작 과정:
- 초기 상태:
- rememberSaveable이 상태를 초기화하고 Slot Table에 저장.
- 구성 변경 발생:
- Bundle에 저장된 상태 값이 복원되어 UI에 전달.
- Recomposition:
- Slot Table을 통해 상태를 추적하며, 필요한 부분만 다시 구성.
Slot Table과 rememberSaveable
- Compose는 Slot Table을 기반으로 상태를 관리하며, Configuration Change 시에는 Bundle에 저장된 데이터를 활용한다. 이를 통해 상태를 안정적으로 유지하면서도 Recomposition 최적화를 가능하게 한다.
Key를 활용한 Recomposition 최적화
Key란 무엇인가?
- Compose에서 각 UI 요소를 고유하게 식별하기 위한 값이다.
- Key는 Recomposition 시 변경된 부분만 다시 그릴 수 있도록 UI 요소를 구분하는 역할을 한다.
- LazyColumn에서 key 설정하는 그 key가 이 key가 맞다.

Key 사용 원리 (위의 Composer.kt 코드 참고)
- startMovableGroup(key: Int, dataKey: Any?)
- 새로운 키로 그룹을 시작하며, 해당 그룹의 상태를 Slot Table에 저장.
- key 값 변경 감지:
- 키 값이 동일하면 이전 그룹의 상태를 재사용.
- 키 값이 변경되면 이전 그룹을 폐기하고 새로 구성.
예제:
@Composable
fun DynamicList(items: List<String>) {
Column {
items.forEach { item ->
key(item) { // 고유 키를 설정
Text(text = item)
}
}
}
}
Key 관리 흐름:
- key를 설정하여 각 항목의 고유성을 보장.
- Slot Table에서 이전 키와 비교하여 동일한 키는 상태를 재사용.
- 변경된 키는 새로 생성하여 Recomposition을 최적화.
만약 key가 없었다면?

- 위와 같이 동일한 내용의 composable도 새로 생성될 것이다. 리스트에 여러 변경이 일어날 때마다 모든 composable이 다시 생성되는 것이다.
key가 있다면!

- 그러나 key를 지정해주면 목록에 새 요소가 추가될 때 고유 key를 확인하여 변경되지 않은 MovieOverview 인스턴스를 인식하고 재사용할 수 있게 된다.
항목 순서 변경에서 발생할 수 있는 문제
만약 key를 사용하지 않고, 리스트 항목의 순서가 변경되거나 요소가 추가되었다면, Slot Table은 이전 상태를 순서대로 매핑하려고 시도한다. 이 경우, 각 항목의 고유성을 보장할 수 없기 때문에 다음과 같은 문제가 발생할 수 있다:
- 잘못된 상태 매핑
- 이전 항목의 상태가 새 항목으로 잘못 매핑되어, UI가 의도한 대로 동작하지 않을 수 있다.
- 불필요한 재구성
- 변경되지 않은 항목도 상태를 재사용하지 못하고 새로 생성되므로 성능 저하가 발생한다.
이에 대해서 직접 확인해보고 싶다면 Lazy Column에서 key 없이 진행해보길 바란다.
참고 링크: https://developer.android.com/develop/ui/compose/lifecycle?hl=ko
Key와 State의 관계는 그래서 무슨 차이인가?
- key:
- Slot Table에서 그룹을 식별하고, 동일한 그룹의 상태를 재사용하거나 새로 생성하는 데 사용된다.
- 이는 UI 요소 간의 상태 매핑을 관리한다.
- remember로 저장된 상태:
- remember는 Slot Table 내부에 상태를 저장하며, Recomposition 시 이를 기반으로 상태를 유지하거나 업데이트한다.
Slot Table과 Recomposition 정리
- 초기 Composition:
- Slot Table에 UI 상태와 구조를 저장.
- Recomposition 발생:
- 상태 변경 시 Slot Table을 참조하여, 변경된 부분만 다시 구성.
- Key 기반 최적화:
- Key를 통해 동일한 상태를 재사용하거나, 새로 생성하여 필요한 부분만 업데이트.
이제 Composable의 트리 구성 방식을 이해했으니, 다음 단계로 Recomposition이 이 트리에서 어떤 방식으로 동작하고 최적화되는지 살펴보자.
Compose 내부에서 Recomposition의 동작
- Compose는 Composer.kt와 Composables.kt에 정의된 다양한 함수들을 활용하여 Recomposition을 처리한다.

Composer.kt
Composer.kt는 Composition 상태를 관리하고 Recomposition을 실행하는 주요 역할을 한다.
주요 함수
- startReplaceableGroup: Composition의 그룹을 시작한다. 상태 추적의 단위가 된다.
- changed(): 이전 상태와 현재 상태를 비교하여 변경 여부를 확인한다.
- invalidate(): 상태 변경 시 Recomposition이 필요하다고 표시한다.
- skipToGroupEnd(): 변경되지 않은 그룹을 스킵하여 효율성을 높인다.
Composables.kt
Composables.kt는 Recomposition과 관련된 주요 유틸리티 함수들을 제공한다.
주요 함수
- remember: 상태를 Composition에 저장하여 Recomposition 중에도 값을 유지한다.
- key: 그룹에 고유한 키를 부여하여, 상태를 올바르게 연결한다.
- ReusableContent: 상태를 유지한 채로 UI 요소를 재사용할 수 있도록 한다.
Recomposition의 동작 예시
상태 읽기 및 변경 감지
val count = remember { mutableStateOf(0) }
@Composable
fun Counter() {
Text(text = "Count: ${count.value}")
}
위 코드에서 count.value를 읽는 순간, Compose는 이 상태를 구독(subscribe)하며, count가 변경되면 Recomposition을 발생시킨다.
Recomposition 스킵핑과 최적화
Compose는 상태가 변경되지 않은 부분은 Recomposition을 스킵한다. 이는 changed()와 skipToGroupEnd()를 통해 이루어진다.
Stablility에 따른 skipping으로 Recomposition 최적화
Stability에 따라 skipping하는 것은 Jetpack Compose에서 컴포지션(Composition) 성능을 최적화하기 위해 사용하는 메커니즘이다. Stability는 컴포지션에서 특정 값이나 객체가 변경되지 않았음을 판단하는 기준으로 사용되며, 이를 기반으로 Recomposition에서 일부 컴포지션을 건너뛸 수 있다.
Stability란 무엇인가?
Stability는 Compose가 객체의 안정성을 평가하는 기준이다. 특정 값이나 객체가 변경되지 않았다고 Compose가 판단할 수 있으면, 해당 값이나 객체와 관련된 컴포지션 과정을 건너뛸 수 있다. 이를 통해 불필요한 재구성을 줄이고 성능을 최적화할 수 있다.
Skipping이란?
Skipping은 컴포지션 과정에서 상태가 안정적(Stability)이라고 판단되면, 해당 상태에 연결된 UI를 재구성하지 않고 건너뛰는 최적화 과정이다. Stability를 평가하여 변경되지 않은 부분을 Recomposition하지 않으므로, 불필요한 계산과 작업을 피할 수 있다.
Stability에 따른 3가지 종류
Stable, Unstable, Immutable의 차이
- Stable: 객체의 속성이 변경될 수 있으나, 이러한 변경 사항을 Compose 런타임이 추적할 수 있는 상태
- 모든 속성이 val로 선언된 데이터 클래스는 안정적으로 간주
- @Stable 또는 @Immutable 어노테이션을 사용하여 클래스의 안정성을 명시적으로 지정 가능
- 안정적인 객체는 상태 변경 시에만 재구성이 발생하므로, 불필요한 재구성을 줄일 수 있음
- Unstable: 객체의 속성이 변경될 수 있으며, Compose가 이러한 변경 사항을 추적할 수 없는 상태를 의미
- var로 선언된 속성을 가진 클래스는 불안정한 것으로 간주
- List, Map 등과 같은 컬렉션 타입은 기본적으로 불안정한 것으로 간주
- 불안정한 객체는 상태 변경 여부와 관계없이 항상 재구성이 발생하여 성능 저하를 유발할 수 있으며, @Stable, @Immutable을 추가하여 관리 가능.
- Immutable: 객체의 모든 속성이 생성 후 변경되지 않는다고 함.
- String, Int, Float 등과 같은 기본 타입은 본질적으로 불변
- 불변 객체는 상태 변경이 없으므로, Recomposition에서 skip 대상.
@Immutable
data class User(val name: String)
@Composable
fun UserCard(user: User) {
Text(text = user.name)
}
위 코드에서 @Immutable 어노테이션은 User 객체가 변경되지 않음을 보장하여 Recomposition을 스킵할 수 있게 한다.
참고 링크: https://developer.android.com/develop/ui/compose/performance/stability?hl=ko
Recomposition 범위 제어
Recomposition 범위를 명확히 정의하면, 불필요한 재구성을 줄이고 성능을 최적화할 수 있다.
Recomposition 최적화를 위한 팁
- Stable 객체 사용:
- @Stable 어노테이션을 활용하여 객체의 안정성을 명시한다.
- 변경되지 않은 객체는 Recomposition을 스킵할 수 있다.
- State Hoisting:
- 상태를 상위 컴포저블로 끌어올려, 상태 변경 범위를 최소화한다. (추후 포스팅 예정)
- Composable 분리:
- 상태를 사용하는 부분과 아닌 부분을 분리하여 Recomposition 범위를 축소한다.
마무리
Recomposition은 Jetpack Compose의 핵심 동작 원리로, 상태를 기반으로 필요한 UI만 다시 그리는 최적화된 과정이다. 이번 글에서는 Composer.kt와 Composables.kt 코드를 통해 Recomposition이 동작하는 방식을 살펴보았다. Compose의 내부 구조를 이해하고, Recomposition 범위를 제어하면 효율적이고 성능이 뛰어난 UI를 설계할 수 있다.
이와 같은 모든 과정을 내가 이해한 대로 아래와 같이 정리해보았다.
Recomposition은 Jetpack Compose의 핵심 동작 원리로, Composable 함수가 호출될 때마다 Slot Table에 쌓이고, 이때 각 Composable에 고유한 Key가 지정되어 트리 구조를 형성한다. remember로 상태를 읽으면 해당 상태(State)가 Slot Table에 Snapshot으로 저장되며, rememberSaveable은 구성 변경(Configuration Change)에도 상태를 유지하기 위해 Bundle 형식으로 저장되었다가 복원된다. 상태가 변경되면 Snapshot 시스템이 이를 감지하고 Recomposition을 발생시키며, Stability를 확인하여 변경되지 않은 부분은 건너뛰고(Skip) 변경된 부분만 효율적으로 다시 구성한다. 이 과정에서 Key는 이전 상태와 변경된 상태를 매핑하여 올바르게 연결되도록 하여, 불필요한 UI 생성과 성능 저하를 방지한다.
다음 글에서는 State Hoisting을 활용한 상태 관리와 Stateless vs Stateful 설계 패턴을 다루며, 상태를 효율적으로 관리하는 방법을 소개하겠다.
