Compose 단계
- 컴포지션(Composition) 단계
- @Composable 함수들이 실행되어 UI 트리(구조)를 구성함
- 이 단계에서 상태를 읽으면 해당 상태가 변경될 때마다 리컴포지션이 발생함
- 레이아웃(Layout) 단계
- 컴포저블의 크기 측정 및 배치를 계산하는 단계
- Modifier.offset { ... }, Modifier.layout { ... } 등은 이 단계에서 실행됨
- 이 안에서 상태를 읽으면, 컴포지션을 다시 하지 않고 레이아웃만 다시 함
- 드로잉(Drawing) 단계
- 실제로 화면에 그려지는 픽셀을 렌더링하는 단계
- 이 단계에서는 Compose 상태를 거의 읽지 않음
- (상태 기반 애니메이션, Modifier.drawBehind 등은 간접적으로 관여)
단계별로 실행해야 하는 이유
위와 같이 3가지 단계는 순차적으로 흘러가야 한다.
여기서 드로잉했다가 레이아웃했다가 이런 식으로 순서 없이 가면 안된다.
위의 그림을 보면 그 이유를 알 수 있다.
UI트리에서 하위 측정값에 따라 상위요소의 자체 크기를 결정하기에
BoxWithConstraints, LazyColumn, LazyRow와 같은 예외는 제외
모든 노드인 UI 트리를 각각 한 번만 통과하면 되므로 성능이 향상한다.
그러면 트리의 노드 수가 증가하면 트리를 탐색하는 데 걸리는 시간이 선형적으로 증가함.
만약에
반대로 각 노드가 여러 번 방문되면 탐색 시간이 기하급수적으로 증가하는 문제가 발생할 수 있다.
그래서 앱개발자한테 화면이 그려지는 과정을 알아야 하는 이유는
과정에 따라 성능에 큰 영향을 미치기 때문이다.
최적화
그럼 어떻게 해야 이 단계를 순차적으로 지켜서 성능을 최적화할 수 있고, 그때 주의할 사항을 알아보자.
단계 중에서 컴포지션 단계의 설명에서 상태 변경마다 리컴포지션이 발생할 수 있다고 한다.
이때 리컴포지션을 조심해야한다.
이미 컴포지션 단계를 거친 UI에 또 컴포지션 단계를 적용하면 리컴포지션이 일어나 전체 컴포즈션을 다시 실행한다.
각 단계가 적절한 경우와 예시를 살펴보자.
상태 읽는 위치 | 어떤 경우 적절한가 | 예시 |
컴포지션 단계 (@Composable 본문에서 읽기) | UI 구조, 내용이 바뀌는 경우 | 버튼 텍스트, 조건부 뷰 |
레이아웃 단계 (Modifier.offset {} 등 람다 안) | 위치, 크기만 바뀌는 경우 | 스크롤에 따른 이미지 이동 |
드로잉 단계 (Modifier.drawBehind {} 등) | 픽셀 기반 효과나 커스텀 드로잉 | 차트 그리기, 배경 등 |
예를 들어,
firstVisibleItemScrollOffset가 Compose 상태이기 때문에
컴포지션 중에 이 값을 읽으면, 리컴포지션이 일어나 전체 컴포즈션을 다시 실행함.
→ 레이아웃 단계에서 실행하면 컴포지션 단계는 넘어가고 레이아웃, 그리기 단계만 실행되어 더욱 효율적이다.
스크롤에 따라 단순히 UI 위치만 바뀌는 경우 컴포지션 중에 상태를 읽지 않고
Modifier.offset {}처럼 레이아웃 시점에 상태를 읽도록 구현하는 것이
리컴포지션을 피하고 퍼포먼스를 최적화하는 핵심이다.
-
전체 컴포지션을 실행한다는 게 뭔 말이야?
난 한 위젯만 실행하는데 앱 전체에 영향을 준다고?
-
이해가 안 갈 수 있다.
코드로 살펴보자.
@Composable
fun MyScreen() {
Column {
...
Text("Offset: ${listState.firstVisibleItemScrollOffset}") // 여기를 읽음
...
}
}
위의 코드에서 Text안에 firstVisibleItemScrollOffset값이 바뀌면 Text만 업데이트되길 바랄 것이다.
나는 Text만 다시 실행하고 싶었는데 Column, MyScreen까지 한 컴포저블이 아예 다시 실행된다.
이러면 당연히 비효율적이다.
범위로 생각하면 편하다.
컴포저블-위젯 하나-상세 위젯의 범위로 단계가 진행된다.
그래서 레이아웃 단계에서 실행하도록 코드를 바꿔야 한다.
@Composable
fun MyScreen() {
val listState = rememberLazyListState()
Box(
modifier = Modifier
.fillMaxSize()
) {
// 예시: offset에 따라 위치를 이동시키는 텍스트
Text(
text = "Offset 이동",
modifier = Modifier
.offset {
// 💡 offset 값을 컴포지션이 아닌 레이아웃 시점에 읽음
IntOffset(x = 0, y = listState.firstVisibleItemScrollOffset)
}
)
}
}
Modifier.offset을 이용해 스크롤에 따라 Text만 업데이트할 수 있다.
따라서, 순차적인 단계를 지켜야 하고 컴포즈 단계를 뒤로 이동하는 리컴포지션을 조심하자!
프레임 간의 순환 의존(Composition loop)을 만들면,
최악의 경우 리컴포지션 루프가 되거나 UI가 몇 프레임에 걸쳐 흔들리는 현상(플리커)이 일어난다.
'Java' 카테고리의 다른 글
[Android] JetPack Compose ViewModel이 필수적인 이유/UI에 상태 관리하면 생기는 문제 (1) | 2025.06.24 |
---|---|
[Android] JetpackCompose을 사용하는 이유/Composable란 (0) | 2025.06.23 |
[백준] 11047번 동전 0 - 그리디 알고리즘 / 탐욕 알고리즘 (0) | 2023.09.12 |
[백준] 1920번 수 찾기 - 이진 탐색 알고리즘/ 함수 이용(재귀X) (0) | 2023.09.06 |
[백준] 2178번 미로 탐색 - BFS / 너비 탐색 알고리즘 / 클래스 (0) | 2023.09.05 |