
지난번엔 XML에서 View를 그리는 과정에 대해 글을 작성했었는데요, 이번엔 Compose에서 View를 그리는 과정에 대해 작성해보려고 합니다.
XML에서는 `setContentView()` 한 줄로 화면이 뜨지만, 그 뒤에서는 XML 파싱 -> View 트리 생성 -> Measure .. 등과 같은 파이프 라인이 돌아갑니다.
이번 글은 같은 질문을 Jetpack Compose 버전으로 작성하는 글입니다.
Compose는 "View를 생성해서" 그리는 게 아니라, "UI를 설명하는 트리"를 만들고, 그 결과를 "Canvas에 그려서" 보여줍니다.
그리고 또 다른 포인트 하나!
Composable은 View가 아닙니다. 하지만 Compose UI는 결국 하나의 Android View(ComposeView) 안에서 그려집니다.
Compose가 활발히 사용되고 있는 요즘, XML의 렌더링 과정과 비교해서 보면 재밌지 않을까 싶습니다!
1️⃣ `setContent{ }` 가 호출되면 무슨 일이 벌어질까?
Compose에서 가장 흔히 보는 시작 코드는 아래와 같을 것이라고 생각됩니다
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyScreen()
}
}
}
이 한 줄이 하는 일을 큰 흐름으로 보면 아래와 같습니다.
1. Compose UI를 담을 그릇을 만든다.
- 이 그릇이 보통 ComposeView (=Android View) 입니다.
2. 그 그릇을 Window (DecorView의 content 영역)에 붙인다.
- XML에서 `setContentView()` 로 루트 View 붙이던 것과 같은 느낌!
3. `MyScreen()` 을 실행할 준비를 한다.
- 내부적으로 Composition/Recomposer 같은 "Compose 런타임 엔진"이 준비됨
한 줄로 다시 요약해보자면,
`setContent {}` = "ComposeView를 루트에 붙이고, Composable을 실행해서 화면을 그릴 준비를 한다"
그리고 ComposeView는 View이기 때문에 Fragment/XML과 함께 사용도 가능하답니다!
2️⃣ Compose가 화면을 그리는 "3단계 (Phase)"
Compose 공식 문서에서 UI를 그리는 과정을 3개의 phase로 설명하고 있습니다.
Compose UI Rendering 3 Phases :
1. Composition : 무엇을 그릴지 결정 (UI 설명서 만들기)
2. Layout : 어디에/얼마나 크게 놓을지 결정 (Measurement + Placement)
3. Drawing : 실제 픽셀을 그림 (Canvas에 그림)
View System의 Measure/Layout/Draw와 비슷하지만, Compose는 맨 앞에 Composition 단계가 추가됩니다.
3️⃣ Phase 1 - Composition : UI 설계도 만들기
XML에서는 `LayoutInflator` 가 설계도(XML)를 보고 View 객체 트리를 생성했습니다.
Compose에서는 "설계도 파일" 대신 Composable 함수 자체가 설계도입니다.
@Composable
fun MyScreen() {
Column {
Text("Hello")
Button(onClick = { /*...*/ }) {
Text("Click")
}
}
}
Composition에서 하는 일은
- @Composable 함수들을 실행해서
- “현재 상태라면 화면이 이렇게 생겨야 해!” 라는 UI 트리(설명)를 만들고
- 이전 결과와 비교해서 “바뀐 부분만” 다음 단계로 넘길 준비를 합니다.
여기에서 핵심은 Compose는 `TextView()` 같은 View를 만들지 않는다. 대신 UI를 설명하는 트리를 만든다
그래서 Compose는 "Compose는 함수가 여러 번 실행될 수 있다"가 자연스러운 전제입니다.
4️⃣ Recomposition : 상태(state)가 바뀌면 "언제/어떻게" 다시 그릴까?
Compose의 반응성은 보통 `mutableStateOf` 같은 State에서 시작합니다.
@Composable
fun Counter() {
var count by remember { mutableIntStateOf(0) }
Button(onClick = { count++ }) {
Text("count = $count")
}
}
여기에서 `count ++ ` 한 줄이 일으키는 일은 아래와 같습니다.
- count가 바뀜 (State write)
- Compose는 “이 상태를 읽었던” Composable 범위를 invalidate(무효화) 해둠
- 그리고 즉시 다시 그리는 게 아니라, 보통 다음 프레임에 맞춰 한 번에 처리합니다
이 “다음 프레임에 맞춰 실행”되는 감각이 View의 invalidate()와 비슷한데, Compose는 여기에 Recomposer가 붙어 있다고 보면 됩니다.
그리고 안드로이드에서는 프레임 타이밍이 Choreographer(VSYNC 기반)와 연결되는데, Compose 쪽에도 AndroidUiDispatcher / AndroidUiFrameClock 같은 구성요소가 Choreographer 프레임 디스패치를 기다리는 구조가 잡혀 있습니다.
다시 정리를 해보자면 이와 같습니다.
State 변경
↓
(어떤 composable이 이 state를 읽었는지 기록 기반으로) invalidation
↓
다음 VSYNC 프레임 타이밍
↓
Recomposition + 변경사항 적용
↓
필요하면 Layout / Drawing 수행
5️⃣ Phase 2 - Layout : "크기/위치 계산하기"
Compose도 결국 화면에 그리려면 크기/위치 계산이 필요합니다.
공식적으로 Compose의 Layout phase는 두 단계로 나뉘는데요,
- Measurement: “너 얼마 크기로 그릴래?”
- Placement: “그럼 좌표 어디에 둘래?”
XML의 MeasureSpec과 비슷한 개념이 Compose에는 Constraints로 존재합니다.
“이 영역 안에서 너의 크기를 정해줘(최소/최대)”
레이아웃 단계가 실제로 보이도록 코드를 구성해본다면,
@Composable
fun OneChildLayout(content: @Composable () -> Unit) {
Layout(content = content) { measurables, constraints ->
val placeable = measurables.first().measure(constraints)
layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
}
}
위 코드를 보면 View의 흐름을 떠올릴 수 있습니다.
measure로 크기를 결정하고, layout(=placement)로 위치를 결정합니다.
그리고 또 다른 포인트는 Compose의 경우, Measurement와 Placement가 별도의 “재시작 범위(restart scope)”를 가질 수 있어서, 경우에 따라 “측정까지는 다시 안 하고 배치만 다시” 하는 최적화도 가능합니다.
6️⃣ Phase 3 - Drawing : 진짜 픽셀을 그리기
Layout이 끝나면 이제 실제로 화면을 그려야겠죠?
Compose의 Drawing Phase 문서에서도 단순하게 이렇게 정리하고 있습니다
- UI 요소들이 Canvas에 draw 한다
즉 Compose의 각 요소는 내부적으로 “그려야 할 것들”을 Canvas에 기록하고, 최종적으로 사용자는 화면을 보게 됩니다.
7️⃣ "State를 어디에서 읽느냐"가 성능과 동작을 결정한다
추가로, Compose를 이용해 작업을 하다보면 이런 경험이 생깁니다.
- “왜 이건 recomposition이 발생하지?”
- “왜 layout까지 다시 도는 거지?”
- “왜 draw만 다시 되지?”
이 원인들은 아마 "State를 어느 phase에서 읽었는지에 따라, 다시 실행되는 단계가 달라진다" 이지 않을까 싶은데요
각 state에서 읽은 위치와 실행되는 것을 표로 정리해보자면,
| State를 읽은 위치 | 다시 수행될 수 있는 것 |
| Composition에서 읽음 | Recomposition(→ 필요하면 Layout/Drawing도) |
| Layout에서 읽음 | Layout(→ 필요하면 Drawing) |
| Drawing에서 읽음 | Drawing |
Ex 1) Composition에서 state를 읽는 경우 (recomposition 트리거)
var padding by remember { mutableStateOf(8.dp) }
Text(
text = "Hello",
modifier = Modifier.padding(padding) // padding을 '구성(Composition)' 중에 읽음
)
`padding` 이 바뀌면 recomposition이 발생할 가능성이 큽니다.
Ex 2) Layout에서 state를 읽는 경우 (layout만)
아래처럼 람다로 offset을 주는 형태는(핵심: 값 계산이 layout 시점에 일어남) layout 단계 쪽에 걸릴 수 있습니다.
val offsetX by remember { mutableIntStateOf(0) }
Box(
Modifier.offset { IntOffset(offsetX, 0) }
) { /* ... */ }
Ex 3) Drawing에서 state를 읽는 경우 (draw만)
val alpha by remember { mutableFloatStateOf(1f) }
Box(
Modifier.drawBehind {
// 이 블록은 draw 단계에서 실행
drawRect(androidx.compose.ui.graphics.Color.Black.copy(alpha = alpha))
}
)
이렇게 각 "상태 읽기 위치"를 의식하기 시작하면 Compose의 성능 튜닝이 조금 더 쉬워질 것 같습니다! 😮
마지막으로, Compose의 전체 흐름을 정리하고 마무리하자면 아래와 같습니다
setContent
↓
ComposeView(= Android View) 준비 & Window에 attach
↓
Composition (무엇을 그릴지: Composable 실행, UI 트리 구성)
↓
Layout (Measurement → Placement)
↓
Drawing (Canvas에 그림)
↓
State 변경 시 invalidation
↓
다음 프레임에 recomposition (프레임은 Choreographer 기반으로 동기화)
📚 참고
https://developer.android.com/develop/ui/compose/phases?hl=ko
Jetpack Compose 단계 | Android Developers
이 문서에서는 Jetpack Compose의 UI 렌더링의 세 가지 핵심 단계(컴포지션, 레이아웃, 그리기)를 자세히 설명하고 상태 읽기가 각 단계와 상호작용하여 성능을 최적화하는 방법을 설명합니다.
developer.android.com
뷰에서 Compose 사용 | Jetpack Compose | Android Developers
이 문서에서는 활동에 setContent()를 사용하고 프래그먼트와 XML 레이아웃에 ComposeView를 사용하여 Jetpack Compose UI를 기존 뷰 기반 Android 애플리케이션에 통합하는 방법을 설명하고, 컴포지션 수명 주
developer.android.com
'Android' 카테고리의 다른 글
| [Android] Android 앱 아이콘을 클릭하는 순간부터 화면에 그려지기까지 (0) | 2026.03.08 |
|---|---|
| [Android] XML이 View를 그리기까지의 과정 (0) | 2026.02.08 |
| [Android] Mutex와 Coroutine으로 동시성 문제 해결하기 (0) | 2026.01.25 |
| [Android] Build Variant는 왜 필요할까: 안드로이드 빌드 관리하기 (0) | 2026.01.11 |
| [Android] 결제에서 Consume은 왜 필요할까? (0) | 2025.12.28 |