Jetpack Compose의 정렬선(Alignment lines)
Compose의 레이아웃 모델은 AlignmentLine을 사용하여 사용자 정의 정렬선을 만들 수 있도록 지원합니다. 이러한 정렬선은 부모 레이아웃이 자식들을 정렬하거나 배치할 때 사용할 수 있습니다. 예를 들어, Row는 자식들이 제공하는 커스텀 정렬선을 이용해 자식들을 정렬할 수 있습니다.
특정 AlignmentLine에 대한 값을 레이아웃이 제공하면, 부모는 측정 후 해당 자식의 Placeable 인스턴스를 통해 정렬선 위치 값을 읽을 수 있습니다 (Placeable.get 연산자 사용). 부모는 이렇게 얻은 정렬선의 위치를 기반으로 자식의 배치 위치를 결정할 수 있습니다.
일부 Compose 컴포저블은 이미 기본적으로 정렬선을 제공합니다. 예를 들어, BasicText 컴포저블은 FirstBaseline과 LastBaseline이라는 정렬선을 노출하고 있죠.
아래 예시에서는 firstBaselineToTop이라는 커스텀 LayoutModifier를 통해 Text의 첫 번째 베이스라인(FirstBaseline) 값을 읽어, 해당 베이스라인부터 위쪽으로 패딩을 적용하는 방식이 구현되어 있습니다.

fun Modifier.firstBaselineToTop(
firstBaselineToTop: Dp,
) = layout { measurable, constraints ->
// 컴포저블을 측정
val placeable = measurable.measure(constraints)
// 컴포저블이 FirstBaseline 값을 가지고 있는지 확인
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)
}
}
@Preview
@Composable
private fun TextWithPaddingToBaseline() {
MaterialTheme {
Text("Hi there!", Modifier.firstBaselineToTop(32.dp))
}
}
예제에서 FirstBaseline을 읽기 위해, 측정 단계에서 placeable[FirstBaseline]이 사용됩니다.
단, "firstBaselineToTop은 학습용으로 만든 예제일 뿐이고, 실제로는 Compose에 이미 그런 기능이 내장된 paddingFrom()이라는 Modifier가 있습니다.
Modifier.paddingFrom(alignmentLine = FirstBaseline, before = 32.dp)
즉, 실제 앱에서는 굳이 firstBaselineToTop() 같은 Modifier를 직접 만들 필요 없이, 처럼 paddingFrom() 을 사용해서 정렬선 기준 패딩이 가능합니다.
커스텀 정렬선 만들기
커스텀 Layout 컴포저블이나 LayoutModifier를 만들 때, 커스텀 정렬선(alignment line)을 제공할 수 있으며, 이를 통해 부모 컴포저블이 자식들을 정렬하거나 배치하는 데 사용할 수 있습니다.
아래 예시는 MaxChartValue와 MinChartValue라는 두 개의 커스텀 정렬선을 노출하는 BarChart 컴포저블을 보여줍니다. 이 정렬선을 기준으로 Max와 Min이라는 두 개의 텍스트 요소가 각각 차트의 최대값과 최소값 중심에 정렬되어 있습니다.

