본문 바로가기
Android/Compose

[Android] Compose에서 Modifier 커스텀하기

by 태크민 2025. 5. 19.

커스텀 Modifier 만들기

Compose는 일반적인 동작을 위한 다양한 Modifier를 기본으로 제공하지만, 직접 커스텀 Modifier를 만들 수도 있습니다.

Modifier는 다음과 같은 여러 구성 요소로 이루어져 있습니다:

 

1. Modifier Factory (Modifier 팩토리)

  • Modifier에 확장 함수로 작성됩니다.
  • 체이닝(chaining) 이 가능하도록 해주며, Compose에서 Modifier를 구성하는 표준적인 API 역할을 합니다.
  • 내부적으로는 실제 UI를 수정하는 Modifier 요소(Modifier Element) 를 생성해 반환합니다.

2. Modifier Element (Modifier 요소)

  • Modifier의 실제 동작을 구현하는 부분입니다.

필요한 기능에 따라 커스텀 Modifier를 구현하는 방법은 여러 가지가 있습니다.

가장 쉬운 방법은 이미 존재하는 Modifier들을 조합하여 Modifier Factory만 정의하는 것입니다.

  • 예: padding, background 등을 조합한 Modifier

보다 고급 동작이 필요한 경우에는 Modifier.Node API를 사용하여 Modifier 요소를 직접 구현할 수 있으며, 이 방식은 더 low level이지만 훨씬 더 유연한 제어가 가능합니다.

 


기존 Modifier들을 체이닝하여 조합하기

대부분의 경우, 기존 Modifier들을 조합하는 것만으로도 커스텀 Modifier를 만들 수 있습니다.
예를 들어, Modifier.clip()은 내부적으로 graphicsLayer Modifier를 사용하여 구현되어 있습니다.
이 방식은 기존 Modifier 요소들을 활용하고, 여기에 직접 만든 Modifier 팩토리를 더해 구성하는 전략입니다.

 

따라서 커스텀 Modifier를 구현하기 전에, 이와 같은 전략으로 구현할 수 있는지 먼저 확인하는 것이 좋습니다. (Modifier.Node를 사용한 직접 구현은 복잡하고 유지보수 비용이 큽니다.)

fun Modifier.clip(shape: Shape) = graphicsLayer(shape = shape, clip = true)

 

또는, 동일한 Modifier 조합을 자주 반복해서 사용하는 경우, 이를 하나의 커스텀 Modifier로 감싸서 사용할 수도 있습니다:

fun Modifier.myBackground(color: Color) = padding(16.dp)
    .clip(RoundedCornerShape(8.dp))
    .background(color)

 


Composable Modifier Factory를 사용한 커스텀 Modifier 만들기

기존 Modifier에 값을 전달하기 위해 Composable 함수를 활용하여 커스텀 Modifier를 만들 수도 있습니다.
이러한 방식을 Composable Modifier Factory라고 부릅니다.

💡 참고:
이전 Compose 버전에서는 composed {}를 사용하는 것을 권장하고, 이 방식은 lint 규칙을 통해 제한했습니다.
하지만 현재는 composed {} 사용이 권장되지 않으며, 관련 lint 규칙도 제거되었습니다.

 

Composable Modifier Factory를 사용하면 animate*AsState 같은 상위 Compose API 나, Compose 상태 기반 애니메이션 API 들을 활용할 수 있는 장점이 있습니다.

예를 들어, 아래 코드는 활성화 여부에 따라 알파값(투명도) 를 애니메이션으로 변경하는 Modifier입니다:

@Composable
fun Modifier.fade(enable: Boolean): Modifier {
    val alpha by animateFloatAsState(if (enable) 0.5f else 1.0f)
    return this then Modifier.graphicsLayer { this.alpha = alpha }
}

 

⚠️ 주의:
커스텀 Modifier를 만들 때 Modifier 체인을 끊으면 안 됩니다. this를 참조하지 않으면 이전에 적용된 Modifier들이 모두 무시됩니다.
예제처럼 return this then Modifier 형식으로 반환하거나, 혹은 return graphicsLayer { ... }와 같이 암묵적으로 this를 유지하도록 작성해야 합니다.

 

