Compose 수정자

수정자를 사용하면 컴포저블을 장식하거나 강화할 수 있습니다. 수정자를 통해 다음과 같은 종류의 작업을 실행할 수 있습니다.

  • 컴포저블의 크기, 레이아웃, 동작 및 모양 변경
  • 접근성 라벨과 같은 정보 추가
  • 사용자 입력 처리
  • 요소를 클릭 가능, 스크롤 가능, 드래그 가능 또는 확대/축소 가능하게 만드는 높은 수준의 상호작용 추가

수정자는 표준 Kotlin 객체입니다. Modifier 클래스 함수 중 하나를 호출하여 수정자를 만듭니다.

@Composable
private fun Greeting(name: String) {
    Column(modifier = Modifier.padding(24.dp)) {
        Text(text = "Hello,")
        Text(text = name)
    }
}

컬러 배경에 텍스트 두 줄이 있고 텍스트 주위에 패딩이 있습니다.

다음과 같이 이러한 함수를 함께 연결하여 구성할 수 있습니다.

@Composable
private fun Greeting(name: String) {
    Column(
        modifier = Modifier
            .padding(24.dp)
            .fillMaxWidth()
    ) {
        Text(text = "Hello,")
        Text(text = name)
    }
}

이제 텍스트 뒤의 컬러 배경이 기기의 전체 너비로 확장합니다.

위의 코드에서 다양한 수정자 함수가 함께 사용된 것을 확인할 수 있습니다.

  • padding: 요소 주위에 공간을 배치합니다.
  • fillMaxWidth: 컴포저블이 상위 요소로부터 부여받은 최대 너비를 채우도록 합니다.

모든 컴포저블이 modifier 매개변수를 허용하고 UI를 내보내는 첫 번째 하위 요소에 이 수정자를 전달하는 것이 좋습니다. 이렇게 하면 코드의 재사용 가능성이 커지며 코드 동작이 더 예측 가능해지고 이해하기 쉬워집니다. 자세한 내용은 Compose API 가이드라인, 수정자 매개변수를 허용 및 준수하는 요소를 참고하세요.

수정자의 순서가 중요

수정자 함수의 순서는 중요합니다. 각 함수는 이전 함수에서 반환한 Modifier를 변경하므로 순서는 최종 결과에 영향을 줍니다. 다음 예를 살펴보겠습니다.

@Composable
fun ArtistCard(/*...*/) {
    val padding = 16.dp
    Column(
        Modifier
            .clickable(onClick = onClick)
            .padding(padding)
            .fillMaxWidth()
    ) {
        // rest of the implementation
    }
}

가장자리 주변의 패딩을 포함한 영역 전체가 클릭에 반응함

위의 코드에서는 padding 수정자가 clickable 수정자 뒤에 적용되었기 때문에 주변 패딩을 포함하여 전체 영역을 클릭할 수 있습니다. 수정자 순서가 뒤집히면 다음과 같이 padding으로 추가된 공간은 사용자 입력에 반응하지 않습니다.

@Composable
fun ArtistCard(/*...*/) {
    val padding = 16.dp
    Column(
        Modifier
            .padding(padding)
            .clickable(onClick = onClick)
            .fillMaxWidth()
    ) {
        // rest of the implementation
    }
}

레이아웃 가장자리 주변의 패딩이 더 이상 클릭에 반응하지 않음

내장 수정자

Jetpack Compose는 컴포저블을 장식하거나 강화하는 데 도움이 되는 내장 수정자 목록을 제공합니다. 다음은 레이아웃을 조정하는 데 사용하는 몇 가지 일반적인 수정자입니다.

paddingsize

기본적으로 Compose에서 제공되는 레이아웃은 하위 요소를 래핑합니다. 하지만 size 수정자를 사용하여 크기를 설정할 수 있습니다.

@Composable
fun ArtistCard(/*...*/) {
    Row(
        modifier = Modifier.size(width = 400.dp, height = 100.dp)
    ) {
        Image(/*...*/)
        Column { /*...*/ }
    }
}

지정한 크기가 레이아웃의 상위 요소에서 수신된 제약 조건을 충족하지 않는 경우 적용되지 않을 수 있습니다. 수신된 제약 조건과 관계없이 컴포저블의 크기를 고정해야 하는 경우 requiredSize 수정자를 사용하세요.

