이전 스터디에서는 안드로이드 앱 화면의 UI 컴포넌트를 '동적'으로 구성하기 위한 컴포즈 Slot API 를 살펴봤다. Slot API 라는 이름때문에 뭔가 특별한것 같지만, 알고보면 컴포저블 함수를 함수 파라미터로 전달하는 방식이라고 생각하면 된다. 또한 여러가지 컴포저블의 공통된 속성(property)을 변경하기 위한 모디파이어(modifier)에 대해서도 간단히 살펴봤다. 모디파이어를 사용해서 컴포저블의 배경색상, 컴포저블의 상하좌우 크기, 패딩(padding) 등등을 제어할 수 있다. 마지막으로 특정 UI 컴포넌트를 사용자가 원하는 형태로 배치하기 위한 Row/Column 컴포저블에 대해서 알아봤다.
이번 포스팅에서는 UI 컴포넌트를 화면에 배치하는 추가적인 방법(Box 레이아웃, 커스텀 레이아웃)에 대해 알아본다.
- 지난 스터디 내용
- Ch22 컴포즈 Slot API
- Ch23 컴포즈 Slot API 튜토리얼
- Ch24 모디파이어 이용하기
- Ch25 Row/Column 이용해 레이아웃 구성하기
- 이번 스터디 내용
- Ch26 Box 레이아웃
- Ch27 커스텀 레이아웃 모디파이어
- Ch28 커스텀 레이아웃 구현하기
안드로이드 젯팩 컴포즈 스터디 (ch22~25)
젯팩 컴포즈 스터디를 이어가본다. 아래는 지난 젯팩 컴포즈 포스팅에서 다뤘던 내용과 이번에 다룰 내용 Chapter 정보이다.지난 스터디에서는 젯팩 컴포즈의 가장 기본적인 빌딩블럭인 '컴포저
sparkthenmove.tistory.com
Ch26 Box 레이아웃
Ch25 에서는 컴포넌트를 화면에 배치하기 위한 Row/Column 컴포저블에 대해 알아봤다. Ch26 에서는 또다른 레이아웃 유형인 Box 레이아웃에 대해 살펴본다.
Box 레이아웃은 자식 컴포저블을 위로 쌓아올린다. 위로 쌓아올린다는게 어떤 의미일까? 아래 그림을 보자. 검정색 테투리가 Box 레이아웃인데, 내부에 3개의 버튼을 번호순서대로 배치했다. 그러면 1번이 먼저 배치되고, 그 위에 2번, 3번이 순서대로 배치되면서 아래와 같은 모습이 된다. 이것이 Box 레이아웃의 동작방식이다. (3번이 1번보다 위에 배치된다. 이를 Stack 이라고 말하기도 한다)

Box 컴포저블은 여러가지 정렬(Alignment) 파라미터를 제공한다. 제공하는 정렬 파라미터 종류는 다음과 같다.
- Alignment.TopStart
- Alignment.TopCenter
- Alignment.TopEnd
- Alignment.CenterStart
- Alignment.Center
- Alignment.CenterEnd
- Alignment.BottomCenter
- Alignment.BottomEnd
- Alignment.BottomStart
각 정렬 파라미터 설정시 아래와 같이 배치된다.

BoxScope 모디파이어
지난 스터디에서 RowScope, ColumnScope 에 대해서 알아봤듯이, Box 레이아웃(컴포저블) 또한 마지막 람다함수가 BoxScope 에 해당한다. 따라서 BoxScope 에서는 BoxScope 모디파이어(Modifier)에서 제공하는 기능(속성)을 자식 컴포저블에 적용할 수 있다. (참고: XYZScope 모디파이어에 대한 내용은 블로그의 이전글을 참고한다)
Box 레이아웃에서는 BoxScope Modifier 를 자식 컴포저블에 적용가능하다. align() 함수를 사용하면, 자식 컴포저블을 원하는 위치에 배치할 수 있다. 아래 그림은 align() 함수를 사용하여 여러위치에 Text 컴포저블을 배치한 결과이다.

