본문 바로가기
Android/Compose

[Android] Compose의 UI 렌더링 동작 매커니즘

by 태크민 2025. 3. 7.

다른 대부분의 UI 툴킷과 마찬가지로, Compose는 여러 개의 구분된 단계(Phase)를 거쳐 프레임을 렌더링합니다.
Android의 기존 View 시스템을 살펴보면, 측정(Measure), 배치(Layout), 그리기(Drawing)의 세 가지 주요 단계를 가집니다.
Compose도 이와 유사하지만, 가장 처음에 "Composition"이라는 중요한 추가 단계가 존재합니다.

 


프레임의 세 가지 단계

Compose에는 세 가지 주요 단계(Phase)가 있습니다.

Compose가 데이터를 UI로 변환하는 세 가지 단계

 

1. Composition: 어떤 UI를 표시할지 결정

  • Compose가 컴포저블 함수를 실행하여 UI의 구조를 생성하고, 이를 설명하는 Composition을 만듭니다.

2. Layout: UI를 어디에 배치할지 결정

  • 이 단계는 두 가지 과정으로 구성됩니다: 측정(Measurement)과 배치(Placement)
  • 레이아웃 요소들은 자신의 크기를 측정하고, 자식 요소들을 2D 좌표에 배치합니다.

3. Drawing: UI를 어떻게 렌더링할지 결정

  • UI 요소들은 Canvas에 그려지며, 일반적으로 디바이스 화면에 출력됩니다.

이 단계들의 순서는 일반적으로 동일하며, 데이터가 한 방향으로 흐르면서 구성(Composition) → 레이아웃(Layout) → 그리기(Drawing) 단계를 거쳐 프레임을 생성합니다.(이를 단방향 데이터 흐름이라고 합니다).

그러나 BoxWithConstraints, LazyColumn, LazyRow와 같은 일부 예외적인 컴포저블은 부모의 레이아웃(Layout) 단계에 따라 자식의 구성(Composition) 단계가 달라질 수 있습니다. 

(일반적인 Row와 Column은 모든 항목을 한 번에 구성(Composition)하고 배치(Layout) 하지만, LazyRow와 LazyColumn은 화면에 필요한 항목만 동적으로 구성하고 제거하는 방식으로 동작합니다.)

 

개념적으로는 이러한 단계가 매 프레임마다 실행되지만, 성능 최적화를 위해 Compose는 동일한 입력으로 동일한 결과를 계산하는 작업을 반복하지 않습니다. 즉, 이전 결과를 재사용할 수 있는 경우 Composable 함수 실행을 건너뛰고, Compose UI는 전체 트리를 다시 레이아웃하거나 다시 그리지 않습니다. Compose는 UI를 업데이트하는 데 필요한 최소한의 작업만 수행합니다. 이러한 최적화는 Compose가 각 단계에서 상태(State) 읽기를 추적하기 때문에 가능합니다.

 


단계 이해하기

이 섹션에서는 Compose의 세 가지 단계가 컴포저블에서 어떻게 실행되는지 더 자세히 설명합니다.

1. Composition (구성) 단계

Composition 단계에서는 Compose 런타임이 컴포저블 함수를 실행하고, UI를 나타내는 트리 구조를 생성합니다.
이 UI 트리는 레이아웃 노드(Layout Nodes)로 구성되며, 다음 단계에서 필요한 모든 정보를 포함하고 있습니다.

컴포지션 단계에서 생성된 UI를 나타내는 트리입니다.

 

코드와 UI 트리의 일부는 다음과 같습니다.

코드와 이에 해당하는 UI 트리의 일부.

 

위 예제에서는 코드의 각 컴포저블 함수가 UI 트리의 하나의 레이아웃 노드와 매핑됩니다.
더 복잡한 예제에서는 컴포저블이 로직과 제어 흐름을 포함할 수 있으며, 상태에 따라 다른 UI 트리를 생성할 수도 있습니다.

 