정렬선은 말 그대로 컴포저블 내부에 숨겨진 '눈에 보이지 않는 기준선'입니다. 부모 레이아웃은 이 정렬선을 참고해 자식들을 정렬합니다.
예를 들어 막대 그래프인 BarChart를 만들고, 그래프의 최댓값 위치와 최솟값 위치를 다른 컴포저블들과 정렬하고 싶다면, 그 위치에 정렬선을 만들어야 합니다. 그래야 Text("MAX"), Text("MIN") 같은 텍스트를 막대그래프의 위쪽과 아래쪽에 딱 맞게 배치할 수 있습니다.
이런 경우에는 Compose에서 제공하는 HorizontalAlignmentLine을 사용해서 정렬선을 만들 수 있습니다. 이름은 자유롭게 정할 수 있고, 예제에서는 아래와 같이 정의합니다:
* AlignmentLine defined by the maximum data value in a [BarChart]
*/
private val MaxChartValue = HorizontalAlignmentLine(merger = { old, new ->
min(old, new)
})
/**
* AlignmentLine defined by the minimum data value in a [BarChart]
*/
private val MinChartValue = HorizontalAlignmentLine(merger = { old, new ->
max(old, new)
})
예제에서 사용할 커스텀 정렬선은 HorizontalAlignmentLine 타입입니다. 이 정렬선은 자식들을 수직 방향으로 정렬할 때 사용되기 때문입니다.
여러 레이아웃이 같은 정렬선에 값을 제공하는 경우를 대비해, 병합 정책(merge policy)을 파라미터로 전달합니다.
Compose의 레이아웃 좌표계와 Canvas 좌표계는 [0, 0]이 왼쪽 위 모서리이며, x축과 y축은 아래 방향으로 양의 값을 가지므로, MaxChartValue의 값은 항상 MinChartValue보다 작게 됩니다.
따라서 병합 정책은 다음과 같습니다:
- 최대값(MaxChartValue) 정렬선: min (값들 중 가장 위쪽)
- 최소값(MinChartValue) 정렬선: max (값들 중 가장 아래쪽)
실제로 아래 예제와 같이 커스텀 Layout 또는 LayoutModifier를 만들 때는 MeasureScope.layout 메서드에서 alignmentLines: Map<AlignmentLine, Int> 파라미터를 통해 커스텀 정렬선을 지정할 수 있습니다.
@Composable
private fun BarChart(
dataPoints: List<Int>,
modifier: Modifier = Modifier,
) {
val maxValue: Float = remember(dataPoints) { dataPoints.maxOrNull()!! * 1.2f }
BoxWithConstraints(modifier = modifier) {
val density = LocalDensity.current
with(density) {
// ...
// Calculate baselines
val maxYBaseline = // ...
val minYBaseline = // ...
Layout(
content = {},
modifier = Modifier.drawBehind {
// ...
}
) { _, constraints ->
with(constraints) {
layout(
width = if (hasBoundedWidth) maxWidth else minWidth,
height = if (hasBoundedHeight) maxHeight else minHeight,
// Custom AlignmentLines are set here. These are propagated
// to direct and indirect parent composables.
alignmentLines = mapOf(
MinChartValue to minYBaseline.roundToInt(),
MaxChartValue to maxYBaseline.roundToInt()
)
) {}
}
}
}
}
}
이 컴포저블의 부모는 해당 컴포저블이 제공하는 정렬선(Alignment Line)을 사용할 수 있습니다.
아래 예시는 두 개의 Text 슬롯(maxText, minText)과 dataPoints를 인자로 받아, 막대 그래프의 최대값과 최소값 위치에 두 텍스트를 정렬하는 커스텀 레이아웃을 만드는 코드입니다.
@Composable
private fun BarChartMinMax(
dataPoints: List<Int>,
maxText: @Composable () -> Unit,
minText: @Composable () -> Unit,
modifier: Modifier = Modifier,
) {
Layout(
content = {
maxText()
minText()
// Set a fixed size to make the example easier to follow
BarChart(dataPoints, Modifier.size(200.dp))
},
modifier = modifier
) { measurables, constraints ->
check(measurables.size == 3)
val placeables = measurables.map {
it.measure(constraints.copy(minWidth = 0, minHeight = 0))
}
val maxTextPlaceable = placeables[0]
val minTextPlaceable = placeables[1]
val barChartPlaceable = placeables[2]
// Obtain the alignment lines from BarChart to position the Text
val minValueBaseline = barChartPlaceable[MinChartValue]
val maxValueBaseline = barChartPlaceable[MaxChartValue]
layout(constraints.maxWidth, constraints.maxHeight) {
maxTextPlaceable.placeRelative(
x = 0,
y = maxValueBaseline - (maxTextPlaceable.height / 2)
)
minTextPlaceable.placeRelative(
x = 0,
y = minValueBaseline - (minTextPlaceable.height / 2)
)
barChartPlaceable.placeRelative(
x = max(maxTextPlaceable.width, minTextPlaceable.width) + 20,
y = 0
)
}
}
}
@Preview
@Composable
private fun ChartDataPreview() {
MaterialTheme {
BarChartMinMax(
dataPoints = listOf(4, 24, 15),
maxText = { Text("Max") },
minText = { Text("Min") },
modifier = Modifier.padding(24.dp)
)
}
}
위의 코드를 하나 하나 분석해보겠습니다.
@Composable
private fun BarChartMinMax(
dataPoints: List<Int>,
maxText: @Composable () -> Unit,
minText: @Composable () -> Unit,
modifier: Modifier = Modifier,
)
이 함수는 maxText, minText 컴포저블 슬롯과 데이터 리스트를 받아, maxText()와 minText()를 각각 최대/최소값 정렬선에 정렬시킵니다. 내부에 BarChart도 포함되며, 이 BarChart가 정렬선을 제공합니다.
Layout(
content = {
maxText()
minText()
BarChart(dataPoints, Modifier.size(200.dp))
},
고정된 크기의 BarChart를 넣어 예제를 단순하게 만들었고, 총 3개의 컴포저블이 들어간다고 가정합니다.
val placeables = measurables.map {
it.measure(constraints.copy(minWidth = 0, minHeight = 0))
}
각각의 요소들을 측정할 때 최소 너비/높이를 0으로 지정해 자유롭게 배치할 수 있도록 합니다.
val minValueBaseline = barChartPlaceable[MinChartValue]
val maxValueBaseline = barChartPlaceable[MaxChartValue]
BarChart가 제공한 정렬선 값을 여기서 꺼내옵니다.
이 값은 y 좌표이며, Text("Max")와 Text("Min")을 이 위치에 배치하기 위한 기준이 됩니다.
layout(constraints.maxWidth, constraints.maxHeight) {
maxTextPlaceable.placeRelative(
x = 0,
y = maxValueBaseline - (maxTextPlaceable.height / 2)
)
minTextPlaceable.placeRelative(
x = 0,
y = minValueBaseline - (minTextPlaceable.height / 2)
)
Text("Max")는 MaxChartValue 기준선의 중앙에 오도록 배치되고, Text("Min")은 MinChartValue 기준선의 중앙에 오도록 배치됩니다.
barChartPlaceable.placeRelative(
x = max(maxTextPlaceable.width, minTextPlaceable.width) + 20,
y = 0
)
BarChart는 텍스트 옆에 충분한 간격(20dp)을 두고 오른쪽에 배치됩니다.
정렬 기준이 아직 정해지지 않았는데, 어떻게 먼저 호출되는 Text가 거기에 정렬될 수 있나요?
Layout(
content = {
maxText()
minText()
// Set a fixed size to make the example easier to follow
BarChart(dataPoints, Modifier.size(200.dp))
},
modifier = modifier
){ .. }
그 이유는 Compose는 “그리기 전에 모든 컴포저블을 먼저 측정”해서 정렬 기준을 계산한 뒤, 다시 실제 위치를 배치하기 때문이에요.
- 측정 (Measure)
각 컴포저블이 "나는 어느 정도 크기야", "내 정렬선은 여기야"라고 정보를 제공함
👉 이때 BarChart는 MaxChartValue, MinChartValue 정렬선을 Map에 담아서 보고함 - 정렬선 수집 (Alignment Line Propagation)
부모(Column, Row)는 모든 자식들이 제공한 정렬선 정보를 수집 - 배치 (Placement)정렬선을 기준으로 자식들(Text("Max"), BarChart, Text("Min"))을 배치함
👉 alignBy(MaxChartValue)에 따라 Text("Max")를 정렬선 위치에 맞게 배치
자식 컴포저블을 measure할 때 minWidth = 0, minHeight = 0을 설정해서 왜 측정하나요?
결론부터 말하면, 자식의 내용만큼만 공간을 차지하도록 하고 싶기 때문입니다.
그래서 강제로 minWidth/minHeight를 0으로 설정하는 거예요.
설정 여부에 따라 아래와 같은 차이가 발생합니다.
minWidth = 0 없이 그냥 부모의 constraints를 사용하여 측정한 경우
val placeables = measurables.map {
it.measure(constraints) // 부모로부터 받은 제약 그대로 사용
}
- 부모가 minWidth = 100dp라는 제약을 줬다고 가정하면, "Max" 텍스트도 최소 너비 100dp 이상으로 측정됨.
- 그럼 실제로 텍스트 내용이 작아도 공간을 크게 차지함 → 불필요한 여백 발생
- 배치 예시: [ Max ][ BarChart ]
↑ 실제 글자보다 여백이 많음
반면, minWidth = 0으로 제약을 풀어준 경우
val placeables = measurables.map {
it.measure(constraints.copy(minWidth = 0, minHeight = 0))
}
- "Max" 텍스트는 글자 크기만큼만 딱 측정됨.
- BarChart는 텍스트가 차지한 만큼만 오른쪽으로 밀려 배치됨.
- 배치 예시: [Max][ BarChart ]
↑ 딱 글자 크기만큼만 공간 차지
끝.
참고자료
Jetpack Compose의 정렬 선 | Android Developers
이 페이지는 Cloud Translation API를 통해 번역되었습니다. Jetpack Compose의 정렬 선 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Compose 레이아웃 모델을 사용하
developer.android.com
'Android > Compose' 카테고리의 다른 글
[Android] Compose의 ConstraintLayout (1) | 2025.06.05 |
---|---|
[Android] Compose 레이아웃에서 Intrinsic 측정 (0) | 2025.06.02 |
[Android] Compose는 왜 View System과 달리 측정을 한번만 할까? (1) | 2025.06.01 |
[Android] Compose의 커스텀 레이아웃 (0) | 2025.06.01 |
[Android] Compose의 Flow Layout (0) | 2025.05.31 |