Composition vs Subcomposition

  • Composition은 기본 단위로 항상 사용되며,
  • Subcomposition 은 특별한 상황에서 동적으로 자식 UI를 제어하기 위해 사용하는 별도 Composition 단위입니다.
항목CompositionSubcomposition
정의Composable 함수 실행 결과를 담는 기본 Composition 단위특정 목적을 가진 동적/지연 Composition 단위
생성 방식setContent, 일반 Composable 내부에서 자동 생성subcompose()를 통해 명시적 생성
생성 시점Composition 트리 구성 시 즉시 실행필요 시점까지 지연 가능 (예: 레이아웃 계산 이후)
노드 트리 연결현재 Composition 트리에 노드들이 바로 연결됨별도 노드 트리로 구성 가능
CompositionContext상위 Composition의 Context를 자동 상속명시적으로 Context 설정 필요
Recomposition 처리부모 Composition의 상태 변화에 따라 재구성됨forceRecomposeChildren() 등으로 독립적 재구성 가능
사용 목적기본적인 UI 구조 구성자식 UI를 상태/측정값/조건 기반으로 동적 구성
사용 예시Column, Box, 일반 Composable들SubcomposeLayout, LazyColumn, Popup, Dialog, BoxWithConstraints
slot table루트 Composition 단위로 독립적으로 존재보통 별도의 slot table 사용, runtime이 따로 관리
구조 유연성고정된 자식 구조동적 구성, 유연한 배치 가능

Composition

  • setContent → 루트 Composition 생성 → ReusableComposeNode로 노드 구성
  • 각 Composition은 독립적인 Composable 트리를 가지며, 서로 영향을 주지 않음
  • 내부적으로는 변화 감지, 노드 생명주기, Replaceable Group 등 복잡한 로직으로 구성됨

setContent의 역할

  • setContent는 새로운 루트 Composition (Root Composition) 을 생성합니다.
  • Compose UI 라이브러리가 런타임(Compose Runtime) 과 만나는 가장 일반적인 진입 지점입니다.
  • 이 루트 Composition은 가능한 경우 재사용되며, 각기 다른 Composable 트리를 독립적으로 관리합니다.

루트 Composition의 특징

  • 각 루트 Composition서로 연결되어 있지 않음.
  • Fragment 예시:
    • Fragment 1 → setContent() 호출
    • Fragment 2 → XML 내 여러 ComposeView → 각자 setContent() 호출
    • Fragment 3 → setContent() 호출 위 예시에서는 총 5개의 루트 Composition이 생기며, 모두 독립적입니다.

LayoutNode

  • LayoutNode는 Compose UI에서 실제 UI 구조를 구성하는 핵심 노드입니다.  
  • ReusableComposeNode를 통해 생성되며, 화면에 그려질 Composable 트리의 실질적인 기반이 되는 객체입니다.  
  • 이 노드는 MeasurePolicy, Modifier, LayoutDirection 등의 정보를 포함하며, 레이아웃 측정 및 배치에 사용됩니다.

아래는 Layout() 컴포저블 내부에서 LayoutNode가 어떻게 생성되고 설정되는지를 보여주는 예시입니다:

ReusableComposeNode 동작 과정

@Composable
@UiComposable
inline fun Layout(
    content: @Composable @UiComposable () -> Unit,
    modifier: Modifier = Modifier,
    measurePolicy: MeasurePolicy
) {
    val compositeKeyHash = currentCompositeKeyHashCode.hashCode()
    val localMap = currentComposer.currentCompositionLocalMap
    val materialized = currentComposer.materialize(modifier)
    ReusableComposeNode<ComposeUiNode, Applier<Any>>(
        factory = ComposeUiNode.Constructor,
        update = {
            set(measurePolicy, SetMeasurePolicy)
            set(localMap, SetResolvedCompositionLocals)
            set(compositeKeyHash, SetCompositeKeyHash)
            set(materialized, SetModifier)
        },
        content = content
    )
}

Layout.kt

  1. 노드 생성: factory 함수로 생성
  2. 초기화: update 람다로 초기화
  3. Replaceable Group 생성:
    • 콘텐츠를 감싸는 그룹
    • 고유 키를 통해 식별 가능
    • 그룹 내부에서 실행된 content는 해당 노드의 자식 노드로 간주됨

