소개
이 글은 Compose의 기본 개념과 UI 렌더링 과정에 관심있는 독자를 대상으로 작성되었습니다.
목차
1. 왜 Compose라고 부르는 것일까?
2. 선언형 UI
3. Compose에서 UI를 그리는 방법
4. 마치며
5. 출처
Jetpack Compose라는 이름을 처음 들었을 때, 그 의미가 무엇일지 궁금했던 적이 있을겁니다. "Compose"는 구성하다, 조합하다라는 뜻을 가지고 있습니다. 그럼 왜 Android UI 프레임워크인 Jetpack Compose가 이런 이름을 가졌을까요?
Jetpack Compose는 선언형 방식으로 UI를 구성하는 Kotlin 언어 기반 Android UI 프레임워크입니다. 기존 Android View 시스템과 달리, Jetpack Compose에서는 개발자가 UI를 선언하기만 하면 프레임워크가 상태에 따라 자동으로 화면을 관리하고 렌더링합니다. 이러한 과정에서 UI를 구성하는 방식이 "Compose"와 밀접한 관계를 가지기 때문에 이러한 이름이 붙여졌습니다.
전통적인 객체 지향 프로그래밍(OOP)에서는 상속(Inheritance)이 중요한 개념으로 여겨졌습니다. 상속을 통해 기존 클래스의 기능을 확장하거나 변경할 수 있었죠. 하지만 최근에는 상속보다는 조합을 사용하라는 이야기를 들어보셨을 겁니다. 이와 관련한 내용은 여기에서 참고하실 수 있습니다.
조합이라는 개념은 객체를 재사용 가능한 구성 요소로 나누고 이를 결합하여 더 큰 객체를 만들어가는 방법입니다. Jetpack Compose는 이러한 조합의 개념을 UI 개발에서 적용하고 있습니다. UI를 하나의 큰 계층 구조로 만들지 않고 재사용 가능한 Composable 함수로 나누어 정의하고 있습니다. 이러한 Composable 함수를 조합하여 화면을 구성하기 때문에 Compose라는 이름이 붙여졌습니다.
Jetpack Compose에서는 계층 구조를 활용한 상속 방식 대신 Composable을 조합하여 화면을 구성하는 것을 알 수 있었습니다. 그렇다면 이번에는 선언형 UI(Declarative UI)란 무엇이며 Jetpack Compose에서 어떻게 구현되고 있는지 알아보겠습니다.
명령형(Imperative) 방식은 무엇(What)과 어떻게(How)를 중심으로 개발자가 모두 작성해주어야합니다. 기존의 Android View 시스템이 이 방식에 해당합니다.
다음은 UI를 기존의 Android XML로 구현한 코드입니다.
<TextView
android:id="@+id/tv_greeting"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello, Kkosang!" />
val textView = findViewById<TextView>(R.id.tv_greeting)
textView.text = "Hello, James!"
반면 선언형(Declarative) 방식은 무엇(What)에만 집중하고 "어떻게(How)"는 프레임워크가 처리하는 방식입니다. 즉 화면에 무엇을 보여줄지 선언하고 구현에 대한 세부사항은 분리한 채, 프레임워크를 통해 렌더링하고 업데이트 하는 방식입니다. 이러한 방법을 Jetpack Compose에서 사용하고 있습니다.
다음은 UI를 Jetpack Compose로 구현한 코드입니다.
// 명령형 UI
@Composable
fun Greeting(name: String) {
Text(text = "Hello, $name!")
}
이 코드는 Text라는 Composable 함수를 호출하여 UI를 선언적으로 구성합니다.
UI를 자동으로 업데이트 하기 위해서는 상태를 관리해야 합니다. Compose는 상태를 관찰하고 변경사항이 있을 때 Composable 함수를 다시 호출하여 UI를 갱신합니다. 상태 관리와 관련된 자세한 내용은 다음 글에서 다루도록 하겠습니다.
아래는 명령형 UI와 선언형 UI를 비교하여 정리한 표입니다.
명령형 UI | 선언형 UI |
---|---|
개발자가 UI 요소를 명시적으로 업데이트 | UI 상태(State)에 따라 자동 업데이트 |
화면의 변경 사항을 수동으로 처리 (조건부) | 상태 기반으로 화면 변경을 자동 처리 |
코드가 복잡하고 유지보수가 어려움 | 코드가 간결하고 가독성이 높음 |
예: Android View 기반 XML 코드 | 예: Jetpack Compose |
Jetpack Compose는 선언형 UI 프레임워크로 Composable 함수를 사용하여 UI를 구성합니다. 그렇다면 Compose는 어떤 과정을 거쳐서 화면에 렌더링할까요? 기존의 Android View 시스템처럼 화면을 그리는 과정은 측정(Measure), 레이아웃(Layout), 그리기(Draw)로 비슷하지만 Compose에서는 Composition이라는 단계로 시작됩니다. 각 단계에서 어떤 방식으로 처리되는지 알아보도록 하겠습니다.
Composition 단계에서는 컴포저블 함수들을 호출하여 UI 트리 구조를 생성합니다. 이때 각 Composable 함수는 트리의 노드가 되어 서로의 부모-자식 관계를 형성합니다.
아래와 같은 프로필 카드 컴포저블 함수가 있다고 가정해보겠습니다.
@Composable
fun ProfileCard() {
Row {
Image() // 이미지 컴포넌트
Column {
Text(text = "Kkosang") // 텍스트 컴포넌트
Text(text = "Compose 학습 중")
}
}
}
위의 컴포저블 함수 ProfileCard
가 호출되면 트리 구조를 구성합니다.
Row는 부모 노드가 되고 두 개의 자식 노드(Image,Column)를 갖습니다. 자식 노드 중 Column은 아래에 두 개의 Text 컴포저블이 자식 노드로 연결됩니다.
아래는 생성된 트리 구조입니다.
-> Composition
ProfileCard
├── Row
│ ├── Image
│ └── Column
│ ├── Text ("Kkosang")
│ └── Text ("Compose 학습 중")
Layout 단계에서는 Composition 단계에서 생성된 트리 구조를 기반으로, UI 요소의 측정과 배치가 이루어집니다.
트리 구조는 아래와 같은 세 단계를 거쳐서 탐색됩니다.
탐색 과정이 끝나면, 각 노드에는 할당된 width,height 그리고 배치 될 좌표(x,y)가 있습니다.
ProfileCard
의 Layout 단계 과정은 다음과 같습니다.
Row가 자식 노드들을 측정
Image 측정
Column 측정
첫 번째 Text 측정
두 번째 Text 측정
Column 크기 결정
Column 배치
Row 크기 결정
Row 배치
마지막으로 Drawing 단계에서는 Composition과 Layout 단계를 거쳐 결정된 노드의 크기와 위치 정보를 바탕으로 화면에 UI를 그립니다.
이때 UI를 그리기 위하여 트리 구조를 다시 탐색하며 각 노드의 내용을 화면에 그립니다. 탐색 과정은 최상위 노드에서부터 시작하여 하위 노드로 내려가며 이에따라 UI 요소를 위에서 아래로 그리게 됩니다.
ProfileCard
의 Drawing 과정은 아래와 같은 방식으로 처리됩니다.
최상위 노드 그리기
자식 노드 그리기
순서대로 하위 요소 그리기
자식 노드 반복 처리
이 과정이 완료되면 최종적으로 화면에 ProfileCard의 모든 요소가 렌더링됩니다.
Jetpack Compose는 선언형 UI와 단방향 데이터 흐름 패턴을 중심으로 동작하며 이를 통해 보다 직관적인 방식으로 UI를 설계할 수 있습니다. Composition, Layout, Drawing 단계를 통해 효율적으로 UI를 구성하고 렌더링합니다. 또한 변경 사항에 따라 자동으로 화면을 갱신합니다.
Jetpack Compose를 효율적으로 사용하기 위해서는 상태 관리의 개념을 이해하는 것이 중요합니다. 다음 글에서는 상태를 관리하고 UI를 효율적으로 관리하는 방법에 대해 알아보도록 하겠습니다 :)
https://developer.android.com/develop/ui/compose/phases
https://tecoble.techcourse.co.kr/post/2020-05-18-inheritance-vs-composition/