@Composable
fun ArtistCard(/*...*/) {
    Row(
        modifier = Modifier.size(width = 400.dp, height = 100.dp)
    ) {
        Image(
            /*...*/
            modifier = Modifier.requiredSize(150.dp)
        )
        Column { /*...*/ }
    }
}

하위 이미지가 상위 이미지의 제약 조건보다 큼

이 예에서는 상위 요소 height100.dp로 설정되더라도 Image 높이는 150.dp가 됩니다. requiredSize 수정자가 우선하기 때문입니다.

하위 레이아웃이 상위 요소에 의해 허용된 모든 가용 높이를 채우도록 하려면 fillMaxHeight 수정자를 추가합니다(Compose는 fillMaxSizefillMaxWidth도 제공함).

@Composable
fun ArtistCard(/*...*/) {
    Row(
        modifier = Modifier.size(width = 400.dp, height = 100.dp)
    ) {
        Image(
            /*...*/
            modifier = Modifier.fillMaxHeight()
        )
        Column { /*...*/ }
    }
}

이미지 높이가 상위 요소만큼 큼

요소 주위에 패딩을 추가하려면 padding 수정자를 설정합니다.

레이아웃 상단에서 기준선까지 특정 거리가 유지되도록 텍스트 기준선 위에 패딩을 추가하려면 paddingFromBaseline 수정자를 사용합니다.

@Composable
fun ArtistCard(artist: Artist) {
    Row(/*...*/) {
        Column {
            Text(
                text = artist.name,
                modifier = Modifier.paddingFromBaseline(top = 50.dp)
            )
            Text(artist.lastSeenOnline)
        }
    }
}

위에 패딩이 있는 텍스트

오프셋

원래 위치를 기준으로 레이아웃을 배치하려면 offset 수정자를 추가하고 x축 및 y축에 오프셋을 설정합니다. 오프셋은 양수일 수도 있고 양수가 아닐 수도 있습니다. paddingoffset의 차이점은 컴포저블에 offset을 추가해도 측정값이 변경되지 않는다는 것입니다.

@Composable
fun ArtistCard(artist: Artist) {
    Row(/*...*/) {
        Column {
            Text(artist.name)
            Text(
                text = artist.lastSeenOnline,
                modifier = Modifier.offset(x = 4.dp)
            )
        }
    }
}

텍스트가 상위 컨테이너의 오른쪽으로 이동함

offset 수정자는 레이아웃 방향에 따라 가로로 적용됩니다. 왼쪽에서 오른쪽으로 컨텍스트에서 양수 offset은 요소를 오른쪽으로 이동하고 오른쪽에서 왼쪽으로 컨텍스트에서는 요소를 오른쪽으로 이동합니다. 레이아웃 방향을 고려하지 않고 오프셋을 설정해야 하는 경우 양의 오프셋 값이 항상 요소를 오른쪽으로 이동시키는 absoluteOffset 수정자를 확인하세요.

offset 수정자는 오버로드 두 개를 제공합니다. 오프셋을 매개변수로 사용하는 offset과 람다를 사용하는 offset입니다. 각각을 사용하는 시기와 성능을 위해 이를 최적화하는 방법에 관한 자세한 내용은 Compose 성능 - 최대한 읽기 연기 섹션을 참고하세요.

Compose의 범위 안전성

Compose에는 특정 컴포저블의 하위 요소에 적용될 때만 사용할 수 있는 수정자가 있습니다. Compose는 맞춤 범위를 통해 이를 적용합니다.

예를 들어 하위 요소를 Box 크기에 영향을 미치지 않고 상위 Box만큼 크게 만들려면 matchParentSize 수정자를 사용합니다. matchParentSizeBoxScope에서만 사용할 수 있습니다. 따라서 Box 상위 요소 내 하위 요소에만 사용할 수 있습니다.

범위 안전성을 사용하면 다른 컴포저블 및 범위에서 작동하지 않는 수정자를 추가할 수 없으며 시행착오로부터 시간을 절약할 수 있습니다.

범위 지정 수정자는 상위 요소가 하위 요소에 관해 알아야 하는 정보를 상위 요소에 알립니다. 일반적으로 상위 데이터 수정자라고도 합니다. 내부 요소는 범용 수정자와 다르지만 사용 관점에서 이러한 차이는 중요하지 않습니다.

Box 효과의 matchParentSize