update 블록의 set 호출

  • set 호출은 다음 조건에서만 실행:
    • 노드가 처음 생성될 때
    • 해당 속성의 값이 이전과 다를 때
  • 성능 최적화를 위한 변화 감지 메커니즘

Subcomposition

구조

graph TD
	  Recomposer["🧠 Recomposer"]
	  RootA["📦 Root Composition (Activity)"]
	  RootB["📦 Root Composition (Dialog)"]
	
	  SubA1["📦 Subcomposition A1 (LazyColumn)"]
	  SubA1Item1["📦 Subcomposition A1-1 (LazyColumn Item 1)"]
	  SubA1Item2["📦 Subcomposition A1-2 (LazyColumn Item 2)"]
	  SubA2["📦 Subcomposition A2 (BoxWithConstraints)"]
	  CompositionA2Child["📦 Composition A2-Child"]
	
	  SubB1["📦 Subcomposition B1 (Popup)"]
	  SubB2["📦 Subcomposition B2 (DropdownMenu)"]
	  CompositionB1Child["📦 Composition B1-1 (DialogContent)"]
	
	  Recomposer --> RootA
	  Recomposer --> RootB
	
	  RootA --> SubA1
	  SubA1 --> SubA1Item1
	  SubA1 --> SubA1Item2
	  SubA1Item2 --> CompositionA2Child
	  RootA --> SubA2
	
	  RootB --> SubB1
	  SubB1 --> CompositionB1Child
	  RootB --> SubB2
	
	  classDef root fill:#E0F7FA,stroke:#00796B,stroke-width:2px
	  classDef sub fill:#FFF3E0,stroke:#F57C00,stroke-width:1px
	  classDef comp fill:#E8F5E9,stroke:#388E3C,stroke-width:1.5px
	
	  class RootA,RootB root
	  class SubA1,SubA1Item1,SubA1Item2,SubA2,SubB1,SubB2 sub
	  class CompositionA2Child,CompositionB1Child comp
  • 각 Composition은 자신의 부모 CompositionContext에 대한 참조를 가짐. 단, 루트 Composition만 예외적으로 부모가 Recomposer임
  • CompositionLocal 값을 상위에서 하위로 전파할 수 있음
  • Invalidation(상태 변경 감지)도 트리 구조에서 자연스럽게 전파됨
  • 결과적으로 마치 단일 Composition처럼 동작

용도

1. 초기 Composition 지연

  • 어떤 정보가 먼저 계산되어야 하는 경우, 해당 정보를 기준으로 하위 UI를 구성해야 할 때 사용
  • 일반적인 Layout과 유사하지만, 레이아웃 단계에서 독립적인 Composition을 생성하고 실행함.
  • 트리 구조 덕분에, 자식 Composable이 상위에서 계산된 값에 의존할 수 있음.
  • BoxWithConstraints가 내부적으로 사용함

2. 특정 서브트리가 다른 종류의 노드를 생성

  • 하위 Composable 트리가 기존 노드 구조와 다른 타입의 노드를 만들어야 할 때 Subcomposition을 사용
  • 노드 트리의 유연한 구성 및 관리에 유리

예시: SubcomposeLayout

@Composable
@UiComposable
fun SubcomposeLayout(
    state: SubcomposeLayoutState,
    modifier: Modifier = Modifier,  
    measurePolicy: SubcomposeMeasureScope.(Constraints) -> MeasureResult // ✅ Subcompose용 MeasurePolicy
 
) {
    val compositeKeyHash = currentCompositeKeyHashCode.hashCode()
    val compositionContext = rememberCompositionContext() // ✅ 별도 CompositionContext 생성
    val materialized = currentComposer.materialize(modifier)
    val localMap = currentComposer.currentCompositionLocalMap
    ReusableComposeNode<LayoutNode, Applier<Any>>( // ✅ LayoutNode 사용 (하위 Composition 지원)
        factory = LayoutNode.Constructor,
        update = {
            set(state, state.setRoot) // ✅ SubcomposeLayoutState 등록
            set(compositionContext, state.setCompositionContext) // ✅ 하위 Composition 연결
            set(measurePolicy, state.setMeasurePolicy)
            set(localMap, SetResolvedCompositionLocals)
            set(materialized, SetModifier)
            set(compositeKeyHash, SetCompositeKeyHash)
        }
    )
    // ✅ Subcomposition 재구성을 강제로 트리거
    if (!currentComposer.skipping) {
        SideEffect { state.forceRecomposeChildren() }
    }
}

SubcomposeLayout.kt