당신의 커스텀 Modifier가 CompositionLocal에서 기본값을 가져와서 제공하는 편의 메서드라면,
이를 구현하는 가장 쉬운 방법은 Composable Modifier Factory를 사용하는 것입니다:

@Composable
fun Modifier.fadedBackground(): Modifier {
    val color = LocalContentColor.current
    return this then Modifier.background(color.copy(alpha = 0.5f))
}

 

이 방식에는 아래에서 설명할 몇 가지 주의사항(caveats)이 있습니다.

 

CompositionLocal 값은 Modifier 팩토리가 호출되는 위치에서 해석된다

Composable Modifier Factory를 사용해 커스텀 Modifier를 만들 때, CompositionLocal 값은 Modifier가 실제로 사용되는 곳이 아니라, 생성된 시점의 Composition 트리에서 가져옵니다.
이로 인해 예상치 못한 결과가 발생할 수 있습니다.

예를 들어, 앞서 소개한 CompositionLocal 기반 Modifier를 약간 다르게 작성한 예제를 보겠습니다:

@Composable
fun Modifier.myBackground(): Modifier {
    val color = LocalContentColor.current
    return this then Modifier.background(color.copy(alpha = 0.5f))
}

@Composable
fun MyScreen() {
    CompositionLocalProvider(LocalContentColor provides Color.Green) {
        // 이 시점에서 배경 Modifier는 초록색으로 생성됨
        val backgroundModifier = Modifier.myBackground()

        // LocalContentColor를 빨간색으로 변경
        CompositionLocalProvider(LocalContentColor provides Color.Red) {

            // Box는 빨간색이 아닌, 초록색 배경을 가짐
            Box(modifier = backgroundModifier)
        }
    }
}

 

이 경우, Box는 빨간색이 아닌 초록색 배경을 가지게 됩니다.
이유는 Modifier.myBackground()가 호출될 당시 LocalContentColor 값이 초록색이었기 때문입니다.

 

💡 만약 Modifier가 사용하는 CompositionLocal 값을 항상 실제 사용 시점에서 가져오길 원한다면, Modifier.Node를 사용해 커스텀 Modifier를 구현해야 합니다.
이 방식에서는 CompositionLocal 값이 정확하게 사용 위치에서 해석되며, 안전하게 외부로 분리(hoist)할 수도 있습니다.

 

Composable 함수 기반 Modifier는 스킵되지 않는다

Composable Modifier Factory는 반환값이 있는 Composable 함수이기 때문에 절대 스킵되지 않습니다.
즉, 이 Modifier 함수는 모든 recomposition 시점마다 호출되며, 재구성이 자주 일어날 경우 비용이 많이 들 수 있습니다.

 

Composable 함수 기반 Modifier는 Composable 함수 내부에서만 호출 가능하다

모든 Composable 함수와 마찬가지로, Composable Modifier Factory도 반드시 Composition 내부에서 호출되어야 합니다.
이로 인해 Modifier를 Composition 외부로 hoist(분리)할 수 없게 되며, 재사용과 성능 최적화에 제약이 생깁니다.

반면, Composable이 아닌 일반 Modifier Factory는 Composable 함수 외부로 자유롭게 hoist할 수 있어, 할당을 줄이고 성능을 높이는 데 유리합니다:

✅ 일반 Modifier (호이스팅 가능)

val extractedModifier = Modifier.background(Color.Red) // 컴포저블 밖으로 추출 가능 → 재사용 & 성능 향상

 

❌ 컴포저블 Modifier (호이스팅 불가)

@Composable
fun Modifier.composableModifier(): Modifier {
    val color = LocalContentColor.current.copy(alpha = 0.5f)
    return this then Modifier.background(color)
}

@Composable
fun MyComposable() {
    val composedModifier = Modifier.composableModifier() // 여기보다 더 위로는 추출 불가
}

 


Modifier.Node를 사용한 커스텀 Modifier 동작 구현

Modifier.Node는 Compose에서 커스텀 Modifier를 구현할 수 있는 저수준(low-level) API입니다.
Compose의 기본 Modifier들도 이 API를 사용해 구현되어 있으며, 커스텀 Modifier를 만들 때 가장 성능이 뛰어난 방식입니다.