2. Layout 단계

Layout 단계에서는 Composition 단계에서 생성된 UI 트리를 입력으로 사용합니다.
레이아웃 노드들은 각 노드의 크기와 2D 공간에서의 위치를 결정하는 데 필요한 모든 정보를 포함하고 있습니다.

레이아웃 단계에서 UI 트리의 각 레이아웃 노드를 측정하고 배치합니다.

 

Layout 단계에서는 트리를 순회하며 다음 세 단계 알고리즘을 수행합니다.

  1. 자식 측정 (Measure children): 노드가 자식(하위) 요소를 가지고 있는지 측정합니다.
  2. 자신의 크기 결정 (Decide own size): 측정된 결과를 기반으로, 노드는 자신의 크기를 결정합니다.
  3. 자식 배치 (Place children): 각 자식(하위) 노드를 자신의 위치를 기준으로 적절한 위치에 배치합니다.

이 단계를 마치면, 각 레이아웃 노드는 다음과 같은 정보를 가지게 됩니다.

  • 할당된 너비(width)와 높이(height)
  • 그려질 x, y 좌표

 

위에서 설명한 UI 트리를 다시 참고해 보세요.

 

이 UI 트리에 대해, 알고리즘은 다음과 같이 동작합니다.

 

1. Row가 자식 요소인 Image와 Column을 측정합니다.

2. Image를 먼저 측정합니다.

  • 자식 요소가 없으므로 자신의 크기를 결정한 후, 해당 크기를 Row에 전달합니다.

3. 다음으로 Column을 측정합니다.

  • Column은 먼저 자신의 자식 요소(Text 컴포저블 두 개)를 측정합니다.

4. 첫 번째 Text가 측정됩니다.

  • 자식 요소가 없으므로 자신의 크기를 결정하고, 해당 크기를 Column에 전달합니다.
  • 두 번째 Text도 같은 방식으로 측정하여 Column에 전달합니다.

5. Column은 자식 요소들의 측정 결과를 사용하여 자신의 크기를 결정합니다.

  • 너비(width): 자식 요소 중 가장 넓은 요소의 너비를 사용
  • 높이(height): 자식 요소 중 가장 높은 요소의 너비를 사용

6. Column은 자신의 자식 요소를 배치합니다.

  • 각 Text 요소를 세로 방향으로 차례로 배치합니다.

7. Row는 자식 요소들의 측정 결과를 사용하여 자신의 크기를 결정합니다.

  • 너비(width): 자식 요소 중 가장 넓은 요소의 너비를 사용
  • 높이(height): 모든 자식 요소들의 높이를 합산하여 결정
  • 그런 다음, 자식 요소들을 적절한 위치에 배치합니다.

각 노드는 오직 한 번만 방문됩니다.
Compose 런타임은 UI 트리를 한 번만 순회하여 모든 노드를 측정하고 배치할 수 있으므로 성능이 향상됩니다.
UI 트리의 노드 개수가 증가하더라도, 순회 시간은 선형적으로 증가하며,
만약 노드를 여러 번 방문해야 했다면 탐색 시간이 기하급수적으로 증가했을 것입니다.

 

XML (View시스템 )와 컴포즈 렌더링 방식의 차이

Jetpack Compose는 Composable 함수 기반의 선언형 UI로, 기존 XML View 시스템과는 렌더링 방식이 완전히 다릅니다.

 

XML 기반 View 시스템에서는 중첩된 뷰 계층(View Hierarchy)이 깊어질수록 부모 뷰가 자식 뷰를 여러 번 측정하게 되어, 같은 노드를 반복해서 방문하며 추가적인 계산이 필요합니다.

예를 들어, Nested ScrollView 안에 LinearLayout이 있고, 그 안에 여러 개의 뷰가 있는 경우, 부모(ViewGroup)가 여러 번 자식 뷰를 측정하면서 성능이 떨어지게 됩니다.

 

