커스텀 레이아웃
Compose에서 UI 요소는 컴포저블 함수(composable function)로 표현되며, 이 함수가 호출되면 화면에 렌더링되는 UI 트리의 일부를 생성합니다. 각 UI 요소는 하나의 부모를 가지며, 여러 개의 자식 요소를 가질 수 있습니다. 또한, 각 요소는 부모 내에서 (x, y) 위치와 너비 및 높이로 구성된 크기 정보를 가집니다.
부모 요소는 자식 요소에 대해 제약 조건(constraints)을 정의합니다. 자식 요소는 이 제약 조건 내에서 자신의 크기를 결정해야 하며, 제약 조건은 요소의 최소 및 최대 너비와 높이를 제한하는 역할을 합니다. 만약 어떤 요소가 자식 요소들을 가지고 있다면, 자신의 크기를 결정하기 위해 자식 요소들을 측정할 수도 있습니다.
그리고 요소가 자신의 크기를 결정하고 보고한 이후에는, 자식 요소들을 자신을 기준으로 어떻게 배치할지 정의할 수 있는 기회가 주어집니다.
이 내용은 커스텀 레이아웃 만들기 문서에서 자세히 설명되어 있습니다.
UI 트리의 각 노드를 배치하는 과정은 세 단계로 이루어집니다. 각 노드는 다음을 수행해야 합니다:
- 자식 요소들을 측정한다
- 자신의 크기를 결정한다
- 자식 요소들을 배치한다

참고: Compose UI는 다중 측정(multi-pass measurement)을 허용하지 않습니다. 이는 레이아웃 요소가 자식 요소를 서로 다른 측정 구성을 시도하기 위해 두 번 이상 측정할 수 없다는 것을 의미합니다. 즉, 자식 하나당 measure()는 정확히 한 번만 호출 가능합니다.
스코프(scope)의 사용은 자식 요소를 언제 측정하고 배치할 수 있는지를 정의합니다.
해당 스코프는 Layout()을 선언하면 자동으로 주어지게 됩니다.
측정은 측정 및 레이아웃 단계에서만 수행할 수 있으며, 자식 요소는 측정이 완료된 이후, 레이아웃 단계에서만 배치할 수 있습니다.
MeasureScope, PlacementScope와 같은 Compose의 스코프 덕분에, 이러한 제약은 컴파일 시점에 강제됩니다.
즉, Jetpack Compose에서는 MeasureScope와 PlacementScope 같은 특정 역할의 스코프(scope) 안에서만
측정(measure())과 배치(place()) 같은 작업을 할 수 있게 만들어져 있습니다.
올바른 코드
Layout(
content = { ... }
) { measurables, constraints ->
val placeable = measurables[0].measure(constraints) // ✅ MeasureScope 안이라서 OK
layout(width, height) {
placeable.place(0, 0) // ✅ PlacementScope 안이라서 OK
}
}
컴파일 에러 발생 코드
Layout(
content = { ... }
) { measurables, constraints ->
val placeable = measurables[0].measure(constraints)
placeable.place(0, 0) // ❌ PlacementScope 바깥이므로 컴파일 에러
}
layout 수정자 사용하기
layout 수정자를 사용하면 요소가 측정되고 배치되는 방식을 수정할 수 있습니다.
layout은 람다(lambda)이며, 이 람다의 매개변수로는 측정할 수 있는 요소인 measurable과, 해당 컴포저블에 주어진 제약 조건(constraints)이 포함됩니다.
사용자 정의 레이아웃 수정자는 다음과 같이 작성될 수 있습니다:
fun Modifier.customLayoutModifier() =
layout { measurable, constraints ->
// ...
}
화면에 Text를 표시하고, 텍스트 첫 번째 줄의 기준선(baseline)으로부터 위쪽 거리를 제어해 보겠습니다.
이것이 바로 paddingFromBaseline 수정자가 수행하는 작업이며, 여기서는 그 동작을 예시로 직접 구현해보는 것입니다.
이를 위해 layout 수정자를 사용하여 컴포저블을 수동으로 화면에 배치합니다.
아래는 Text의 상단 패딩을 24.dp로 설정했을 때의 원하는 동작입니다.

다음은 해당 위 내용 구현하는 코드입니다:
fun Modifier.firstBaselineToTop(
firstBaselineToTop: Dp
) = layout { measurable, constraints ->
// 컴포저블 측정
val placeable = measurable.measure(constraints)
// 컴포저블에 첫 번째 기준선이 있는지 확인
check(placeable[FirstBaseline] != AlignmentLine.Unspecified)
val firstBaseline = placeable[FirstBaseline]
// 기준선부터 위쪽 패딩까지의 거리 계산
val placeableY = firstBaselineToTop.roundToPx() - firstBaseline
val height = placeable.height + placeableY
layout(placeable.width, height) {
// 컴포저블이 배치되는 위치 지정
placeable.placeRelative(0, placeableY)
}
}
해당 코드에서 이루어지는 동작은 다음과 같습니다:
- measurable 람다 파라미터에서는, measurable.measure(constraints)를 호출하여 measurable로 표현된 Text를 측정합니다.
- layout(width, height) 메서드를 호출하여 컴포저블의 크기를 지정하고, 이 메서드는 또한 람다를 통해 내부 요소들을 배치하는 방법을 정의합니다. 이 경우, 높이는 마지막 기준선(baseline)과 추가된 상단 패딩 사이의 거리입니다.
- placeable.place(x, y)를 호출하여 내부 요소들을 화면에 배치합니다. 배치하지 않으면 해당 요소는 화면에 표시되지 않습니다.
여기서 y 위치는 상단 패딩 - 텍스트의 첫 번째 기준선 위치에 해당합니다.
이 동작이 제대로 작동하는지 확인하기 위해, 해당 modifier를 Text에 적용해보겠습니다.
@Preview
@Composable
fun TextWithPaddingToBaselinePreview() {
MyApplicationTheme {
Text("Hi there!", Modifier.firstBaselineToTop(32.dp))
}
}
@Preview
@Composable
fun TextWithNormalPaddingPreview() {
MyApplicationTheme {
Text("Hi there!", Modifier.padding(top = 32.dp))
}
}

