본문 바로가기

Android

[Android] Compose가 UI를 그리기까지의 과정

 

지난번엔 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 ++ ` 한 줄이 일으키는 일은 아래와 같습니다.

 

  1. count가 바뀜 (State write)
  2. Compose는 “이 상태를 읽었던” Composable 범위를 invalidate(무효화) 해둠
  3. 그리고 즉시 다시 그리는 게 아니라, 보통 다음 프레임에 맞춰 한 번에 처리합니다

 

 

이 “다음 프레임에 맞춰 실행”되는 감각이 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

 

https://developer.android.com/develop/ui/compose/migrate/interoperability-apis/compose-in-views?hl=ko

 

뷰에서 Compose 사용  |  Jetpack Compose  |  Android Developers

이 문서에서는 활동에 setContent()를 사용하고 프래그먼트와 XML 레이아웃에 ComposeView를 사용하여 Jetpack Compose UI를 기존 뷰 기반 Android 애플리케이션에 통합하는 방법을 설명하고, 컴포지션 수명 주

developer.android.com