위에서 언급했듯이 하위 레이아웃이 Box 크기에 영향을 미치지 않고 상위 Box와 크기가 같이지도록 하려면 matchParentSize 수정자를 사용하세요.

matchParentSizeBox 범위 내에서만 사용할 수 있습니다. 즉 Box 컴포저블의 직접 하위 요소에만 적용됩니다.

아래 예에서 하위 Spacer는 상위 Box에서 크기를 가져오고 결과적으로 가장 큰 하위 요소(이 경우에는 ArtistCard)에서 크기를 가져옵니다.

@Composable
fun MatchParentSizeComposable() {
    Box {
        Spacer(
            Modifier
                .matchParentSize()
                .background(Color.LightGray)
        )
        ArtistCard()
    }
}

컨테이너를 채우는 회색 배경

matchParentSize 대신 fillMaxSize가 사용된 경우 Spacer는 허용된 모든 가용 공간을 상위 요소로 가져온 다음 상위 요소에서 모든 가용 공간을 확장하고 채웁니다.

화면을 채우는 회색 배경

RowColumnweight

이전 패딩 및 크기 섹션에서 확인했듯이 기본적으로 컴포저블 크기는 컴포저블이 래핑하는 콘텐츠로 정의됩니다. RowScopeColumnScope에서만 사용할 수 있는 weight 수정자를 사용하여 컴포저블 크기를 상위 요소 내에서 유연하게 설정할 수 있습니다.

Box 컴포저블이 포함된 Row를 사용해 보겠습니다. 첫 번째 상자의 weight가 두 번째 상자의 두 배로 지정되므로 너비가 두 배로 지정됩니다. Row의 너비가 210.dp이므로 첫 번째 Box의 너비는 140.dp이고 두 번째는70.dp입니다.

@Composable
fun ArtistCard(/*...*/) {
    Row(
        modifier = Modifier.fillMaxWidth()
    ) {
        Image(
            /*...*/
            modifier = Modifier.weight(2f)
        )
        Column(
            modifier = Modifier.weight(1f)
        ) {
            /*...*/
        }
    }
}

이미지 너비가 텍스트 너비의 두 배임

수정자 추출 및 재사용

여러 수정자를 함께 체이닝하여 컴포저블을 장식하거나 강화할 수 있습니다. 이 체인은 단일 Modifier.Elements의 순서가 지정된 변경 불가능한 목록을 나타내는 Modifier 인터페이스를 통해 생성됩니다.

Modifier.Element는 레이아웃, 그리기 및 그래픽 동작, 모든 동작 관련, 포커스 및 시맨틱 동작 등의 개별 동작과 기기 입력 이벤트를 나타냅니다. 순서가 중요합니다. 먼저 추가된 수정자 요소가 먼저 적용됩니다.

변수로 추출하고 더 높은 범위로 끌어올리는 방식으로 동일한 수정자 체인 인스턴스를 여러 컴포저블에서 재사용하는 것이 유용할 수도 있습니다. 이는 다음과 같은 이유로 코드 가독성을 개선하거나 앱의 성능 개선에 도움이 될 수 있습니다.

  • 수정자를 사용하는 컴포저블에 리컴포지션이 발생할 때 수정자의 재할당이 반복되지 않습니다.
  • 수정자 체인은 매우 길고 복잡할 수 있으므로 동일한 체인 인스턴스를 재사용하면 Compose 런타임이 이를 비교할 때 해야 하는 워크로드를 줄일 수 있습니다.
  • 이러한 추출을 통해 코드베이스 전체에서 코드 청결도, 일관성, 유지관리성이 향상됩니다.

수정자 재사용 권장사항

자체 Modifier 체인을 만들고 추출하여 여러 컴포저블 구성요소에서 재사용합니다. 수정자는 데이터와 같은 객체이므로 저장해도 괜찮습니다.

val reusableModifier = Modifier
    .fillMaxWidth()
    .background(Color.Red)
    .padding(12.dp)

자주 변경되는 상태를 관찰할 때 수정자 추출 및 재사용

애니메이션 상태나 scrollState처럼 컴포저블 내에서 자주 변경되는 상태를 관찰할 때 리컴포지션이 상당히 많이 발생할 수 있습니다. 이 경우 모든 리컴포지션 및 잠재적으로 모든 프레임에 수정자가 할당됩니다.