반면, Jetpack Compose는 트리 구조를 유지하지만, 불필요한 연산을 최소화하는 방식으로 UI를 구성합니다.

XML View 시스템 처럼 부모 뷰가 자식 뷰를 반복적으로 측정하지 않습니다.

기존 View 시스템처럼 전체 UI를 다시 그리는 방식이 아니라, 상태 변화에 따라 필요한 부분만 리컴포지션(Recomposition)하여 UI를 갱신합니다.

즉, Compose는 UI를 그릴 때, 트리의 노드 개수만큼만 방문하여 측정과 배치를 한 번만 수행하며, 불필요한 연산을 최소화하고 성능을 최적화합니다.

 

3. 그리기 단계

그리기 단계에서는 트리를 다시 위에서 아래로 순회하며, 각 노드가 차례로 화면에 그려집니다.

그리기 단계에서는 화면에 픽셀을 그립니다.

 

이전 예를 사용하면 트리 콘텐츠가 다음과 같이 그려집니다.

 

1. Row가 먼저 그려지며, 배경색과 같은 내용이 표시됩니다.

2. Image가 자신을 화면에 그립니다.

3. Column이 자신을 화면에 그립니다.

4. 첫 번째 와 두 번째 Text가 그려집니다.

 

UI 트리와 그에 해당하는 렌더링된 표현.

 


상태 읽기 (State Reads)

위에서 나열된 단계 중 하나에서 상태(state)의 값을 읽으면, Compose는 해당 값을 읽을 때 어떤 작업을 수행하고 있었는지를 자동으로 추적합니다. 이 추적 덕분에, 상태 값이 변경될 때 Compose는 해당 값을 읽은 부분을 다시 실행할 수 있으며, 이것이 Compose에서 상태 관찰(state observability)의 기반이 됩니다.

상태는 일반적으로 mutableStateOf()를 사용하여 생성되며, 두 가지 방법 중 하나로 접근할 수 있습니다. 첫 번째는 value 속성을 직접 접근하는 방법이고, 두 번째는 Kotlin의 프로퍼티 위임(property delegate)을 사용하는 방법입니다.

 

[프로퍼티 위임 없이 상태 읽기]

// State read without property delegate.
val paddingState: MutableState<Dp> = remember { mutableStateOf(8.dp) }
Text(
    text = "Hello",
    modifier = Modifier.padding(paddingState.value)
)

 

[프로퍼티 위임을 사용한 상태 읽기]

// State read with property delegate.
var padding: Dp by remember { mutableStateOf(8.dp) }
Text(
    text = "Hello",
    modifier = Modifier.padding(padding)
)

 

프로퍼티 위임의 내부에서는 getter와 setter 함수가 사용되어 State의 값을 읽고 변경할 수 있도록 합니다. 이 getter와 setter는 프로퍼티 값을 참조할 때만 호출되며, 상태가 생성될 때는 호출되지 않습니다. 따라서 위의 두 방식은 동일한 동작을 수행하는 코드입니다.

 

각 코드 블록에서 상태 값을 읽고 변경될 때 다시 실행될 수 있는 영역재시작 범위(restart scope) 라고 합니다. Compose는 상태 값 변경과 재시작 범위를 서로 다른 단계에서 추적합니다.

 


단계별 상태 읽기 (Phased State Reads)

앞서 설명한 것처럼, Compose에는 세 가지 주요 단계(Phases)가 있으며, Compose는 각 단계에서 어떤 상태가 읽혔는지를 추적합니다. 이를 통해, Compose는 UI의 각 요소에서 영향을 받는 특정 단계에만 변경 사항을 알리고, 불필요한 작업을 방지할 수 있습니다.

참고:
상태 인스턴스가 어디에서 생성되고 저장되는지는 단계에 큰 영향을 미치지 않습니다. 중요한 것은 상태 값이 언제, 어디에서 읽히는지입니다.

 

