Recomposition은 데이터 변경 시 사용자 인터페이스의 일부를 자동으로 재구축하여 수동 View 업데이트 없이 동작하는 Jetpack Compose의 메커니즘입니다. Composable 함수가 의존하는 상태 변수가 값을 변경하면 Compose는 해당 함수만 다시 시작하고 UI 트리의 나머지 부분은 건드리지 않습니다. Google Android Developers, 2026에 따르면 Recomposition을 올바르게 이해하면 불필요한 다시 그리기를 40–60% 줄일 수 있습니다.
핵심 포인트
Recomposition은 이미 Composition에 참여한 Composable 함수를 새 매개변수 또는 상태 값으로 다시 실행하는 것입니다. 재구성의 주요 목표는 전체 인터페이스를 처음부터 재구축하지 않고 UI 트리를 현재 데이터와 동기화하는 것입니다. 한 번 발생하는 Composition과 달리 Recomposition은 화면 수명 동안 수백 번 트리거될 수 있습니다.
Recomposition은 스마트 무효화 원칙으로 작동합니다: Compose는 각 Composable 함수가 읽는 State 객체를 추적하고 종속성이 변경된 함수만 다시 시작하도록 표시합니다. 이는 실행 중 모든 State 읽기 작업을 기록하는 스냅샷 시스템과 이러한 종속성을 특정 함수에 매핑하는 Composer를 통해 달성됩니다.
이해하는 것이 중요합니다: 재구성이 즉각적인 화면 다시 그리기를 의미하지는 않습니다. Compose는 세 단계로 작동합니다: Composition(UI 설명 구축), Layout(크기 및 위치 계산), Drawing(캔버스에 렌더링). 재구성 후 요소의 크기와 위치가 변경되지 않은 경우 Layout 단계를 건너뛸 수 있습니다. 시각적外观이 변경되지 않은 경우 — Drawing이 건너뛰어집니다. 이 3단계 아키텍처는 각 UI 업데이트의 최소 비용을 보장합니다.
재구성에는 세 가지 주요 트리거가 있습니다. 첫째는 Composable 함수 본문 내에서 읽은 State 객체의 변경입니다. mutableStateOf 또는 derivedStateOf가 값을 변경하면 이전 Composition에서 이 State 읽기를 등록한 모든 함수가 다시 시작하도록 표시됩니다.
둘째 트리거는 부모 함수에서 호출될 때 Composable 함수의 매개변수 변경입니다. 부모 함수가 새 값을 전달하면(예: 텍스트나 숫자가 변경됨) 자식 함수는 내부적으로 State를 읽지 않더라도 다시 시작됩니다. Compose는 equals를 통해 새 매개변수 값과 이전 값을 비교하고 같으면 — 함수를 건너뛸 수 있습니다.
셋째 트리거는 CompositionLocalProvider를 통한 CompositionLocal 변경입니다. .current를 통해 CompositionLocal을 읽는 모든 함수는 공급자가 변경되면 다시 시작됩니다. 이 메커니즘은 MaterialTheme에서 사용됩니다: 테마 전환(라이트/다크)은 MaterialTheme.colorScheme을 읽는 모든 컴포넌트의 재구성을 발생시킵니다.
@Composable
fun RecompositionDemo() {
var counter by remember { mutableStateOf(0) }
var text by remember { mutableStateOf("Hello") }
Column {
Text("Counter: $counter") // recomposition when counter changes
Text("Message: $text") // recomposition when text changes
Button(onClick = { counter++ }) {
Text("+1")
}
Button(onClick = { text = "World" }) {
Text("Change Text")
}
}
}
+1 버튼을 클릭하면 counter가 변경되어 첫 번째 Text 줄과 Column 자체만 재구성됩니다. text를 표시하는 두 번째 Text 줄은 다시 시작되지 않습니다. 이러한 격리는 스냅샷 시스템의 결과입니다: 각 Composable 함수는 읽은 State 객체만 알고 있습니다.
재구성 최적화는 올바른 데이터 구조 선택에서 시작됩니다. 가변 컬렉션(mutableListOf) 대신 불변 컬렉션(listOf, mapOf)을 사용하세요. Compose는 equals를 통해 매개변수를 비교하며 컬렉션이 변경되었지만 equals가 true를 반환하면 함수가 다시 시작되지 않습니다. 가변 컬렉션의 경우 요소 수준에서 올바른 변경 추적을 구현하는 SnapshotStateList를 사용하세요.
두 번째 기술은 UI의 안정적인 부분을 별도의 Composable 함수로 추출하는 것입니다. 화면의 일부가 자주 변경되는 상태에 의존하지 않는 경우 매개변수와 함께 별도의 함수로 추출하세요. 재구성이 발생하면 안정적인 함수는 동일한 매개변수를 받고 Compose가 이를 비교하여 실행을 건너니다. 이는 일부 매개변수가 변경된 큰 함수의 일부로 해당 부분을 다시 시작하는 것보다 효율적입니다.
세 번째 기술은 LazyColumn의 키입니다. LazyColumn, LazyGrid 및 기타 지연 컨테이너의 항목에는 항상 키를 지정하세요. 키를 사용하면 목록 변경(추가, 제거 또는 재정렬) 시 Compose가 요소를 식별할 수 있습니다. 키가 없으면 Compose는 변경 시 목록의 모든 항목을 다시 시작하며, 큰 목록에서는 눈에 띄는 성능 저하가 발생합니다.
// Optimized structure: stable parts extracted separately
@Composable
fun OptimizedScreen(items: List<Item>) {
Column {
Header() // does not depend on items — no recomposition
Spacer(modifier = Modifier.height(8.dp))
LazyColumn {
items(items, key = { it.id }) { item ->
ItemRow(item = item) // recomposition only for changed items
}
}
}
}
@Composable
fun Header() {
Text("Item list", style = MaterialTheme.typography.headlineMedium)
}
@Composable
fun ItemRow(item: Item) {
Text(item.title)
}
Skipping은 모든 매개변수가 변경되지 않은 경우 Compose가 Composable 함수의 실행을 건너뛰는 메커니즘입니다. Skipping이 올바르게 작동하려면 매개변수 타입이 안정적( stable)이어야 합니다. Kotlin 컴파일러는 다음을 안정적인 것으로 표시합니다: 원시 타입(Int, Float, Boolean), String, 람다 함수, 그리고 모든 필드가 안정적이고 val인 클래스.
Stability는 사용자 정의 데이터 클래스에 추가할 수 있는 @Stable 또는 @Immutable 어노테이션입니다. 클래스에 가변 필드(var)가 포함된 경우 컴파일러는 이를 불안정하다고 간주하며 Compose는 이러한 매개변수가 있는 함수를 건너뛸 수 없습니다. var가 있는 클래스의 경우 스냅샷 시스템을 통해 변경 알림이 전송됨을 보장하는 경우 @Stable을 사용하세요.
컴파일러 플래그 -P "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=. /reports"를 사용하여 안정성을 확인할 수 있습니다. 모든 Composable 함수와 해당 매개변수의 목록을 안정성과 함께 생성합니다. 매개변수가 불안정한 경우 해당 함수의 Skipping은 불가능하며 부모 재구성 시마다 다시 시작됩니다.
| 타입 | 안정성 | Skipping |
|---|---|---|
| Int, Float, Boolean | 안정적 | 예 |
| String | 안정적 | 예 |
| 람다 | 안정적 | 예 |
| val 필드의 data class | 안정적 | 예 |
| var 필드의 data class | 불안정 | 아니오 |
| List<String> | 불안정 | 아니오 |
참고: List<String>는 인터페이스이므로 구체적인 구현이 아니기 때문에 불안정한 것으로 간주됩니다. Kotlin Collections Immutable 라이브러리의 immutableListOf()를 사용하거나 목록을 @Stable 클래스로 래핑하세요. 람다는 항상 안정적입니다. equals가 참조만 비교하고 호출 지점에서 새 람다가 생성되면 부모 함수도 다시 시작되기 때문입니다.
재구성 모니터링을 위해 Android Studio는 Compose Recomposition Counts 모드의 Layout Inspector를 제공합니다. 이 모드에서 각 Composable 함수는 재구성 횟수와 다시 시작 이유를 표시합니다. 이를 통해 너무 자주 재구성되는 함수를 빠르게 찾고 근본 원인(불안정한 매개변수 또는 불필요한 State 종속성)을 파악할 수 있습니다.
추가 도구: Compose Metrics(계측 테스트를 통한 통계 수집) 및 Recomposition Timer(각 함수의 실행 시간 측정). Google은 프로파일링 단계에서 이러한 도구를 활성화하고 릴리스 빌드에서는 비활성화할 것을 권장합니다. 재구성당 최대 20%의 오버헤드가 추가되기 때문입니다.
재구성을 분석할 때 불필요한 재구성 패턴을 찾으세요: 출력 UI가 변경되지 않아야 함에도 함수가 다시 시작되는 경우. 일반적인 원인은 remember 없이 람다를 사용하여 매번 새 람다 객체가 생성되고 Compose가 매개변수가 변경된 것으로 간주하는 것입니다. 해결책: 고정 캡처로 remember { }에 람다를 래핑하세요.
// Bad: new lambda on every parent recomposition
@Composable
fun Parent() {
Child(onClick = { doSomething() }) // new lambda every time
}
// Good: remember stabilizes the lambda
@Composable
fun Parent() {
val onClick = remember { { doSomething() } }
Child(onClick = onClick) // same reference
}
자주 묻는 질문
아니요, 재구성은 Composition 단계일 뿐입니다. 이후 Layout과 Drawing이 실행됩니다. 재구성 후 요소의 크기와 위치가 변경되지 않은 경우 Layout과 Drawing을 완전히 건너뛸 수 있어 GPU 리소스를 절약할 수 있습니다.
애니메이션 중 재구성은 초당 최대 120회 실행될 수 있습니다(120fps). 일반 상호작용의 경우 초당 10–60회입니다. 각 재구성이 프레임 예산(8–16ms) 내에 들어가는 것이 중요합니다. 그렇지 않으면 애플리케이션이 지연됩니다.
이유는 부모 함수로부터의 매개변수 변경입니다. 부모가 자신의 이유로 다시 시작되어 새 값을 전달합니다. 이를 방지하려면 매개변수 안정성을 확인하고 remember를 사용하여 람다와 계산된 값을 안정화하세요.
직접적인 비활성화는 없지만 readInComposition을 통한 강제 Skipping이 있습니다 — State가 함수 본문 외부에서 읽혀 종속성으로 등록되지 않습니다. 주의해서 사용하세요: 함수가 변경에 반응하지 않아 오래된 UI가 발생할 수 있습니다.
Composition이 더 비용이 많이 듭니다. 모든 슬롯과 트리 노드를 처음부터 생성하기 때문입니다. Recomposition은 기존 슬롯을 재사용하고 값만 업데이트합니다. 실제로 화면 Composition은 2–10ms가 소요되는 반면 단일 요소의 재구성은 0.1–1ms가 소요됩니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.