💡 참고:
커스텀 Modifier를 만들기 위한 다른 API로 composed {}가 있지만,
성능 이슈로 인해 더 이상 권장되지 않습니다.
Modifier.Node는 이러한 문제를 해결하기 위해 처음부터 설계되었으며,
composed 기반 Modifier보다 훨씬 높은 성능을 제공합니다.

 

Modifier.Node 기반 커스텀 Modifier 구성 요소

Modifier.Node를 사용하여 커스텀 Modifier를 구현하려면 다음 세 가지 구성 요소가 필요합니다:

  1. Modifier.Node 구현체
    • Modifier의 로직과 상태(state) 를 담고 있는 클래스입니다.
  2. ModifierNodeElement 구현체
    • Modifier.Node 인스턴스를 생성 및 갱신(create & update) 하는 역할을 합니다.
  3. (선택) Modifier 팩토리
    • 위에서 설명한 것처럼 확장 함수 형태로 작성되어 사용성을 높여줍니다.

ModifierNodeElement는 상태를 가지지 않는(stateless) 클래스이며, 매 recomposition마다 새로운 인스턴스가 생성됩니다.

반면, Modifier.Node 클래스는 상태를 가질 수 있으며, 여러 번의 recomposition 동안 유지되고, 재사용도 가능합니다.

 

다음 섹션에서는 이 각각의 구성 요소를 설명하고, 원을 그리는 커스텀 Modifier 예제를 통해 구현 방법을 보여줍니다.

 

Modifier.Node

Modifier.Node 구현체(예제에서는 CircleNode)는 커스텀 Modifier의 실제 기능을 구현합니다.

// Modifier.Node
private class CircleNode(var color: Color) : DrawModifierNode, Modifier.Node() {
    override fun ContentDrawScope.draw() {
        drawCircle(color)
    }
}

 

이 예제에서는 Modifier 함수에서 전달받은 색상으로 원을 그리는 동작을 수행합니다.

 

하나의 노드는 Modifier.Node를 구현하며, 필요에 따라 0개 이상의 노드 타입(Node Type) 을 함께 구현할 수 있습니다.
Modifier가 어떤 기능을 수행해야 하느냐에 따라 다양한 타입이 존재합니다.

예제에서는 그리기 기능(draw) 이 필요하므로 DrawModifierNode를 구현했고, 이 타입을 통해 draw() 메서드를 오버라이드할 수 있게 됩니다.

 

사용 가능한 노드 타입들은 다음과 같습니다:

Node 타입 목록 및 설명

노드 설명
LayoutModifierNode 감싸고 있는 콘텐츠의 측정 및 레이아웃 방식을 변경하는 Modifier.Node
DrawModifierNode 레이아웃 공간에 그리기(draw) 작업을 수행하는 Modifier.Node
CompositionLocalConsumerModifierNode CompositionLocal 값을 읽을 수 있게 해주는 Modifier.Node
SemanticsModifierNode 접근성, 테스트 등을 위해 semantics 키/값을 추가하는 Modifier.Node
PointerInputModifierNode 터치 입력(PointerInputChanges) 을 처리하는 Modifier.Node
ParentDataModifierNode 부모 레이아웃에 데이터를 전달하는 Modifier.Node
LayoutAwareModifierNode onMeasured, onPlaced 콜백을 수신하는 Modifier.Node
GlobalPositionAwareModifierNode 콘텐츠의 전역 위치가 변경될 때 onGloballyPositioned 콜백을 수신하는 Modifier.Node
ObserverModifierNode observeReads 블록 내에서 읽은 스냅샷 객체의 변경을 감지하고, onObservedReadsChanged 를 구현하는 Modifier.Node
DelegatingNode 다른 Modifier.Node 인스턴스에 작업을 위임할 수 있는 Modifier.Node
TraversableNode 동일 타입 또는 특정 키를 가진 노드들을 위/아래 방향으로 순회할 수 있게 해주는 Modifier.Node

 

노드(Node)는 해당 노드에 대응되는 엘리먼트(Element)에서 update가 호출 될 때 자동으로 invalidate 됩니다.

예제에서처럼 DrawModifierNode의 경우, 엘리먼트에서 update가 호출되면 노드가 자동으로 다시 그리기(draw) 를 트리거하고,
색상이 올바르게 업데이트됩니다. (color가 변경되면 update()가 호출됩니다.)

 