clip 모디파이어
clip 모디파이어를 사용하면 컴포저블의 모양을 변경할 수 있다. 아래는 Box 컴포넌트를 예시로 가능한 모양을 살펴본 것이다.
Box(Modifier.size(200.dp).clip(CircleShape).background(Color.Blue))
Box(Modifier.size(200.dp).clip(RoundedCornerShape(30.dp)).background(Color.Blue))
Box(Modifier.size(200.dp).clip(CutCornerShape(30.dp)).background(Color.Blue))
| clip(CircleShape) | clip(RoundedCornerShape(30.dp)) | clip(CurCornerShape(30.dp)) |
![]() |
![]() |
![]() |
나는 RoundedCornerShape 를 많이 쓰게 될듯... 내가 좋아하는 모양

Ch27 커스텀 레이아웃 모디파이어
앞에서 3가지 레이아웃 컴포저블에 대해서 살펴봤다. Row, Column, Box 를 사용하여 여러 UI 컴포넌트들을 원하는대로 배치할수 있지만, 실전에서는 좀더 복잡한? 레이아웃을 사용해야 하는 경우가 있다. Ch27 에서는 이럴때 사용하는 커스텀 레이아웃에 대해 살펴본다.
자식 컴포저블의 위치는 부모 컴포저블의 x, y 좌표를 기준으로 정해진다. 부모 컴포저블은 자식 컴포저블이 차지할수 있는 최대/최소 크기를 제한할 수 있다. 부모 컴포저블의 크기는 고정할수도 있고, 자식의 크기에 따라 동적으로 정하는것도 가능하다.
코드를 보면서 커스텀 레이아웃에 대해 살펴본다. 아래코드는 120, 80 크기의 Box 레이아웃 컴포저블이 있고, 내부에 파랑 색상의 Box 를 그리는 코드이다.
@Composable
fun MainScreen() {
Box(modifier = Modifier.size(120.dp, 80.dp)) {
ColorBox(Modifier.background(Color.Blue))
}
}
@Composable
fun ColorBox(modifier: Modifier) {
Box (Modifier.padding(1.dp).size(width = 50.dp, height = 10.dp).then(modifier))
}