각 단계를 살펴보면서, 상태 값이 읽힐 때 어떤 일이 발생하는지 설명하겠습니다.

 

1단계: 컴포지션 (Composition Phase)

@Composable 함수나 람다 블록 내에서 상태를 읽으면, 컴포지션(Composition) 단계에 영향을 주며, 이후 단계(레이아웃 및 그리기)에도 영향을 미칠 수 있습니다.

상태 값이 변경되면, 리컴포저(Recomposer)**는 해당 상태 값을 읽은 모든 컴포저블 함수를 다시 실행하도록 예약합니다. 그러나, 입력이 변경되지 않았다면 일부 또는 모든 컴포저블 함수의 실행을 건너뛸 수도 있습니다. 

 

컴포지션(Composition) 결과에 따라, Compose UI는 레이아웃(Layout)그리기(Drawing) 단계를 실행할 수 있습니다. 하지만, 콘텐츠가 동일하고 크기 및 레이아웃이 변경되지 않는다면, 이러한 단계를 건너뛸 수도 있습니다.

var padding by remember { mutableStateOf(8.dp) }
Text(
    text = "Hello",
    // The `padding` state is read in the composition phase
    // when the modifier is constructed.
    // Changes in `padding` will invoke recomposition.
    modifier = Modifier.padding(padding)
)

 

2단계: 레이아웃 (Layout Phase)

레이아웃 단계는 측정(measurement)배치(placement) 두 단계로 구성됩니다.

  • 측정 단계: 각 컴포저블이 자신의 크기(Width, Height)를 결정하는 단계
    •  Layout 컴포저블에 전달된 measure 람다, LayoutModifier 인터페이스의 MeasureScope.measure 메서드 등이 실행됩니다.
  • 배치 단계: 각 컴포저블이 어디에 위치할지를 결정하는 단계
    • layout 함수의 placement {} , Modifier.offset { … } 등의 람다 블록이 실행됩니다.

각 단계에서 상태를 읽으면 레이아웃 단계와 잠재적으로 그리기 단계에도 영향을 미칩니다. 상태 값이 변경되면 Compose UI는 레이아웃 단계를 다시 실행하도록 예약합니다. 또한, 크기 또는 위치가 변경되면 그리기 단계도 다시 실행됩니다.

보다 정확히 말하면, 측정 단계와 배치 단계는 각각 독립적인 재시작 범위(restart scope)를 가집니다. 즉, 배치 단계에서 상태를 읽어도 측정 단계가 다시 실행되지 않습니다. 하지만 두 단계는 종종 밀접하게 연결되어 있으므로, 배치 단계에서 상태를 읽으면 측정 단계의 다른 재시작 범위에도 영향을 줄 수 있습니다.

var offsetX by remember { mutableStateOf(8.dp) }
Text(
    text = "Hello",
    modifier = Modifier.offset {
        // offsetX 상태는 배치(Placement) 단계에서 읽힘.
        // offsetX가 변경되면 배치 단계만 다시 실행됨 (측정은 다시 하지 않음).
        IntOffset(offsetX.roundToPx(), 0)
    }
)
  • offsetX는 Modifier.offset 내부에서만 읽히므로, 배치(Placement) 단계에서만 사용되는 상태입니다.
  • 즉, offsetX 값이 변경되면 배치 단계만 다시 실행되며, 측정 단계는 다시 실행되지 않습니다. (측정된 크기는 그대로 유지됨)
var textSize by remember { mutableStateOf(20.sp) }
Text(
    text = "Hello",
    fontSize = textSize
)
  • textSize는 Text 컴포저블의 크기를 결정하는 요소이므로, 측정(Measurement) 단계에서 읽힙니다.
  • textSize가 변경되면, 측정 단계가 다시 실행되면서 Text의 크기가 재계산됩니다.
  • 크기가 바뀌었으므로, 배치 단계도 함께 실행됩니다.

