1. Compose 화면이 그려지는 그려지는 과정
@Composable
fun CounterDemo() {
// 1. State 읽기
var count by remember { mutableStateOf(0) }
Counter(
count = count,
// 2. 값 변경 Snapshot 이 변경 사실 기록
onPlus = {
count++
}
)
}
@Composable
private fun Counter(count: Int, onPlus: () -> Unit) {
// 3. Recompose 될 때마다 여기 로그가 한 번씩 찍힌다.
LaunchedEffect(count) {
Log.d("RECOMPOSE", "Counter recomposed with count = $count")
}
// 4. Applier 가 View 에 패치
Button(onClick = onPlus) {
Text("Count = $count")
}
}
1) MutableState 읽기
Composable 함수가 count 같은 State 값을 읽으면, 런타임은 "애가 이 값에 의존하고 있구나" 하고 메모해 둡니다.
2) 사용자가 값 변경
버튼을 눌러 count ++ 가 실행 되면, 그 순간 스냅샷 시스템이 "관찰된 값이 바뀌었다" 는 신호를 Recomposer 에게 보냅니다.
3) Recompose 예약 → 실행
Recomposer 는 다음 프레임에서 해당 함수만 다시 호출합니다. 이때 이전 호출 때 만들어 둔 SlotTable(UI 노드 설계도)을 들고 와서 "바뀐 부분만" 비교 하고 적용합니다.
4) Applier가 실제 View 트리에 패치
마지막으로 androidx.compose.runtime:runtime.Applier 가 변화된 Slot을 View 계층(또는 Compose UI 레이아웃 노드)에 반영하고 끝납니다. "핵심은 읽은 값 감시한다" 라는 점입니다.
2. Recomposition 낭비 중인지 확인 법?
1) 컴파일러 메트릭 켜보기
android.compose.compiler.report=true
android.compose.compiler.metricsDestination=./compose-metrics
gradle.properties 에 위를 적고 Release 빌드를 돌립니다.
(빨리 확인용으로는 debug 빌드를 가끔 쓰기도 하지만.. )
2) skip.txt 파일을 열어봅니다.
skip.txt 생성 디렉토리를 별도로 지정하지 않았다면 root 디렉토리 바로 밑에 compose-metrics/skip.txt 가 있을겁니다.
Counter.kt:25 restartable:3 skippable:10 restartSkips:2
restartable 은 재컴포지션 가능한 함수의 개수 입니다. skippable 은 그중 스킵 검사를 한 횟수 입니다. restartSkips 는 실제로 스킵이 일어난 횟수 입니다. 즉, skippable 숫자가 크고 restartSkips 가 작다면 낭비가 있는 겁니다.
위 예제는 스킵된 비율이 (skippable)10/ (restartSkips) 2 = 20% 라면 검사 10번 중 8 번은 다시 그렸다는 말이므로 낭비가 일부 존재 합니다.
3) 실시간 프로퍼일러
Android Studio → Layout Inspector → Compose 탭을 열고, Recompose 카운터가 계속 튀는 조각을 찾습니다.
3. Recomposition 낭비 방지법
1) 람다(익명 함수) 새로 만들기
Button(onClick = { count++ })
위 같은 람다를 매 프레임 새로 만들면, Compose는 람다도 UI 트리 일부라고 생각해 다시 비교하게 됩니다.
→ 아래와 같이rememberUpdateState 로 람다 내용만 갱신하고 람다 객체는 재사용하면 해결 합니다.
// 1) 최신 count 값을 상태로 보관
val currentCount by rememberUpdatedState(count)
// 2) 람다 인스턴스는 remember 블록에서 단 한 번만 생성
val plusOne = remember {
{
count = currentCount + 1
}
}
Button(onClick = plusOne) {
Text("Count = $count")
}
2) State 규칙 깨뜨리기
data class User(var name: String)
위 처럼 var 프로퍼티가 있는 데이터 클래스를 파라미터로 넘기면 불안정(Unstable) 타입으로 간주돼 모든 하위 UI가 재구성 됍니다.
→ 프로퍼티를 val로 하거나 @State 애노테이션으로 명시해야 합니다.
3) key 규칙 안 주는 LazyColumn
@Composable
fun ChatsScreen(chats: List<Chat>) {
// 키 없이 사용 – 스크롤 시 매 행을 새 항목으로 오해
LazyColumn {
items(chats) { chat ->
ChatRow(chat)
}
}
}
리스트 아이템에 고유 Key 를 안 주면 스크롤할 때 매 행이 새 데이터로 바뀌었다고 착각합니다. 그렇게 전부 재컴포지션이 일어납니다.
→ 아래와 같이 꼭 key 를 써야 합니다.
items(chats, key = { chat -> chat.id }) { ... }
4) remeber 없이 새 객체를 계속 만들 때
Button(
onClick = { /* … */ },
// recomposition마다 새 객체 생성
interactionSource = MutableInteractionSource()
) {
Text("Click")
}
위 예제 처럼 MutableInteractionSource() 로 새 인스턴스를 생성하면, Compose는 프로퍼티가 바뀌었다고 판단해 Button 자체 + 내부 UI를 전부 다시 그립니다.
→ 아래와 같이 꼭 remember { … } 처럼 객체를 만들어서 사용해야합니다.
val source = remember { MutableInteractionSource() }
Button(
onClick = { /* … */ },
interactionSource = source
) {
Text("Click")
}
5) mutableListOf 직접 수정으로 Snapshot 불일치
@Composable
fun BadListScreen() {
// Compose가 변경을 추적하지 못하는 일반 MutableList
val items = remember { mutableListOf<String>() }
fun add(item: String) {
items.add(item)
}
Column {
// 스크롤·다시 빌드 시 전체 행 재컴포지션 가능
LazyColumn {
items(items) { Text(it) }
}
// add 함수가 Button 클릭에서 호출된다고 가정
Button(onClick = { add("Item ${items.size}") }) {
Text("Add")
}
}
}
위 예제 처럼 하면 문제가 생깁니다. 왜냐하면 mutableListOf 는 Compose Snapshot 시스템이 변경을 관찰하지 못합니다. 즉, Recomposer는 리스트 전체가 새로 바뀌었다고 오해하고 모든 행을 재컴포지션·재측정 해버립니다.
→ 두가지 방법이 있습니다.
방법 1. 상태형 리스트 사용
val items = remember { mutableStateListOf<Item>() }
fun add(item: Item) {
items += item
}
방법 2. 불변 리스트 교체
var items by remember { mutableStateOf(emptyList<Item>()) }
fun add(item: Item) {
items = items + item
}
(+ 연산자는 plus() 역할이라서 새로운 리스트를 copy 함)
두 방법 모두 Compose 가 변경을 추적해서 바뀐 행만 리컴포지션 합니다.
5. 정리
- Recomposition 은 “읽은 State 가 바뀌었을 때만” 발생한다—그러나 람다·키·안정성 규칙을 놓치면 불필요한 재컴포지션이 폭증한다.
- 컴파일러 메트릭과 Layout Inspector 를 켜 두면 어느 함수가 낭비를 만드는지 금방 찾을 수 있다.
- 람다 객체 재사용, Stable 데이터, Lazy List key 사용, remember 사용, Lazy List - mutableList 사용 주의 등으로
'안드로이드 Android' 카테고리의 다른 글
안드로이드 개발 (45) Alarm Manager 심층 분석 (0) | 2025.04.29 |
---|---|
안드로이드 개발 (44) Room @Transaction 에 대해 알아보자 (1) | 2025.04.20 |
안드로이드 개발 (42) Kotlin In Action 정리 - 6 (0) | 2023.02.24 |
안드로이드 개발 (41) Kotlin In Action 정리 - 5 (0) | 2023.02.19 |
안드로이드 개발 (38) Kotlin In Action 정리 - 2 (0) | 2023.02.18 |