커스텀 레이아웃 모디파이어
자 이제 커스텀 레이아웃 모디파이어를 만들어보자. 현재 Box 컴포저블은 본인의 왼쪽 모서리를 기준으로 ColorBox 컴포저블을 배치했다. 이것이 아무런 Modifier 를 전달하지 않았을때의 default 동작이다. 커스텀 레이아웃 모디파이어를 ColorBox에 적용하면, 부모 컴포저블인 Box 내부의 다른 위치로 이동이 가능하다.
fun Modifier.MyLayout (
// 선택적 파라미터
) = layout { measurable, constraints ->
// 요소 위치와 크기 조정하는 코드
}
위 코드에서 layout 의 후행 람다(Trailing Lambda)는 measurable, constraints 2개 파라미터를 전달한다.
- measurable: 해당 모디파이어가 호출된 자식요소가 배치될 정보
- constraints: 자식이 이용할 수 있는 최대/최소 폭과 높이
레이아웃 모디파이어는 부모 컨텍스트 안에서의 자식의 기본 위치에 신경쓰지 않는다. 대신 '기본 위치를 기준으로' 자식의 위치를 계산하는데 집중한다. 즉, 자식 컴포넌트의 레이아웃 모디파이어는 부모 컴포저블이 정해준 자식의 기본 위치를 (0, 0)이라고 가정하고, 해당 위치를 기준으로 모디파이어의 속성값을 적용한다.
위에 적은 레이아웃 모디파이어를 설명하는 문장의 의미를 좀더 쉽게 풀어써보면, 다음과 같다.
1) "부모 컨텍스트 안에서의 자식의 기본 위치에 신경 쓰지 않는다."
이때 '기본 위치'란, 자식 컴포넌트가 부모에게서 아무런 정렬(Alignment)이나 오프셋(Offset) 모디파이어를 받지 않았을 때 부모가 정해주는 초기 위치를 의미한다.
- Row, Column, Box 같은 레이아웃 컴포저블은 자식들을 배치하기 위한 고유의규칙을 가진다. 예를들면 Column 컴포저블은 위에서 아래로 자식 컴포저블을 배치한다.
- 자식의 레이아웃 모디파이어는 자식의 초기 위치를 결정하는데 아무런 영향을 미치지 않는다. 해당 역할은 부모 레이아웃 컴포저블 자체의 정렬 규칙에 맡긴다.
- 예를 들면 Column 컴포저블 안에 Text 컴포저블은 기본적으로 가장 왼쪽 상단에 배치(default)된다. 이 기본 위치는 Column 컴포저블이 정한다. 자삭 모디파이어는 이 기본 위치를 정하는데 있어서 아무런 관여를 하지 않는다
2) 대신 "기본 위치를 기준으로" 자식의 위치를 계산하는 데 집중한다.
부모 컴포저블이 자식의 기본위치를 확정한 이후에 자식 레이아웃 모디파이어의 설정이 적용된다.
- 1) 자식 모디파이어는 부모 레이아웃이 정해준 기본 위치 (Base Position)를 기준점(Anchor)으로 사용한다.
- 2) 모디파이어는 부모 레이아웃이 정해준 기준점에서 얼마나 떨어져서(Offset) 위치를 이동시키고, 정렬을 변경할지 계산한다.
Modifier.offset 을 예시로 동작을 이해해보자.
Column {
Text("Hello", Modifier.offset(x = 10.dp))
}
- 위 코드에서 Text 컴포저블은 부모 레이아웃(Column)이 정한 기본 위치(0, 0)에 배치된다.
- Text 의 모디파이어는 Column 이 정해준 (0, 0)을 기준으로 offset(x = 10.dp) 을 적용한다. 즉, 기본 위치(0, 0)을 기준으로 오른쪽으로 10dp만큼 이동한 (10, 0)에 Text를 위치시킨다.
ColorBox 에 커스텀 레이아웃을 적용해보자.
@Composable
fun MainScreen() {
Column(modifier = Modifier.size(120.dp, 80.dp)) {
ColorBox(
Modifier.background(Color.Red)
)
ColorBox(
Modifier
.MyLayout(30, 30)
.background(Color.Blue))
}
}
fun Modifier.MyLayout (
x: Int,
y: Int
) = layout { measurable, constraints ->
// 요소 위치와 크기 조정하는 코드
val placeable = measurable.measure(constraints)
layout(placeable.width, placeable.height) {
placeable.placeRelative(x, y)
}
}
부모인 Column 레이아웃 컴포저블은 자식을 위에서 아래로 순차적으로 배치한다. 커스텀 레이아웃 모디파이어를 적용하지 않으면 Column 이 정해준 기본위치를 (0, 0)라고 생각하고 ColorBox 가 생성되어 배치된다. 하지만 커스텀 레이아웃 모디파이어를 적용하면, Column이 정해준 기본위치(0, 0)에서 placeRelative(x, y)를 적용하여 가로, 세로 30dp 이동한 위치에 배치한다.
| 커스텀 레이아웃 모디파이어 적용안할 경우 | 커스텀 레이아웃 모디파이어 (MyLayout)적용할 경우 |
![]() |
![]() |
추가로 자식의 높이와 폭값에 접근할 수 있다면, 가상의 수평 또는 수직정렬 선을 기반으로 컴포저블 위치를 설정하는 것이 가능하다. 아래 코드를 보면, 가상의 정렬선을 사용하여 컴포넌트를 배치하는 코드이다.
@Composable
fun MainScreen() {
Column(
modifier = Modifier.size(120.dp, 80.dp)) {
ColorBox(
Modifier.background(Color.Red)
)
ColorBox(
Modifier
.MyLayout(0.25f)
.background(Color.Blue))
ColorBox(
Modifier
.MyLayout(0.5f)
.background(Color.Blue))
}
}
fun Modifier.MyLayout (
fraction: Float
) = layout { measurable, constraints ->
// 요소 위치와 크기 조정하는 코드
val placeable = measurable.measure(constraints)
val x = (placeable.width * fraction).roundToInt()
layout(placeable.width, placeable.height) {
placeable.placeRelative(x=x, y=0)
}
}
placeable.width 값에 비율을 적용한만큼 x축 offset 을 적용한 코드의 실행결과는 다음과 같다. ColorBox width 의 25%, 50% 만큼 오른쪽(x축)으로 옮겨진것을 확인할 수 있다.