3단계: 그리기 (Drawing Phase)

그리기 코드에서 상태를 읽으면 그리기(Drawing) 단계에 영향을 줍니다.
일반적인 예제로는 Canvas(), Modifier.drawBehind, Modifier.drawWithContent 등이 있습니다.
상태 값이 변경되면, Compose UI는 오직 그리기 단계만 다시 실행합니다.

var color by remember { mutableStateOf(Color.Red) }
Canvas(modifier = modifier) {
    // The `color` state is read in the drawing phase
    // when the canvas is rendered.
    // Changes in `color` restart the drawing.
    drawRect(color)
}

 

 


상태(state) 읽기 최적화

Compose는 상태 읽기 추적을 수행하므로, 각 상태를 적절한 단계에서 읽으면 성능을 최적화할 수 있습니다.

 

예제를 통해 살펴보겠습니다. 아래 코드에서는 Image()에 offset 수정자를 적용하여 최종 레이아 위치를 조정하는 방식으로, 사용자가 스크롤할 때 이미지가 부드럽게 움직이는 효과를 구현하고 있습니다.

Box {
    val listState = rememberLazyListState()

    Image(
        // ...
        // Non-optimal implementation!
        Modifier.offset(
            with(LocalDensity.current) {
                // State read of firstVisibleItemScrollOffset in composition
                (listState.firstVisibleItemScrollOffset / 2).toDp()
            }
        )
    )

    LazyColumn(state = listState) {
        // ...
    }
}

 

이 코드는 동작하지만 성능이 최적화되지 않은 방식입니다. 위 코드에서는 firstVisibleItemScrollOffset의 상태 값을 읽고, 이를 Modifier.offset(offset: Dp) 함수에 전달하고 있습니다.
사용자가 스크롤할 때 firstVisibleItemScrollOffset 값이 변경되는데, Compose는 상태(state)를 읽는 위치를 추적하여 해당 코드 블록을 다시 실행(reinvoke)합니다.
이 경우, Box의 모든 내용을 다시 실행해야 하며, 그 결과 측정(measure), 배치(layout), 그리고 다시 그리기(draw)까지 수행됩니다.

 

이것은 Composition 단계에서 상태를 읽기 때문입니다.
Composition 단계에서 상태를 읽는 것이 반드시 나쁜 것은 아니며, 이는 Compose의 리컴포지션(recomposition)을 통해 UI를 업데이트하는 기본적인 방식입니다.

 

그러나 이 예제에서는 최적의 방식이 아닙니다.

스크롤 이벤트가 발생할 때마다 전체 Composable 콘텐츠가 다시 평가되고, 측정(measure), 배치(layout), 그리고 그리기(draw) 단계까지 다시 실행됩니다.

실제로 변경된 것은 표시되는 위치뿐이며, 실제 내용 자체는 변하지 않았음에도 불구하고 불필요한 리컴포지션이 발생하는 것입니다.
이 문제를 해결하려면 레이아웃(Layout) 단계에서만 다시 트리거하도록 변경할 수 있습니다. 

 

Compose는 offset 수정자의 또 다른 형태를 제공합니다.

Modifier.offset(offset: Density.() -> IntOffset)

 

이 버전은 람다 블록을 매개변수로 받아, 결과적인 오프셋을 반환하는 방식으로 동작합니다.
이를 적용하여 코드를 최적화해보겠습니다.

Box {
    val listState = rememberLazyListState()

    Image(
        // ...
        Modifier.offset {
            // State read of firstVisibleItemScrollOffset in Layout
            IntOffset(x = 0, y = listState.firstVisibleItemScrollOffset / 2)
        }
    )

    LazyColumn(state = listState) {
        // ...
    }
}

 

이 코드가 성능이 더 뛰어난 이유는 무엇일까요?

