지난 글: [Android] Compose의 모든 것(1) - Compose, View System, Kotlin
https://yujinius45.tistory.com/139
[Android] Compose의 모든 것(1) - Compose, View System, Kotlin
시작하기에 앞서최근 Nature Album 프로젝트를 통해 Compose를 처음 학습하고 프로젝트에 적용하며 많은 것을 배울 수 있었다. 이번 글은 프로젝트에서 사용한 Compose를 제대로 알기 위해 정리 차원에
yujinius45.tistory.com
이번 글에서는 Composable의 Lifecycle과 Compose의 UI 렌더링 단계, Recomposition, State 개념을 중점적으로 살펴보며, Compose가 어떤 단계로 그려지고 최적화를 이루는지 알아보겠다.
Composable의 생명주기
Jetpack Compose에서 Composable의 생명주기는 Composition에 들어가고(Entering Composition), Recomposition을 0번 이상 거친 뒤 Composition에서 나가는(Leaving Composition) 과정으로 정의된다. 이 생명주기를 이해하면 UI 업데이트를 최적화하고 자원을 효율적으로 관리할 수 있다.

참고 링크: https://developer.android.com/develop/ui/compose/lifecycle
- 초기 Composition:
- Compose는 처음으로 @Composable 함수를 실행하여 Composition을 생성한다.
- Composition은 UI를 정의하는 모든 Composable 요소로 구성된 트리 구조를 말한다.
- Recomposition:
- Recomposition은 상태(State<T>) 변경에 따라 필요한 Composable 함수를 다시 실행하는 과정이다.
- Compose는 상태를 추적하고, 해당 상태를 읽은 Composable과 이를 호출하는 다른 Composable 중 스킵할 수 없는 함수를 다시 실행한다.
- Composition에서 제거:
- 더 이상 필요하지 않은 Composable은 Composition에서 제거된다.
Composable의 Lifecycle의 핵심인 Recomposition을 기억하고 다음으로 UI 렌더링 단계를 살펴보도록 하자.
Jetpack Compose UI 렌더링의 3단계
Compose는 UI를 렌더링할 때 Composition, Layout, Drawing의 3단계를 거친다. 각 단계에서 역할이 명확히 분리되며, 성능 최적화를 위해 필요한 부분만 다시 실행한다.

참고 링크: https://developer.android.com/develop/ui/compose/phases?hl=ko
1. Composition 단계

- 어떤 UI를 보여줄지(What)를 결정하는 단계로, @Composable 함수를 실행하여 UI의 구조를 정의한다.
- 주요 작업:
- Composable 함수 실행: UI 요소를 생성.
- UI 트리 생성: 각 UI 요소를 노드(Node)로 표현하여 UI 트리를 구성.
- 예시:이 함수는 실행 시 Text 노드를 생성하여 UI 트리에 추가한다.
@Composable
fun Greeting(name: String) {
Text("Hello, $name!")
}
- State 변경 시 최적화: 상태(State)가 변경되면 관련된 UI 요소만 다시 실행(= Recomposition)하며, UI 트리 전체를 다시 생성하지 않는다.
2. Layout 단계

- 어디에 배치할지(Where)를 결정하는 단계로, Composition에서 생성된 UI 트리의 요소별 크기와 위치를 계산한다.
- 주요 작업:
- 측정(Measurement): 각 UI 요소의 크기를 계산.
- 배치(Placement): 부모-자식 관계에 따라 화면에서의 위치를 결정.
- 예시:이 텍스트는 부모 컨테이너의 제약 조건을 기반으로 위치와 크기를 계산한다.
Text("Compose Layout Example")
- State 변경 시 최적화: 크기나 위치가 변경된 요소만 다시 측정 및 배치하며, 영향을 받지 않는 다른 요소는 건너뛴다.
3. Drawing 단계

