Composition은 Jetpack Compose의 핵심 프로세스로, 설명적인 Composable 함수에서 화면에 표시되는 라이브 UI 트리를 구축합니다. 레이아웃이 XML에서 로드되어 불변 객체로 변환되는 Android View 시스템과 달리, Composition은 동적 시스템으로 작동합니다. 함수가 실행되고, 메모리에 슬롯을 생성하며, 노드 계층 구조를 형성하고 상태에 바인딩합니다. Google Android Developers, 2026에 따르면, Composition을 이해하는 것은 Compose 애플리케이션의 성능 최적화에 매우 중요합니다.
핵심 요점
Composition은 Composable 함수를 실행하는 프로세스로, 그 결과 사용자 인터페이스가 노드 트리로 내부 표현됩니다. 이 트리의 각 노드는 내장 컴포넌트(Text, Button, Image) 또는 사용자 정의 Composable 함수 호출에 해당합니다. Composition은 Android View 객체를 직접 생성하지 않고, 나중에 Layout 및 Drawing 단계에서 처리되는 추상적인 설명을 구축합니다.
Composition의 주요 특징은 재시작 가능성입니다. 컴포지션 내의 각 Composable 함수는 입력 매개변수나 읽는 상태 객체가 변경된 경우 언제든지 재시작될 수 있습니다. 시스템은 전체 트리를 재시작하지 않고, 변경된 데이터에 실제로 의존하는 함수만 재시작합니다.
기술적으로, Composition은 Composer를 통해 관리됩니다. Composer는 Kotlin 컴파일러가 각 Composable 함수에 내장하는 내부 엔진입니다. Composer는 슬롯(위치 그룹)에 어떤 함수가 어떤 매개변수로 어떤 순서로 호출되었는지 정보를 기록합니다. 후속 호출에서 Composer는 새 데이터를 저장된 데이터와 비교하고 재시작 여부를 결정합니다.
UI 트리 구축 프로세스는 Activity 또는 Fragment 내에서 setContent 메서드를 호출하는 것으로 시작됩니다. 이 메서드는 초기 Composition을 생성하고 루트 Composable 함수 실행을 시작합니다. 그런 다음 각 중첩된 Composable 함수가 노드를 트리에 추가하여 계층 구조를 형성합니다. Row에는 Text와 Button, Column에는 Image와 Card 등이 포함됩니다.
각 트리 노드는 소스 코드에서의 위치에 기반한 고유한 위치 키를 받습니다. 이 키는 후속 실행 시 노드를 식별하는 데 사용됩니다. 위치 키는 Composable 함수 호출 순서가 조건에 의존해서는 안 되는 이유입니다. 한 실행에서 A -> B가 호출되고 다음 실행에서 B -> A가 호출되면, Compose는 이전 노드와 새 노드를 일치시킬 수 없습니다.
@Composable
fun AppScreen() {
Column { // Column 노드 (위치 1)
HeaderSection() // HeaderSection 노드 (위치 2)
ContentSection() // ContentSection 노드 (위치 3)
FooterSection() // FooterSection 노드 (위치 4)
}
}
@Composable
fun HeaderSection() {
Row { // Row 노드 (위치 2.1)
Text("제목") // Text 노드 (위치 2.2)
Icon(...) // Icon 노드 (위치 2.3)
}
}
이 예에서 각 호출은 코드의 순서에 기반한 위치를 받습니다. Column(위치 1)은 세 개의 자식 노드(위치 2, 3, 4)를 포함합니다. HeaderSection은 두 개의 추가 자식 노드(2.1, 2.2, 2.3)를 추가합니다. 다음 재구성에서 ContentSection이 HeaderSection보다 먼저 호출되면, Composer가 노드를 올바르게 일치시킬 수 없습니다. 따라서 규칙: Composable 함수 호출 순서는 안정적이어야 합니다.
Composition의 상태는 State<T> 타입의 객체를 통해 관리됩니다. Composable 함수가 위임된 속성(by)을 통해 State에서 값을 읽으면 해당 State에 대한 의존성을 등록합니다. 값이 변경되면 이 State를 읽은 모든 함수가 다음 컴포지션 단계에서 재시작 대상으로 표시됩니다.
의존성 등록 메커니즘을 스냅샷 시스템이라고 합니다. State가 변경될 때마다 스냅샷이 모든 변경 사항을 기록하고 어떤 함수가 해당 State에 의존하는지 Composer에 알립니다. 중요한 것은: 비-Composable 코드(예: onClick 람다)에서 State를 읽는 것은 의존성을 등록하지 않습니다. Composable 함수 내부 또는 컴포지션 컨텍스트에서 실행되는 람다에서 읽는 것만 의존성을 등록합니다.
스냅샷 시스템은 트랜잭션 방식으로 작동합니다. 단일 이벤트 내의 여러 State 변경이 하나의 트랜잭션으로 결합되어 여러 번의 재구성을 방지합니다. 이는 제스처 처리 시 특히 중요합니다. 한 번의 움직임이 여러 State 객체를 변경하지만, Compose는 한 번의 재구성만 수행합니다.
@Composable
fun StateExample() {
var text by remember { mutableStateOf("Hello") }
var isVisible by remember { mutableStateOf(true) }
Column {
Text(text) // 텍스트에 대한 의존성 등록
if (isVisible) { // isVisible에 대한 의존성 등록
TextField(value = text, onValueChange = { text = it })
}
Button(onClick = { isVisible = !isVisible }) {
Text(if (isVisible) "숨기기" else "보이기")
}
}
}
텍스트를 변경하면 Column, Text, TextField만 재구성됩니다. Column, Button 및 isVisible 조건은 변경되지 않습니다. 이러한 재구성의 격리는 전체 화면을 다시 그리는 시스템에 비해 Compose의 주요 장점입니다. 각 Composable 함수는 직접 읽는 State 객체만 추적합니다.
Composition과 Recomposition은 Composable 함수를 실행하는 두 가지 다른 모드입니다. Composition은 화면 생성 시 한 번 발생합니다. 시스템이 초기 값으로 모든 Composable 함수를 실행하고 초기 UI 트리를 구축합니다. Recomposition은 데이터 변경 시 여러 번 발생합니다. 시스템이 변경된 상태에 의존하는 함수만 재시작합니다.
모드 Composition은 모든 트리 노드를 활성화하고 각 함수에 슬롯을 할당하며 모든 하위 항목을 등록합니다. Recomposition은 선택적으로 작동합니다. Compose가 각 함수의 새 매개변수 값과 이전 매개변수 값을 비교하고, 변경되지 않은 경우 함수가 실행되지 않습니다(스키핑).
Composition과 Recomposition은 비용이 다릅니다. 첫 번째 Composition은 전체 트리 구축과 슬롯 할당이 필요하므로 더 비쌉니다. Recomposition은 더 저렴하며, 특히 대부분의 함수가 안정적인 경우 매개변수가 equals로 비교되고 Compose가 호출을 건너뜁니다. 최대 성능을 위해서는 대부분의 재구성이 가능한 적은 수의 함수에 영향을 미치도록 해야 합니다.
| 특성 | Composition | Recomposition |
|---|---|---|
| 발생 시점 | 한 번, 첫 표시 시 | 여러 번, 데이터 변경 시 |
| 범위 | 전체 트리 | 변경된 함수만 |
| 매개변수 비교 | 수행되지 않음 | 스키핑을 위해 수행 |
| 슬롯 생성 | 예, 모든 슬롯 생성 | 새 노드에만 |
CompositionLocal은 컴포지션 트리를 통해 암시적으로 데이터를 전달하는 메커니즘입니다. 매개변수를 직접 사용하지 않는 수십 개의 중첩된 Composable 함수를 통해 매개변수를 전달해야 하는 문제를 해결합니다. 명시적 매개변수 체인 대신, 데이터는 최상위 수준에서 설정되고 CompositionLocal.current를 통해 중첩된 함수에서 읽힙니다.
MaterialTheme은 CompositionLocal의 가장 잘 알려진 예입니다. 모든 Compose 컴포넌트는 MaterialTheme.colorScheme, MaterialTheme.typography, MaterialTheme.shapes를 통해 색상, 타이포그래피 및 모양을 읽으며, 매개변수를 통해 받지 않습니다. 개발자는 현재 사용자, 로컬라이제이션 설정 또는 화면 구성과 같은 데이터에 대해 자체 CompositionLocal을 만들 수 있습니다.
중요한 제한 사항: CompositionLocal은 자주 변경되는 데이터(스크롤 위치, 입력 필드의 텍스트)에 사용해서는 안 됩니다. CompositionLocal을 읽는 컴포넌트는 값이 변경될 때마다 재시작되므로, 동적 데이터에는 명시적 매개변수 또는 State를 사용하는 것이 좋습니다. CompositionLocal은 거의 또는 전혀 변경되지 않는 구성 데이터에 최적입니다.
val LocalUser = compositionLocalOf<User?> { null }
@Composable
fun AppRoot(user: User, content: @Composable () -> Unit) {
CompositionLocalProvider(LocalUser.provides(user)) {
content()
}
}
@Composable
fun UserAvatar() {
val user = LocalUser.current // 명시적 매개변수 없이 읽기
AsyncImage(model = user?.avatarUrl, contentDescription = "Avatar")
}
CompositionLocalProvider는 LocalUser.current가 지정된 값을 반환하는 범위를 생성합니다. UserAvatar는 중간 함수를 통해 명시적으로 매개변수를 전달하지 않고 사용자를 읽습니다. 이는 데이터가 소수의 리프 노드에서만 필요한 깊은 계층 구조에서 특히 가치가 있습니다.
자주 묻는 질문
Composition 중에 State를 변경하면 현재 재구성이 완료된 후 실행될 새 재구성이 예약됩니다. 무한 루프는 발생하지 않습니다. Compose는 각 재구성이 스냅샷 시스템의 별도 트랜잭션에서 수행됨을 보장합니다.
최신 기기에서 50~100개의 Composable 함수가 있는 화면의 Composition은 1~5ms가 소요됩니다. Google은 60fps 프레임의 경우 16ms 이내를 권장합니다. Composition이 이 제한을 초과하는 경우 LazyColumn을 사용하거나 화면을 더 작은 함수로 나누십시오.
Composition의 직접적인 수동 시작은 불가능합니다. Composer에 의해 자동으로 관리됩니다. 그러나 CompositionContext에 액세스할 수 있는 경우 State를 변경하거나 루트 컴포저블에서 invalidate()를 호출하여 재구성을 강제할 수 있습니다.
View 계층 구조는 한 번 생성되는 Java 객체의 불변 트리입니다. Composition은 데이터가 변경될 때마다 재구축되는 가상 트리입니다. View는 인스턴스 변수에 상태를 저장하고, Composition은 함수 호출 위치에 바인딩된 슬롯에 저장합니다.
Composable 함수가 더 이상 호출되지 않는 경우(예: if 조건이 false가 됨), Composition은 해당 노드를 제거하고 DisposableEffect 정리를 트리거합니다. 다시 나타날 때(if가 다시 true가 됨), 새 노드가 생성되고 이전 노드는 복원되지 않습니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.