@Composable
fun LoadingWheelAnimation() {
    val animatedState = animateFloatAsState(/*...*/)

    LoadingWheel(
        // Creation and allocation of this modifier will happen on every frame of the animation!
        modifier = Modifier
            .padding(12.dp)
            .background(Color.Gray),
        animatedState = animatedState
    )
}

대신 다음과 같이 수정자의 동일한 인스턴스를 생성, 추출, 재사용한 후 컴포저블에 전달할 수 있습니다.

// Now, the allocation of the modifier happens here:
val reusableModifier = Modifier
    .padding(12.dp)
    .background(Color.Gray)

@Composable
fun LoadingWheelAnimation() {
    val animatedState = animateFloatAsState(/*...*/)

    LoadingWheel(
        // No allocation, as we're just reusing the same instance
        modifier = reusableModifier,
        animatedState = animatedState
    )
}

범위가 지정되지 않은 수정자 추출 및 재사용

수정자는 범위가 지정되지 않거나 특정 컴포저블로 범위가 지정될 수 있습니다. 범위가 지정되지 않은 수정자의 경우 컴포저블 외부에서 수정자를 간단한 변수로 쉽게 추출할 수 있습니다.

val reusableModifier = Modifier
    .fillMaxWidth()
    .background(Color.Red)
    .padding(12.dp)

@Composable
fun AuthorField() {
    HeaderText(
        // ...
        modifier = reusableModifier
    )
    SubtitleText(
        // ...
        modifier = reusableModifier
    )
}

이는 Lazy 레이아웃과 함께 사용하면 특히 유용합니다. 대부분의 경우 잠재적으로 중요한 항목에 모두 정확히 동일한 수정자를 사용하는 것이 좋습니다.

val reusableItemModifier = Modifier
    .padding(bottom = 12.dp)
    .size(216.dp)
    .clip(CircleShape)

@Composable
private fun AuthorList(authors: List<Author>) {
    LazyColumn {
        items(authors) {
            AsyncImage(
                // ...
                modifier = reusableItemModifier,
            )
        }
    }
}

범위 지정 수정자 추출 및 재사용

특정 컴포저블로 범위가 지정된 수정자를 처리할 때 가능한 한 가장 높은 수준으로 수정자를 추출하고 적절한 경우 재사용할 수 있습니다.

Column(/*...*/) {
    val reusableItemModifier = Modifier
        .padding(bottom = 12.dp)
        // Align Modifier.Element requires a ColumnScope
        .align(Alignment.CenterHorizontally)
        .weight(1f)
    Text1(
        modifier = reusableItemModifier,
        // ...
    )
    Text2(
        modifier = reusableItemModifier
        // ...
    )
    // ...
}

추출된 범위 지정 수정자는 동일한 범위의 직접 하위 요소에만 전달해야 합니다. 이것이 중요한 이유를 자세히 알아보려면 Compose의 범위 안전성 섹션을 참고하세요.

Column(modifier = Modifier.fillMaxWidth()) {
    // Weight modifier is scoped to the Column composable
    val reusableItemModifier = Modifier.weight(1f)

    // Weight will be properly assigned here since this Text is a direct child of Column
    Text1(
        modifier = reusableItemModifier
        // ...
    )

    Box {
        Text2(
            // Weight won't do anything here since the Text composable is not a direct child of Column
            modifier = reusableItemModifier
            // ...
        )
    }
}

추출된 수정자의 추가 체이닝

추출된 수정자 체인은 .then() 함수를 호출하여 더 체이닝하거나 추가할 수 있습니다.

val reusableModifier = Modifier
    .fillMaxWidth()
    .background(Color.Red)
    .padding(12.dp)

// Append to your reusableModifier
reusableModifier.clickable { /*...*/ }

// Append your reusableModifier
otherModifier.then(reusableModifier)

수정자 순서가 중요하다는 점을 유의해야 합니다.

자세히 알아보기

Google에서는 매개변수 및 범위가 포함된 수정자 전체 목록을 제공합니다.

수정자를 사용하는 방법에 관한 자세한 내용은 Compose의 기본 레이아웃 Codelab을 살펴보거나 Now in Android 저장소를 참고하세요.

맞춤 수정자와 이를 만드는 방법에 관한 자세한 내용은 맞춤 레이아웃 - 레이아웃 수정자 사용에 관한 문서를 참고하세요.