- 어떻게 그릴지(How)를 결정하는 단계로, Layout 단계에서 계산된 정보를 바탕으로 UI를 Canvas에 그린다.
- 주요 작업:
- Canvas 렌더링: 텍스트, 이미지, 모양 등을 화면에 출력.
- 시각적 요소 그리기: UI 요소를 디바이스 화면에 표시.
- State 변경 시 최적화: 상태 변경으로 영향을 받은 UI 요소만 다시 그리며, 나머지 요소는 그대로 둔다.
위의 3단계에서 언급된 State 변경 시 최적화라는 것이 무엇일까?
- 공식 문서(https://developer.android.com/develop/ui/compose/phases)에는 아래와 같이 나와 있다.
Phased state reads
As mentioned above, there are three main phases in Compose, and Compose tracks what state is read within each of them. This allows Compose to notify only the specific phases that need to perform work for each affected element of your UI.
Note: Where a state instance is created and stored has little bearing on the phases, it only matters when and where a state value is read.

- 이는 각 단계에서 State를 read했다면 recomposition 때 그 부분부터 실행된다는 내용이다. 그리고 그 아래의 부분을 보면 다음과 같다.
Recomposition loop (cyclic phase dependency)

- Layout의 state를 읽고 있고 이것이 변경되면 다시 composition으로 reinvoke된다는 내용이다. 뭔가 이상하지 않은가? 분명 위에서는 State Read된 곳부터 실행한다고 했는데, 그럼 Layout으로 reinvoke 되어야 하는 것이 아닌가? 라고 생각했던 적이 있다.
- 또한, Recomposition은 말 그대로 composition을 다시 실행한다는건데 그럼 위의 State Read 부터 실행한다는 것은 도대체 뭐지??하며 혼란스러웠던 적이 있었다.
그리고 이에 대해 다른 동료가 링크를 준 아래의 문서를 읽어보면서 확실하게 이해했다
However, coding mistakes can make it hard for Compose to know which phases it can safely skip, in which case Compose runs all three phases, which can slow down your UI. So, many of the performance best practices are to help Compose skip the phases it doesn't need to do.
skip을 초점을 맞춰야 했었다.
- Compose의 모든 것(1) 포스팅에서 말했듯이 Compose는 함수로 구성되어 있다. 그래서 즉, UI 렌더링에서 Composition, Layout, Drawing 순으로 실행되지만, 만약 Layout에서 읽은 State가 변경되었을 경우 Recomposition이 일어나 다시 Composition으로 reinvoke 되지만, State를 read한 Layout의 함수 실행 블록까지 Skip이 되는 것이다. 이를 통해 변경된 부분만 다시 함수가 실행되도록 최적화 되어 있는 것이다.
- 내부적으로 어떻게 동작되는지에 대해서도 살펴보았었는데 이에 대해서는 추후 다른 글을 통해 찾아오도록 하겠다.
단방향 데이터 흐름(UDF)

참고 링크: https://developer.android.com/develop/ui/compose/architecture?hl=ko#udf
- Jetpack Compose의 데이터 흐름은 일반적으로 Composition → Layout → Drawing의 단방향으로 진행되지만, 특정 레이아웃(BoxWithConstraints, LazyColumn, LazyRow)은 예외이다.
예외적인 레이아웃: BoxWithConstraints, LazyColumn
BoxWithConstraints
- 상위 요소의 크기 정보를 기반으로 하위 요소를 컴포지션.
- 예시: 상위 요소의 제약 조건(maxWidth)에 따라 하위 요소의 UI가 결정된다.
@Composable
fun ResponsiveBox() {
BoxWithConstraints {
if (maxWidth > 600.dp) {
Text("Large screen")
} else {
Text("Small screen")
}
}
}
LazyColumn
- 효율성을 위해 필요한 항목만 동적으로 컴포지션.
- 예시:화면에 표시되지 않는 항목은 컴포지션되지 않고, 스크롤 시 필요할 때 추가적으로 생성된다.
@Composable
fun LazyExample() {
LazyColumn {
items(100) { index ->
Text("Item $index")
}
}
}
- 우선은 이 포스팅에서는 전체적인 것을 다루기에 위의 것은 이정도만 알고 넘어가자.
Recomposition과 상태 관리
Recomposition은 상태(State) 변경을 감지하고, 관련 Composable 함수를 다시 실행하는 과정이다. 이 과정을 통해 Compose는 최신 상태를 UI에 반영한다. 이번 글에서는 전체적이 개념만 훑고 넘어가보도록 하자.
State와 Recomposition
- State란 무엇인가?
- UI와 데이터 상태를 동기화하는 Compose의 핵심 개념으로, 상태 변경 시 관련 UI를 자동으로 업데이트한다.
- 특징:
- 관찰 가능: 상태 변경 시 Compose가 이를 감지.
- 효율적 Recomposition: 필요한 UI만 다시 렌더링.
- State 선언 방식
- Compose에서 상태를 관리하기 위한 세 가지 방식:
- val mutableState = remember { mutableStateOf(default) } var value by remember { mutableStateOf(default) } val (value, setValue) = remember { mutableStateOf(default) }
- State 관리 기법
- remember: 컴포지션에 상태를 저장. 구성 변경 시 상태를 유지하지 못함.
- rememberSaveable: 구성 변경에도 상태를 유지. Parcelable이나 Serializable 필요.
- State Hoisting: 상태를 상위 계층으로 끌어올려 관리.
Recomposition 최적화: Stability와 Skipping
Compose는 불필요한 Recomposition을 방지하기 위해 내부적으로 상태의 안정성(Stability)을 분석한다.
Recomposition Skipping
- 언제 발생하는가?
- 상태가 Immutable하거나 Stable한 경우.
- Composable 함수의 매개변수가 변경되지 않은 경우.
- Stability 보장
- 데이터 클래스에 @Stable 또는 @Immutable 어노테이션을 사용하여 안정성을 명시.
- 불필요한 Recomposition 방지.
- Compose 성능 최적화를 위한 팁
- Stable 객체 사용.
- 필요한 경우에만 상태 변경 및 읽기 수행.
마무리
Jetpack Compose는 Composition → Layout → Drawing의 단계를 통해 UI를 효율적으로 렌더링하며, 상태 관리와 Recomposition을 최적화하여 성능을 극대화한다. 각 단계의 역할과 최적화 원리를 이해하면 더 나은 UI를 설계하고 효율적인 코드 작성을 할 수 있다.