젯팩 컴포즈에 내장된 Row, Column, Box 레이아웃 컴포저블과 Scope modifier 를 사용하면 다양한 속성을 적용할수 있지만, 이러한 표준 컴포저블로는 구현할수 없는 복잡한 레이아웃을 적용해야 하는 경우, 커스텀 레이아웃 모디파이어를 생성하여 자식 컴포저블에 적용하면 된다.

Ch28 커스텀 레이아웃 구현하기
젯팩 컴포즈가 제공하는 커스텀 레이아웃을 사용해서 직접 레이아웃 컴포넌트를 만들어본다. 먼저 코드부터 살펴본다.
@Composable
fun DoNothingLayout(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
// 측정 코드
val placeables = measurables.map { measurable ->
// 각 자식들을 측정
measurable.measure(constraints)
}
// 배치 코드
layout(constraints.maxWidth, constraints.maxHeight) {
placeables.forEach { placeable ->
placeable.placeRelative(0, 0)
}
}
}
}
@Composable
fun MainScreen2() {
DoNothingLayout(Modifier.padding(8.dp)) {
Text("Text1")
Text("Text2")
Text("Text3")
Text("Text4")
}
}
위 코드는 DoNothingLayout 이라는 레이아웃 컴포저블이다. 파라미터로 Modifier 와 Slot API 를 통해 UI 컴포넌트(컴포저블 함수)를 받는다. 내부에서 Layout() 컴포너블 함수를 호출하는데, 후행 람다로 measurable, constraints 파라미터를 받는다. 람다 함수내부에서 각각의 자식들을 측정하고, 측정값은 Placeable 객체 리스트와 매핑된다. Placeable 객체는 placeRelative(0, 0) 을 호출하기 때문에, 실제로 자식 컴포넌트의 위치값에는 아무런 영향을 주지 않는다.
DoNothingLayout 에 Text 4개를 배치하면 다음과 같이 모두 겹쳐져서 보이게 된다.

그래서 자식 컴포저블(Text)이 겹치지 않도록 위치 조정이 되도록 코드를 수정한다. 아래 예시는 20 정도 간격을 적용하는 코드와 실행결과 이다. 각각의 자식 Text 컴포저블이 x축, y축 간격만큼 떨어져서 배치되는 것을 확인할 수 있다.
@Composable
fun CascaseLayout(
spacing: Int = 0, // 자식컴포넌트간 띄울 간격
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
var indent = 0
// 측정 코드
val placeables = measurables.map { measurable ->
measurable.measure(constraints)
}
// 배치 코드
layout(constraints.maxWidth, constraints.maxHeight) {
var yCoord = 0
placeables.forEach { placeable ->
placeable.placeRelative(x=indent, y=yCoord)
// 컴포넌트를 배치한 이후에, x, y 축 각각 spacing 만큼 증가.
indent += placeable.width + spacing
yCoord += placeable.height + spacing
}
}
}
}
@Composable
fun MainScreen2() {
CascaseLayout(20, Modifier.padding(8.dp)) {
Text("Text1")
Text("Text2")
Text("Text3")
Text("Text4")
}
}

커스텀 레이아웃은 여기까지 !!
Spark Then Move !!
'안드로이드 앱' 카테고리의 다른 글
| 안드로이드 앱 개발, 어떤 언어를 사용해야 할까? (Kotlin vs Java) (0) | 2025.11.29 |
|---|---|
| 젯팩 컴포즈에서 XYZScope 의미와 동작원리 (RowScope, ColumnScope, BoxScope) (0) | 2025.11.28 |
| 안드로이드 젯팩 컴포즈 (ch22~25) - Slot API, 모디파이어(Modifier), Row/Column 레이아웃 (0) | 2025.11.26 |
| 안드로이드 젯팩 컴포즈 (ch18~21) - 컴포저블 개념, 상태 및 재구성 (0) | 2025.11.26 |
| 안드로이드 젯팩 컴포즈 스터디 시작. (0) | 2025.11.23 |