수정된 코드에서 offset의 람다 블록은 Layout 단계(정확히는 배치 단계)에서 실행됩니다.
즉, firstVisibleItemScrollOffset 상태를 Composition 단계에서 읽지 않기 때문에, 상태 값이 변경될 때에도 리컴포지션을 발생시키지 않고 배치 및 그리기 단계만 다시 수행하게 됩니다.
이렇게 하면 불필요한 Composition을 방지할 수 있어, 매 프레임 변경되는 상태에서도 성능을 최적화할 수 있습니다.

참고사항:
일반적으로 람다 블록을 사용하면 단순한 값 전달보다 약간의 비용이 추가될 수 있습니다.
람다는 익명 함수(Anonymous Function)로, 일반적인 변수보다 호출 비용(Invocation Cost)이 존재 합니다.
람다를 실행할 때 새로운 호출 스택 프레임이 생성 되며, 이 과정에서 메모리 할당 및 실행 컨텍스트 관리 비용이 추가됩니다.

하지만 위 예제의 경우, firstVisibleItemScrollOffset은 스크롤할 때마다 프레임마다 변하는 값이므로, Composition을 계속 다시 실행하는 비용보다 Layout 단계에서만 상태를 읽도록 하는 것이 훨씬 효율적 입니다.
따라서 람다를 사용하는 방식이 성능 최적화에 유리합니다.

 

이 예제는 서로 다른 offset 수정자를 활용하여 코드를 최적화하는 방법을 보여주지만, 여기서 중요한 일반적인 원칙은 상태(state) 읽기를 가능한 한 가장 적은 단계로 국한하여 Compose가 최소한의 작업만 수행하도록 하는 것입니다.

 

물론, Composition 단계에서 상태를 읽어야 하는 경우가 많습니다. 이러한 경우에는 상태 변경을 필터링하여 재구성(Recomposition) 횟수를 최소화할 수 있는 derivedStateOf를 고려해볼 수 있습니다.

 


리컴포지션 루프

이전에 Compose의 단계들은 항상 동일한 순서로 호출되며, 동일한 프레임 내에서 뒤로 돌아갈 수 있는 방법이 없다고 언급했습니다. 그러나 이는 애플리케이션이 서로 다른 프레임 간에 재구성 루프(composition loop) 에 빠지는 것을 막지는 않습니다. 다음 예제를 살펴보겠습니다,

Box {
    var imageHeightPx by remember { mutableStateOf(0) }

    Image(
        painter = painterResource(R.drawable.rectangle),
        contentDescription = "I'm above the text",
        modifier = Modifier
            .fillMaxWidth()
            .onSizeChanged { size ->
                // Don't do this
                imageHeightPx = size.height
            }
    )

    Text(
        text = "I'm below the image",
        modifier = Modifier.padding(
            top = with(LocalDensity.current) { imageHeightPx.toDp() }
        )
    )
}

 

위 코드에서는 잘못된 방식으로 세로 정렬을 구현하고 있습니다.
이미지가 상단에 배치되고, 그 아래에 텍스트가 위치하도록 만들고자 했습니다.

이를 위해 Modifier.onSizeChanged()를 사용하여 이미지의 크기가 결정된 후 해당 높이를 가져오고, 이를 Modifier.padding()을 이용해 텍스트의 top 패딩 값으로 적용하고 있습니다. 하지만 픽셀(Px)을 다시 Dp로 변환하는 부자연스러운 과정 자체가 코드에 문제가 있음을 암시합니다.

 

문제점: 한 프레임 안에서 최종 레이아웃이 결정되지 않음

이 코드의 문제는 단일 프레임 내에서 최종적인 레이아웃을 결정하지 못한다는 점입니다.
즉, 여러 프레임에 걸쳐 불필요한 작업이 수행되며, 결과적으로 UI가 화면에서 깜빡이거나 요소가 갑자기 이동하는 문제(jumping UI) 가 발생할 수 있습니다.

 