자동 invalidate는 기본 동작이며, 원한다면 이를 비활성화(opt-out) 할 수도 있습니다.
(자세한 방법은 아래에 설명됩니다.)

 

ModifierNodeElement

ModifierNodeElement는 커스텀 Modifier를 생성(create) 하거나 업데이트(update) 하기 위한 데이터를 담고 있는 불변 클래스(immutable class) 입니다.

// ModifierNodeElement
private data class CircleElement(val color: Color) : ModifierNodeElement<CircleNode>() {
    override fun create() = CircleNode(color)

    override fun update(node: CircleNode) {
        node.color = color
    }
}

 

ModifierNodeElement를 구현할 때는 다음 메서드들을 반드시 오버라이드해야 합니다:

  • create:
    이 함수는 Modifier가 처음 적용될 때 노드를 생성하는 데 사용됩니다.
    보통 이 함수에서는 Modifier Node를 생성하고, 전달된 파라미터로 초기화합니다.
    (위 예제에서는 color를 사용해 CircleNode 생성)
  • update:
    이 함수는 기존 위치에 동일한 Modifier가 다시 제공되었지만, 속성이 변경된 경우 호출됩니다.
    (예: color 값이 바뀐 경우)
    이 비교는 클래스의 equals 메서드에 의해 결정되며, 기존 노드 인스턴스가 파라미터로 전달됩니다.
    여기서 노드의 상태를 새 값에 맞게 갱신해야 하며, 이처럼 노드를 재사용할 수 있도록 하는 구조는 Modifier.Node가 제공하는 성능 향상의 핵심입니다.
    → 따라서 update 내에서 새 노드를 만들면 안 되고, 반드시 기존 노드를 업데이트해야 합니다.

또한 ModifierNodeElement는 equals와 hashCode 를 올바르게 구현해야 합니다.

equals 비교 결과가 false일 때만 update가 호출되므로, 이를 잘못 구현하면 불필요한 업데이트가 반복되어 성능이 저하될 수 있습니다.

이를 자동으로 처리하려면 data class로 선언하는 것이 가장 좋습니다.

 

위 예제에서는 이를 처리하기 위해 data class를 사용했습니다.
이 equals 및 hashCode 메서드는 노드가 업데이트되어야 하는지를 판단하는 데 사용됩니다.

만약 ModifierNodeElement에 포함된 속성 중에서 노드의 업데이트 필요 여부에 영향을 주지 않는 속성 등의 이유로 data class 사용을 피하고 싶다면, 직접 equals와 hashCode를 구현할 수 있습니다.

 

Modifier 팩토리

Modifier 팩토리는 사용자가 접근하는 퍼블릭 API입니다.
대부분의 구현에서는 단순히 ModifierNodeElement를 생성하고 Modifier 체인에 추가하는 역할을 합니다:

// Modifier 팩토리
fun Modifier.circle(color: Color) = this then CircleElement(color)

 

전체 예제

다음은 Modifier.Node API를 사용하여 원을 그리는 커스텀 Modifier를 구현한 전체 예제입니다:

// Modifier 팩토리
fun Modifier.circle(color: Color) = this then CircleElement(color)

// ModifierNodeElement
private data class CircleElement(val color: Color) : ModifierNodeElement<CircleNode>() {
    override fun create() = CircleNode(color)

    override fun update(node: CircleNode) {
        node.color = color
    }
}

// Modifier.Node
private class CircleNode(var color: Color) : DrawModifierNode, Modifier.Node() {
    override fun ContentDrawScope.draw() {
        drawCircle(color)
    }
}

 


Modifier.Node를 사용할 때 자주 마주치는 상황들

Modifier.Node를 사용해 커스텀 Modifier를 만들 때, 다음과 같은 일반적인 상황들을 자주 마주치게 됩니다.

 

매개변수가 없는 경우 (Zero parameters)

Modifier에 전달되는 매개변수가 전혀 없다면, Modifier는 업데이트될 필요가 없기 때문에, data class일 필요도 없습니다.

다음은 Composable에 고정된 패딩을 적용하는 Modifier를 구현한 예시입니다:

// Modifier 팩토리
fun Modifier.fixedPadding() = this then FixedPaddingElement