커스텀 레이아웃 만들기
layout 수정자는 자신이 호출된 컴포저블만 변경할 수 있습니다.
여러 개의 컴포저블을 측정하고 배치하려면 Layout 컴포저블을 사용해야 합니다.
이 컴포저블을 사용하면 자식 요소들을 수동으로 측정하고 배치할 수 있으며,
Column이나 Row와 같은 상위 수준 레이아웃 컴포저블들 역시 모두 Layout을 기반으로 만들어졌습니다.
참고: 기존 View 시스템에서는 커스텀 레이아웃을 만들기 위해 ViewGroup을 상하고 measure 및 layout 함수를 구현해야 했습니다.
하지만 Compose에서는 Layout 컴포저블을 사용하여 단순히 함수를 작성하기만 하면 됩니다.
이제 아주 기본적인 버전의 Column을 직접 만들어보겠습니다.
대부분의 커스텀 레이아웃은 다음과 같은 패턴을 따릅니다:
@Composable
fun MyBasicColumn(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
// measure and position children given constraints logic here
// ...
}
}
layout 수정자와 마찬가지로, measurables는 측정이 필요한 자식 요소들의 목록이고, constraints는 부모로부터 전달된 제약 조건입니다.
앞서 설명한 논리와 동일하게, MyBasicColumn은 다음과 같이 구현할 수 있습니다:
@Composable
fun MyBasicColumn(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
// 자식 뷰에 추가 제약을 가하지 않고, 주어진 constraints로 측정
// 측정된 자식들의 리스트
val placeables = measurables.map { measurable ->
// 각 자식 요소를 측정
measurable.measure(constraints)
}
// 레이아웃의 크기를 가능한 한 크게 설정
layout(constraints.maxWidth, constraints.maxHeight) {
// 자식이 배치된 y 좌표를 추적
var yPosition = 0
// 자식들을 부모 레이아웃에 배치
placeables.forEach { placeable ->
// 아이템을 화면에 배치
placeable.placeRelative(x = 0, y = yPosition)
// 다음 아이템이 배치될 y 좌표 업데이트
yPosition += placeable.height
}
}
}
}
자식 컴포저블들은 Layout의 제약 조건에 따라 제한되며, 이전 컴포저블의 yPosition 값을 기준으로 배치됩니다.
아래는 해당 커스텀 컴포저블을 사용하는 방법입니다:
@Composable
fun CallingComposable(modifier: Modifier = Modifier) {
MyBasicColumn(modifier.padding(8.dp)) {
Text("MyBasicColumn")
Text("places items")
Text("vertically.")
Text("We've done it by hand!")
}
}

레이아웃 방향 (Layout direction)
컴포저블의 레이아웃 방향을 변경하려면 LocalLayoutDirection CompositionLocal을 변경하면 됩니다.
- LayoutDirection.Ltr: 왼쪽 → 오른쪽 (기본값)
- LayoutDirection.Rtl: 오른쪽 → 왼쪽
컴포저블을 화면에 수동으로 배치할 때는, layout 수정자나 Layout 컴포저블의 LayoutScope 내에서 LayoutDirection이 사용됩니다.
단, layoutDirection을 사용할 때는 placeRelative 대신 place 메서드를 사용하여 컴포저블을 배치하세요.
place는 레이아웃 방향(왼쪽에서 오른쪽 또는 오른쪽에서 왼쪽)에 따라 동작이 변하지 않습니다.
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
Row(modifier = Modifier.fillMaxWidth()) {
Text("오른쪽에서 시작됨")
Text("두 번째 아이템")
}
}
위 코드처럼 LocalLayoutDirection을 Rtl로 설정하면, Row 내부 아이템들이 오른쪽에서 왼쪽으로 배치됩니다.
끝.
참고자료
맞춤 레이아웃 | Jetpack Compose | Android Developers
이 페이지는 Cloud Translation API를 통해 번역되었습니다. 맞춤 레이아웃 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Compose에서 UI 요소는 호출될 때 UI 요소
developer.android.com
'Android > Compose' 카테고리의 다른 글
[Android] Compose의 정렬선(Alignment lines) (1) | 2025.06.02 |
---|---|
[Android] Compose는 왜 View System과 달리 측정을 한번만 할까? (1) | 2025.06.01 |
[Android] Compose의 Flow Layout (0) | 2025.05.31 |
[Android] Compose에서의 Pager (0) | 2025.05.31 |
[Android] Compose에서 Modifier 커스텀하기 (0) | 2025.05.19 |