각 프레임에서 어떤 일이 발생하는지 단계별로 살펴보겠습니다.

 

첫 번째 프레임

1. 구성(Composition) 단계

  • imageHeightPx의 초기 값이 0이므로, Text의 Modifier.padding(top = 0)이 설정됩니다.

2. 레이아웃(Layout) 단계

  • onSizeChanged 콜백이 호출되면서 imageHeightPx가 실제 이미지의 높이 값으로 업데이트됩니다.
  • Compose는 값이 변경되었음을 감지하고 다음 프레임에서 재구성을 예약합니다.

3. 그리기(Draw) 단계

  • 이 프레임에서는 여전히 imageHeightPx = 0 이므로, 텍스트는 0의 패딩을 가진 상태로 렌더링됩니다.

두 번째 프레임 (재구성 후)

1. 구성(Composition) 단계

  • 이번에는 imageHeightPx가 업데이트된 상태이므로, Text의 padding이 이미지의 높이만큼 설정됩니다.

2. 레이아웃(Layout) 단계

  • onSizeChanged가 다시 호출되지만, imageHeightPx의 값이 변하지 않았기 때문에 추가적인 재구성은 발생하지 않습니다.

3. 그리기(Draw) 단계

  • 이제 올바른 패딩이 적용된 상태로 텍스트가 렌더링됩니다.

최종적으로 텍스트가 올바른 패딩을 가지지만, 패딩 값을 다른 단계로 전달하기 위해 한 프레임을 추가로 소비하는 비효율적인 방식입니다.
이로 인해 UI가 한 프레임 동안 겹쳐 보이는 문제가 발생할 수 있습니다.

 

 

 

이 예제가 다소 인위적으로 보일 수 있지만, 다음과 같은 패턴을 조심해야 합니다:

  1. Modifier.onSizeChanged(), onGloballyPositioned() 같은 레이아웃 관련 작업을 실행
  2. 실행된 결과를 사용해 상태(state)를 업데이트
  3. 업데이트된 상태를 padding(), height() 같은 레이아웃 속성에 반영
  4. 이런 과정이 반복될 가능성이 있음

위 문제를 해결하는 방법은 적절한 레이아웃 방식을 사용하는 것입니다. 예제에서처럼 이미지를 위에 두고, 그 아래에 텍스트를 배치하고 싶다면, Column()을 사용하면 간단히 해결됩니다.

하지만 더 복잡한 경우에는 직접 커스텀 레이아웃을 만들어야 할 수도 있습니다. 이에 대한 자세한 내용은 커스텀 레이아웃 가이드를 참고하세요.

 

중요한 원칙

화면에 배치되는 여러 UI 요소가 서로 영향을 주는 경우, "하나의 기준 값"(single source of truth)을 정하는 것이 중요합니다.
적절한 레이아웃을 사용하면 여러 요소의 크기나 위치를 조정하는 역할을 하는 부모 요소가 자동으로 관계를 조정해 줍니다.

하지만, 상태(state)를 직접 업데이트하면서 레이아웃을 조정하려고 하면 이 원칙이 깨지게 됩니다.
그 결과 불필요한 재구성이 발생하거나 화면이 깜빡이는 문제가 생길 수 있습니다.

 

 

끝.


참고자료

https://developer.android.com/develop/ui/compose/phases?hl=ko&_gl=1*2rcudw*_up*MQ..*_ga*MTI5NjMzODEzOC4xNzQxMjQwMjk1*_ga_6HH9YJMN9M*MTc0MTI0MzkwOC4yLjAuMTc0MTI0MzkwOC4wLjAuMTI5NTkwOTk2Mg..

 

Jetpack Compose 단계  |  Android Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. Jetpack Compose 단계 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 대부분의 다른 UI 도구 키트와 마찬가

developer.android.com