// ModifierNodeElement (매개변수 없음)
data object FixedPaddingElement : ModifierNodeElement<FixedPaddingNode>() {
    override fun create() = FixedPaddingNode()
    override fun update(node: FixedPaddingNode) {}
}

// Modifier.Node
class FixedPaddingNode : LayoutModifierNode, Modifier.Node() {
    private val PADDING = 16.dp

    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult {
        val paddingPx = PADDING.roundToPx()
        val horizontal = paddingPx * 2
        val vertical = paddingPx * 2

        val placeable = measurable.measure(
            constraints.offset(-horizontal, -vertical) // 패딩만큼 공간을 줄임
        )

        val width = constraints.constrainWidth(placeable.width + horizontal)
        val height = constraints.constrainHeight(placeable.height + vertical)

        return layout(width, height) {
            placeable.place(paddingPx, paddingPx) // 내용 배치 위치에 패딩 적용
        }
    }
}

 

이 예제에서는:

  • fixedPadding()은 항상 동일한 Modifier를 반환하므로 상태 업데이트가 필요 없습니다.
  • FixedPaddingElement는 매개변수가 없는 단일 인스턴스이므로 data object로 선언됩니다.
  • FixedPaddingNode는 LayoutModifierNode를 구현하여 패딩을 적용한 레이아웃 측정과 배치를 수행합니다.

CompositionLocal 참조하기

Modifier.Node 기반 Modifier는 기본적으로 CompositionLocal 같은 Compose 상태 객체의 변경을 자동으로 관찰하지 않습니다.

