[Android] Compose의 Modifier 제약 조건
Modifier의 제약 조건(Constraints)
Compose에서는 여러 Modifier를 체이닝하여 Composable의 모양과 동작을 변경할 수 있습니다. 이러한 Modifier 체인은 Composable에 전달되는 제약 조건(Constraints) 즉, 너비와 높이의 범위에 영향을 줄 수 있습니다.
이 페이지에서는 Modifier를 체이닝했을 때 제약 조건이 어떻게 영향을 받고, 그에 따라 Composable이 측정(measurement) 되고 배치(placement) 되는 방식이 어떻게 달라지는지를 설명합니다.
UI 트리에서의 Modifier
Modifier들이 서로에게 어떤 영향을 미치는지 이해하려면, 컴포지션 단계에서 생성되는 UI 트리에서 Modifier가 어떻게 나타나는지를 시각화하는 것이 도움이 됩니다.
UI 트리에서는 Modifier가 레이아웃 노드를 감싸는 래퍼 노드(wrapper node) 로 시각화됩니다.
Composable에 여러 Modifier를 추가하면 Modifier 체인이 생성됩니다. 여러 Modifier를 체이닝하면, 각 Modifier 노드는 그 다음 Modifier 체인과 레이아웃 노드를 감싸는 구조가 됩니다.
예를 들어, clip Modifier와 size Modifier를 체이닝할 경우, clip Modifier 노드가 size Modifier 노드를 감싸고, 그 안에 Image 레이아웃 노드가 위치하게 됩니다.
레이아웃 단계에서는 트리를 순회하는 알고리즘은 동일하게 유지되지만, 각 Modifier 노드도 함께 방문됩니다. 이렇게 함으로써 Modifier는 자신이 감싸고 있는 Modifier 또는 레이아웃 노드의 크기 요구사항(size requirements) 과 배치(placement) 에 영향을 줄 수 있습니다.
아래 그림에서 볼 수 있듯이, Image와 Text 같은 Composable의 구현은 실제로는 하나의 레이아웃 노드를 여러 Modifier가 감싸고 있는 구조입니다. 반면 Row와 Column 같은 Composable은 자식들을 어떻게 배치할지를 정의하는 레이아웃 노드 자체입니다.
요약하자면:
- Modifier는 하나의 Modifier 또는 레이아웃 노드를 감싼다.
- 레이아웃 노드는 여러 자식 노드를 배치할 수 있다.
- 이후 섹션에서는 이 개념 모델을 기반으로 Modifier 체이닝이 어떻게 동작하는지, 그리고 Composable의 크기에 어떤 영향을 미치는지를 설명합니다.
레이아웃 단계에서의 제약 조건(Constraints)
레이아웃 단계는 각 레이아웃 노드의 너비, 높이, 그리고 x, y 좌표를 계산하기 위해 다음의 세 단계 알고리즘을 따릅니다:
- 자식 측정(Measure children): 노드는 자식이 있다면 자식 노드를 측정합니다.
- 자신의 크기 결정(Decide own size): 자식 노드들의 측정 결과를 바탕으로 자신의 크기를 결정합니다.
- 자식 배치(Place children): 각 자식 노드를 자신의 위치를 기준으로 배치합니다.
이 과정에서 제약 조건(Constraints) 은 첫 번째와 두 번째 단계에서 노드의 적절한 크기를 결정하는 데 도움을 줍니다.
Constraints는 노드의 너비와 높이에 대한 최소값과 최대값을 정의하며, 노드는 자신이 결정한 크기가 이 제약 범위 안에 들어야 합니다.
제약 조건의 종류
제약 조건은 다음 중 하나일 수 있습니다:
- Bounded(제한된): 노드는 최소 너비/높이와 최대 너비/높이를 모두 갖습니다.
- Unbounded(무제한): 노드는 크기에 대한 제약이 없습니다. 최대 너비와 높이 제한이 무한대로 설정됩니다.
- Exact(정확한): 노드는 정확한 크기를 따라야 합니다. 최소값과 최대값이 동일한 값으로 설정됩니다.
- Combination(조합형): 위의 제약 조건들을 혼합한 형태입니다. 예를 들어, 너비는 제한되지만 높이는 무제한일 수 있고, 너비는 정확하게 지정되지만 높이는 범위가 지정될 수도 있습니다.
다음 섹션에서는 이러한 제약 조건이 부모에서 자식으로 어떻게 전달되는지를 설명합니다.
제약 조건이 부모에서 자식으로 전달되는 방식
앞서 설명한 레이아웃 단계의 알고리즘의 첫 번째 단계에서, UI 트리에서 부모 노드가 자식 노드를 측정할 때 제약 조건(Constraints)을 자식에게 전달하게 됩니다.
부모 노드는 자식에게 "얼마나 크게 또는 작게 될 수 있는지"를 알려주기 위해 이러한 제약 조건을 제공합니다. 이후, 부모 노드도 자신의 부모로부터 전달받은 제약 조건에 따라 자신의 크기를 결정합니다.
상위 수준에서 이 알고리즘은 다음과 같은 방식으로 작동합니다:
- 루트 노드(root node) 는 자식들의 크기를 측정하기 위해 제약 조건을 첫 번째 자식에게 전달합니다.
- 자식이 측정에 영향을 주지 않는 Modifier라면, 그 제약 조건을 다음 Modifier에게 그대로 전달합니다.
→ 만약 측정에 영향을 주는 Modifier를 만나면, 해당 Modifier가 제약 조건을 재조정(resize) 합니다. - 자식이 더 이상 자식을 가지지 않는 "리프 노드(leaf node)" 에 도달하면, 전달받은 제약 조건에 따라 자신의 크기를 결정하고, 그 결과를 부모에게 반환합니다.
- 부모는 해당 자식의 측정 결과를 바탕으로 자신의 다음 자식에게 전달할 제약 조건을 조정하여 전달합니다.
- 부모가 모든 자식의 측정을 완료하면, 자신의 크기를 결정하고, 그 정보를 자신의 부모 노드에게 전달합니다.
이러한 방식으로 UI 트리 전체가 깊이 우선 탐색(depth-first traversal) 되며, 최종적으로 모든 노드의 크기가 결정되고 측정 단계가 완료됩니다.
제약 조건에 영향을 주는 Modifier
이전 섹션에서 일부 Modifier는 제약 조건의 크기에 영향을 줄 수 있다는 것을 배웠습니다.
이번 섹션에서는 실제로 제약 조건에 영향을 주는 구체적인 Modifier 들을 설명합니다.
size Modifier
size Modifier는 콘텐츠의 선호 크기(preferred size) 를 선언합니다.
예를 들어, 다음과 같은 UI 트리는 300dp × 200dp 크기의 컨테이너 안에서 렌더링되어야 합니다.
이 경우 제약 조건은 너비 100dp~300dp, 높이 100dp~200dp 로 제한된(bounded) 상태입니다:
size Modifier는 자신에게 전달된 제약 조건을 자신이 받은 값에 맞게 조정합니다.
아래 예시에서는 지정된 크기가 150dp일 때:
지정된 너비와 높이가 최소 제약값보다 작거나, 최대 제약값보다 클 경우에는 size Modifier는 제약 조건 범위 안에서 가장 가깝게 맞는 크기로 조정합니다:
❗ 주의:
여러 개의 size Modifier를 체이닝하는 것은 작동하지 않습니다.
첫 번째 size Modifier가 최소값과 최대값을 고정된 값으로 설정하기 때문에, 두 번째 Modifier에서 더 작거나 큰 값을 지정하더라도 이미 정해진 제약 조건을 벗어날 수 없어 무시됩니다:
requiredSize Modifier
노드가 전달받은 제약 조건을 무시하고 지정한 크기를 반드시 적용해야 할 경우에는 size 대신 requiredSize Modifier를 사용하세요.
requiredSize Modifier는 전달된 제약 조건을 완전히 대체하고, 지정한 크기를 정확한 제약 조건(Exact bounds) 으로 전달합니다.
크기 결정이 완료되어 트리 상단으로 전달될 때, 자식 노드는 사용 가능한 공간 내에서 중앙에 배치됩니다:
width 및 height Modifier
size Modifier는 너비와 높이 모두를 제약 조건에 맞게 조정합니다.
하지만 width Modifier를 사용하면 고정된 너비만 설정하고, 높이는 제약 없이 남겨둘 수 있습니다.
마찬가지로 height Modifier를 사용하면 고정된 높이만 설정하고, 너비는 제약 없이 유지할 수 있습니다.
sizeIn Modifier
sizeIn Modifier를 사용하면 너비와 높이에 대한 최소 및 최대 제약 조건을 정확하게 설정할 수 있습니다.
제약 조건을 세밀하게 조정해야 하는 경우 sizeIn Modifier를 사용하는 것이 적합합니다.
예시
이번 섹션에서는 여러 Modifier가 체이닝된 코드 조각의 출력 결과를 설명합니다.
Image(
painterResource(R.drawable.hero),
contentDescription = null,
Modifier
.fillMaxSize()
.size(50.dp)
)
이 코드의 출력 결과는 다음과 같습니다:
- fillMaxSize Modifier는 최소 너비와 높이 제약을 최대값으로 설정합니다.
→ 즉, 너비는 300dp, 높이는 200dp로 설정됩니다. - 그 뒤에 오는 size(50.dp) Modifier는 50dp를 사용하고자 하지만, 이미 전달된 최소 제약(300dp × 200dp) 을 반드시 따라야 합니다.
따라서, 이 Modifier는 결과적으로 300 × 200의 제약 조건을 그대로 출력하게 되며, size에 명시된 50dp는 무시됩니다. - 결국 Image는 이 제약 조건(300dp × 200dp)을 따르게 되며, 크기는 300 × 200으로 결정되어 트리 상단까지 전달됩니다.
Image(
painterResource(R.drawable.hero),
contentDescription = null,
Modifier
.fillMaxSize()
.wrapContentSize()
.size(50.dp)
)
이 코드의 출력 결과는 다음과 같습니다:
- fillMaxSize Modifier는 제약 조건을 조정하여 최소 너비와 높이를 최대값으로 설정합니다 — 예를 들어, 너비 300dp, 높이 200dp로 고정됩니다.
- wrapContentSize Modifier는 이러한 최소 제약 조건을 초기화합니다.
따라서 fillMaxSize로 인해 고정되었던 제약 조건이 다시 제한된(bounded) 제약 조건으로 되돌아갑니다.
이제 이후 노드는 전체 공간을 차지할 수도 있고, 그보다 작은 크기도 가질 수 있습니다. - size Modifier는 최소 및 최대 제약 조건을 50dp로 설정합니다.
- Image는 이 제약 조건에 따라 50 × 50dp 크기로 결정되며, size Modifier는 이 값을 상위로 전달합니다.
- wrapContentSize Modifier에는 특별한 속성이 있습니다.
자식 Composable을 전달받은 최소 제약(min bounds)의 중앙에 배치합니다.
따라서 부모에게 전달하는 자신의 크기도, 전달받은 최소 제약값과 동일한 크기가 됩니다.
Image(
painterResource(R.drawable.hero),
contentDescription = null,
Modifier
.clip(CircleShape)
.padding(10.dp)
.size(100.dp)
)
이 코드의 출력 결과는 다음과 같습니다:
- clip Modifier는 제약 조건을 변경하지 않습니다.
- padding Modifier는 최대 제약 조건(max constraints)을 줄입니다.
→ 즉, 패딩만큼의 공간을 자식에게서 빼앗습니다. - size Modifier는 모든 제약 조건을 100dp로 고정합니다.
- Image는 이러한 제약 조건을 따르며 100 × 100dp 크기로 결정됩니다.
- padding Modifier는 모든 방향에 10dp의 여백을 추가하므로, 전체 크기는 20dp 증가하여 120 × 120dp로 보고됩니다.
- 드로잉(drawing) 단계에서는, clip Modifier가 120 × 120dp 크기의 캔버스 위에서 동작합니다.
→ 즉, 이 크기에 맞춰 원형 마스크(circle mask) 를 생성합니다. - 이후 padding Modifier는 내용을 안쪽으로 10dp씩 밀어넣기(inset) 때문에, 캔버스 크기는 100 × 100dp로 축소됩니다.
- Image는 이 축소된 캔버스 위에 그려지며,
원래 120dp 원형 마스크 기준으로 잘리기 때문에, 결과적으로 이미지는 완전히 원형이 아닌 형태로 잘립니다.
끝.
참고자료
제약 조건 및 수정자 순서 | Jetpack Compose | Android Developers
이 페이지는 Cloud Translation API를 통해 번역되었습니다. 제약 조건 및 수정자 순서 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Compose에서는 여러 수정자를
developer.android.com