하지만 Modifier.Node는 Composable 함수로 만든 Modifier와 달리, Modifier가 UI 트리 내에서 실제 사용되는 위치 기준**으로 CompositionLocal의 값을 읽을 수 있는 장점이 있습니다. 이때는 currentValueOf` 함수를 사용합니다.

 

다만, Modifier Node 인스턴스는 상태 변경을 자동으로 감지하지는 않기 때문에, CompositionLocal의 변경에 자동으로 반응하게 하려면 특정 Scope 내부에서 값을 읽어야 합니다:

  • DrawModifierNode → ContentDrawScope
  • LayoutModifierNode → MeasureScope, IntrinsicMeasureScope
  • SemanticsModifierNode → SemanticsPropertyReceiver

다음은 LocalContentColor의 값을 관찰하여, 해당 색상으로 배경을 그리는 Modifier 예제입니다.
ContentDrawScope는 스냅샷 상태 변화를 관찰하므로, LocalContentColor가 바뀌면 자동으로 다시 그려집니다:

class BackgroundColorConsumerNode :
    Modifier.Node(),
    DrawModifierNode,
    CompositionLocalConsumerModifierNode {

    override fun ContentDrawScope.draw() {
        val currentColor = currentValueOf(LocalContentColor)
        drawRect(color = currentColor)
        drawContent()
    }
}

 

 

Scope(스코프) 바깥의 상태 변경에도 반응하여 Modifier를 자동으로 업데이트하고 싶을 경우에는 ObserverModifierNode 를 사용해야 합니다.

예를 들어, Modifier.scrollable은 이 방식을 사용하여 LocalDensity의 변경을 감지합니다.
아래는 그와 유사한 단순화된 예제입니다:

class ScrollableNode :
    Modifier.Node(),
    ObserverModifierNode,
    CompositionLocalConsumerModifierNode {

    // 기본 플링 동작 객체 (Density가 준비되면 초기화)
    val defaultFlingBehavior = DefaultFlingBehavior(splineBasedDecay(UnityDensity))

    override fun onAttach() {
        updateDefaultFlingBehavior()
        observeReads { currentValueOf(LocalDensity) } // Density 변경 감시
    }

    override fun onObservedReadsChanged() {
        // Density가 변경되면 기본 플링 동작을 업데이트
        updateDefaultFlingBehavior()
    }

    private fun updateDefaultFlingBehavior() {
        val density = currentValueOf(LocalDensity)
        defaultFlingBehavior.flingDecay = splineBasedDecay(density)
    }
}

 

이 예제에서:

  • observeReads { ... } 블록은 현재 노드에서 참조하는 Compose 상태(Density 등)를 추적합니다.
  • 이후 해당 상태가 변경되면 onObservedReadsChanged()가 호출되어 Modifier 동작을 자동으로 업데이트할 수 있습니다.

위임을 통한 Modifier 간 상태 공유

Modifier.Node는 다른 노드에 작업을 위임(delegate) 할 수 있습니다.
이는 다양한 상황에서 유용한데, 예를 들어 여러 Modifier 간에 공통 구현을 추출하거나, 또는 Modifier 간에 상태를 공유하는 데 사용할 수 있습니다.

예를 들어, 클릭 가능한 Modifier를 구현하면서 interactionData라는 공통 상태를 다른 Modifier 노드들과 공유하는 예는 다음과 같습니다:

class ClickableNode : DelegatingNode() {
    val interactionData = InteractionData()

    val focusableNode = delegate(
        FocusableNode(interactionData)
    )

    val indicationNode = delegate(
        IndicationNode(interactionData)
    )
}

 

이 예제에서는 interactionData를 중심으로 여러 노드가 협력하며 동작하게 됩니다.

 

노드 자동 invalidation 비활성화하기

Modifier.Node는 기본적으로 해당 노드에 대응되는 ModifierNodeElement에서 update가 호출될 때 invalidate 됩니다.

하지만 더 복잡한 Modifier를 구현할 경우, invalidate 시점을 세밀하게 제어하고 싶을 수 있습니다.
이럴 땐 자동 invalidate를 비활성화(opt-out)할 수 있습니다.

 

이 기능은 특히 하나의 Modifier가 레이아웃과 드로잉 모두를 제어할 경우 유용합니다.
예를 들어 color 같은 그리기 관련 속성이 바뀔 때는 draw만 invalidate 하고, size가 바뀔 때만 layout을 invalidate 하면, 불필요한 invalidate 를 방지하여 Modifier의 성능을 향상시킬 수 있습니다.

 

아래는 color, size, onClick 속성을 가진 가상의 Modifier 예제입니다. 이 Modifier는 필요한 경우에만 무효화를 수행하고, 그 외에는 무효화를 생략하여 성능을 최적화합니다:

class SampleInvalidatingNode(
    var color: Color,
    var size: IntSize,
    var onClick: () -> Unit
) : DelegatingNode(), LayoutModifierNode, DrawModifierNode {

    override val shouldAutoInvalidate: Boolean
        get() = false // 자동 무효화 비활성화

    private val clickableNode = delegate(
        ClickablePointerInputNode(onClick)
    )

    fun update(color: Color, size: IntSize, onClick: () -> Unit) {
        if (this.color != color) {
            this.color = color
            // 색상 변경 시 draw만 무효화
            invalidateDraw()
        }

        if (this.size != size) {
            this.size = size
            // 크기 변경 시 layout만 무효화
            invalidateMeasurement()
        }

        // onClick만 변경된 경우는 무효화하지 않음
        clickableNode.update(onClick)
    }

    override fun ContentDrawScope.draw() {
        drawRect(color)
    }

    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult {
        val size = constraints.constrain(size)
        val placeable = measurable.measure(constraints)
        return layout(size.width, size.height) {
            placeable.place(0, 0)
        }
    }
}

 

 

 

 

끝.


참고자료

https://developer.android.com/develop/ui/compose/custom-modifiers?_gl=1*1lpxgb1*_up*MQ..*_ga*MjAzODEzMTUxOS4xNzQ3NjQ0NTU3*_ga_6HH9YJMN9M*czE3NDc2NDkwMTkkbzIkZzAkdDE3NDc2NDkwMTkkajAkbDAkaDE3MDA5Nzk1NDEkZEFOYW9aS1JfR0VNd3p2bjMyN0IyX0s2clJTckFoUVdISnc.#common-situations

 

맞춤 수정자 만들기  |  Jetpack Compose  |  Android Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. 맞춤 수정자 만들기 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Compose는 일반적인 동작을 위한 여

developer.android.com

'Android > Compose' 카테고리의 다른 글

[Android] Compose의 Flow Layout  (0) 2025.05.31
[Android] Compose에서의 Pager  (0) 2025.05.31
[Android] Compose의 Modifier 제약 조건  (0) 2025.05.19
[Android] Compose의 Modifiers 정리  (0) 2025.05.17
[Android] Compose의 Layout 기초  (0) 2025.05.16