스택 뷰는 여러 뷰들을 묶어주는 컨테이너 뷰다. 스택 뷰 내부에 서브 뷰를 추가하는 방식으로 스택 뷰를 구성할 수 있다.
스택 뷰 내부의 서브 뷰들은 기기의 상태(회전 방향, 스크린 크기 등)와 스택 뷰의 axis, alignment, distribution, spacing 프로퍼티를 기반으로 자동으로 레이아웃이 결정된다.
intrinsicContentSize
서브 뷰와 스택 뷰의 레이아웃이 어떻게 결정되는지 이해하려면 intrinsicContentSize에 대한 이해가 필요하다.
공식 문서에서는 ‘뷰 자체의 속성만을 고려한, 뷰가 자연스럽게 가지는 크기’ 라고 설명되어 있다. 가장 간단한 UILabel의 예시를 보자.

UILabel은 따로 제약조건을 설정하지 않는 이상 내부 텍스트의 길이에 따라서 뷰의 크기가 결정되게 된다. 위 캡처에서 보이는 두 레이블은 텍스트에 크기에 따라서 자연스럽게 가로 크기가 결정되었다.
스택 뷰도 마찬가지로 내부에 있는 서브 뷰들의 크기에 의해서 intrinsicContentSize가 결정된다.
서브 뷰 레이아웃 결정하기
먼저 스택 뷰를 하나 만들겠다.
private let stackView = UIStackView()
let redView = CustomColorView(color: .red, preferredWidth: 50)
let greenView = CustomColorView(color: .green, preferredWidth: 50)
let blueView = CustomColorView(color: .blue, preferredWidth: 50)
stackView 안에 redView, greenView, blueView를 포함시키려 한다. 스택 뷰는 addArragnedSubview(_:) 를 사용해서 서브 뷰를 추가하게 된다.
stackView.addArrangedSubview(redView)
stackView.addArrangedSubview(greenView)
stackView.addArrangedSubview(blueView)

red, green, blue 순서대로 스택 뷰에 추가했고, 실제 순서도 추가한 것과 같이 나열된 것을 확인할 수 있다. 또한 스택 뷰의 leading에 첫 뷰가 고정되어있고, trailing에 첫 뷰가 고정되어 있는 것도 확인할 수 있다.
여기서 두 가지 규칙을 확인할 수 있다.
- 스택 뷰는 스택에 추가된 순서대로 (정확히는
arrangedSubviews배열의 인덱스 순서대로) 뷰을 나열한다. - 서브 뷰 중 첫 번째와 마지막 뷰를 축에 따라 양 끝에 맞춰서 배치한다.
- 만약 가로 스택 뷰라면, 첫 뷰는 leading, 마지막 뷰는 trailing에 고정된다.
- 세로 스택 뷰 이라면, 첫 뷰는 top, 마지막 뷰는 bottom에 고정된다.
만약 순서를 바꾸면 어떻게 될까? blue, green, red 순서대로 넣어보자.

예상한 대로 blue, green, red 순서대로 배치된 것을 확인할 수 있다.
axis
axis 프로퍼티는 말 그대로 스택 뷰의 축을 결정하게 된다. 위 캡처에서 보면 서브 뷰들이 가로로 배치되어 있는데, 이는 스택 뷰의 기본 axis가 horizontal로 설정되어 있기 때문이다.
axis 가 가질 수 있는 상태는 .vertical, .horizontal 두 개이다.
- horizontal: 스택 뷰의 기본 축 상태이다. 서브 뷰들을 가로로 배치해서 가로 스택 뷰를 만든다. 앞서 나온 캡처들이 horizontal 상태이다.
- vertical: 서브 뷰들을 세로로 배치해서 세로 스택 뷰를 만든다.

distribution
스택 뷰의 축을 따라 뷰가 ‘분포’하는 방식을 결정한다. 말이 좀 어렵지만, 축 위에 어떻게 뷰를 배치할지 결정하는 프로퍼티이다. 기본적으로 .fill 상태로 설정된다.
스택 뷰의 크기를 결정하는 방법은 사용자가 직접 제약을 걸거나, 혹은 내부 서브 뷰들의 크기로 결정되게 하는 방법 두 가지가 있다. distribution은 스택 뷰의 크기가 제약이 걸려 있는 경우에 영향을 끼친다. 한정된 길이의 축에 뷰의 크기를 조정하여 나타내는 방법인데, 만약 내부 컨텐츠에 따라서 축의 길이가 유동적인 경우에는 서브 뷰의 크기를 조정할 필요가 없기 때문이다. (하지만 예외도 있다. 아래에서 설명하겠다.)
따라서 동작을 좀 더 정확하게 보기 위해 스택 뷰의 leading과 trailing을 safe area로 고정해서 디스플레이 너비를 축의 길이로 고정해서 예제를 작성했다.
fill
서브 뷰의 크기를 스택 뷰의 크기에 맞게 조절하는 방법이다. 늘어나야 하는 경우 hugging priority, 줄어들어야 하는 경우 resistance priority를 기준으로 크기가 조정될 서브 뷰를 결정한다.

ColorView 의 기본 intrinsic size를 (100, 100)으로 설정했는데, 그 인스턴스인 redView 의 길이를 늘려서 뷰의 leading과 trailing에 일치시킨 것을 확인할 수 있다.

어떤 뷰가 늘어날지 혹은 줄어들지는 hugging과 compression resistance에 달려있다. 만약 서브 뷰들의 그 값들이 모두 같다면, 인덱스를 기반으로 늘어날 뷰를 선택하게 된다. 기준이 명확하지 않은 것 같지만 일반적으론 맨 앞 혹은 맨 뒤 서브 뷰의 크기를 조정하게 된다.
우리가 hugging priority, compression resistance priority를 임의적으로 조절해서 크기가 조정될 뷰를 직접 지정할 수도 있다.
redView.setContentHuggingPriority(.defaultHigh, for: .horizontal)
greenView.setContentHuggingPriority(.defaultLow, for: .horizontal)
blueView.setContentHuggingPriority(.defaultHigh, for: .horizontal)
만약 이런 식으로 hugging priority를 조정하게 된다면 다음과 같은 결과가 나온다.

greenView의 hugging priority를 가장 낮게 설정했으므로, 늘어나는 뷰는 greenView가 되는 것을 알 수 있다.
fillEqually
서브 뷰들의 축과 평행한 방향의 길이를 모두 똑같이 맞추는 방식으로 서브 뷰의 크기를 조절하는 방법이다. 앞선 fill과 다르게 모든 뷰의 크기를 동일하게 조정하므로, hugging이나 compression resistance priority의 영향을 받지 않는다.
fillProportionally
스택 뷰의 축과 평행행 방향의 서브 뷰의 길이를 비례적으로 조정한다. 즉 원래 긴 뷰는 더 길어지게 된다.
앞선 예제를 그대로 쓰기엔 모든 ColorView 인스턴스들의 너비가 100으로 같으므로, fillEqually와 차이점을 볼 수 없다. 그래서 예제를 살짝 조정해보겠다.
let redView = ColorView(color: .red, preferredWidth: 50)
let greenView = ColorView(color: .green, preferredWidth: 100)
let blueView = ColorView(color: .blue, preferredWidth: 150)

R, G, B 뷰를 각각 50, 100, 150으로 설정해서 1:2:3 비율이 되도록 했다. 이제 각 뷰들은 얼마나 늘어났는지 확인해보자

수치는 늘어났지만, 1:2:3 비율을 유지하면서 늘어났다. 이렇게 비례적으로 뷰의 길이를 조정하는 방식이 .fillProportionally이다.
아래에서도 이 50, 100, 150의 선호 너비를 유지하겠다. 아래 방식들도 서로 뷰의 너비가 다른 것이 더 잘 확인할 수 있다.
equalSpacing
뷰들 사이의 간격을 일정하게 조정하는 것이다. 즉, 가로 스택 뷰에선 앞선 서브 뷰의 trailing과 현재 서브 뷰의 leading의 간격이 일정하게 조정된다.

캡처에서 볼 수 있듯이, 서브 뷰의 높이나 너비를 건들지 않고, 간격만 조정하게 된다. (위에서 말했듯이 이제 각 서브 뷰의 너비를 다르게 설정한거지, 변한게 아니다.) 각 서브 뷰의 간격이 일정한 것을 볼 수 있다.
equalCentering
서브 뷰들의 ‘중심’의 간격을 일정하게 조정하는 것이다.

위 캡처에서 보면 간격이 뒤죽박죽 처럼 보이지만, redView의 중심, greenView의 중심 그리고 blueView의 중심의 간격은 일정한 것을 알 수 있다.
마찬가지로 서브 뷰의 너비는 건들지 않고 간격만 조정한다.
alignment
스택 뷰의 축과 수직인 방향을 정렬하는 프로퍼티이다. 각 상태들의 변화를 더 잘 확인하기 위해 뷰들의 선호 높이를 각각 다르게 설정하고, distribution은 .equalSpacing으로 설정해서 혼란을 방지하겠다.
let redView = ColorView(color: .red, preferredWidth: 50, preferredHeight: 50)
let greenView = ColorView(color: .green, preferredWidth: 100, preferredHeight: 100)
let blueView = ColorView(color: .blue, preferredWidth: 150, preferredHeight: 150)
alignment는 distribution과 다르게 스택의 크기가 명시적인 제약이 존재하든, 아니면 서브 뷰로 정해지든 상관 없이 영향을 끼친다. 또한 방향성도 존재한다. 만약 가로 스택 뷰면 top 혹은 bottom 정렬을 하고, 세로 스택 뷰면 leading 혹은 trailing 정렬을 해야 하기 때문이다.
기본 값은 fill로 정의되어 있다.
fill
스택의 축과 수직인 방향의 크기만큼 서브 뷰들의 (같은 방향의) 크기를 늘린다. 즉 가로 스택 뷰면 스택 뷰의 높이만큼 모든 서브 뷰의 높이를 조정한다.

서브 뷰들의 높이를 각각 다르게 설정했지만 스택 뷰의 높이인 150으로 모두 같게 된 것을 확인할 수 있다. 스택 뷰의 높이가 150인 이유는 다음 섹션에서 설명하겠다(간단하게 가장 높이가 높은 서브 뷰의 높이로 맞춰진다).

center, leading, trailing, top, bottom
말 그대로 해당 방향으로 정렬하는 것이다. 특이한 점은 .leading은 .top과 같고, .trailing은 .bottom과 동등하도록 구현되어 있다. 즉 .top과 .bottom은 단지 syntax sugar일 뿐이고 가로 방향에서도 .leading과 .trailing을 사용해도 큰 문제는 없다.
- center

- top(= leading)

- trailing(= bottom)

spacing
spacing은 서브 뷰들 사이의 간격을 지정하는 프로퍼티이다. 다시 모든 서브 뷰들의 크기를 동일하게 바꾸고 alignment를 .fill로 바꾸고, distribution은 .fillEqually로 변경하겠다.
spacing을 20으로 설정해보자.

서브 뷰들 사이에 간격이 생긴 것을 확인할 수 있다. 여기에서 우리가 spacing의 동작에 대해서 가질만한 의문점 두 개가 있다.
distribution이.equalSpacing일 때 의 동작은 어떻게 될까?- 만약 서브 뷰들의 선호 크기와 간격의 합이 스택 뷰의 크기를 넘어서면 어떻게 될까?
첫 번째부터 확인해보면, 개발자 문서에 .equalSpacing이나 .equalCentering의 경우에는 spacing이 간격의 최소 단위가 된다고 써져있다.
그러면 spacing을 100으로 설정해서 어떻게 동작하는지 확인해보자.

각 뷰들이 수축되는 현상이 일어났다. 그럼 뷰 사이의 간격은 100이 맞을지 확인해보자. (iPhone SE3의 가로 길이는 375 point이다.)

spacing 200 + rgb view 175 하면 375가 나온다. 즉 spacing은 최소 간격을 설정하는 역할임을 확인할 수 있다.
자연스럽게 두 번째 질문의 답도 알 수 있다. 만약 서브 뷰들의 선호 크기 + 뷰간 간격이 스택 뷰의 크기를 넘어선다면, 그냥 뷰의 크기를 축소시켜버린다.
간격 커스터마이징 하기
setCustomSpacing(_:after:) 메소드를 통해서 각 간격마다 다르게 커스터마이징 할 수 있다. 예를 들어 기본 간격이 10인데, greenView와 blueView 사이의 간격만 30으로 설정하고 싶으면 다음과 같이 하면된다.
stackView.setCustomSpacing(30, after: greenView)

잘 동작하는 것을 확인할 수 있다.
스택 뷰 크기 지정하기
앞선 섹션에서는 고정된 제약 조건을 가진 스택 뷰에서 서브 뷰를 어떻게 배치하는지를 살펴봤다. 하지만 스택 뷰의 크기를 서브 뷰들의 크기와 간격으로 자동적으로 계산되게 할 수도 있다.
크기 제약 조건을 명시적으로 설정한 스택 뷰
고정된 크기가 아님을 주의하자. 스택 뷰는 서로 맞닿은 두 개의 엣지만 고정하면 서브 뷰들의 크기로 자연스럽게 크기를 결정할 수 있지만, 스택 뷰의 크기를 제한하고 싶을 수 있다. 앞서 한 것 처럼 이런 경우에는 스택 뷰의 크기와 distribution, alignment 설정에 맞춰서 서브 뷰의 크기를 조절하게 된다.
다음과 같이 스택 뷰의 상하좌우 크기를 제한했다고 가정해보자.
stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
stackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
stackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor)

스택 뷰의 크기가 safe area의 크기와 같아졌으므로, 내부 서브 뷰들이 그 크기에 맞춰서 늘어나게 된다. 만약 작게 잡으면 축소가 된다.
이래서 distribution과 같은 경우는 스택 뷰가 상하좌우 다 제약 조건이 있는 경우(엄밀히 말하면 축과 평행한 방향의 길이의 제약조건이 있는 경우)에만 영향을 끼치게 된다.
스택 뷰의 크기에 제약 조건이 있으면, 내부 컨텐츠 뷰가 정확하게 그 크기에 맞추기 어렵게 된다. 따라서 간격이나 뷰가 늘어나거나 뷰가 축소되는 경우가 생기는 것이다.
크기 제약조건이 없는 스택 뷰
앞서서 살펴봤듯이 스택 뷰의 서브 뷰들을 직접적으로 오토 레이아웃을 사용해서 배치할 필요는 없다. 하지만 스택 뷰 자체는 오토레이아웃을 사용해서 배치해야 한다. 일반적으로 스택 뷰는 서로 맞닿은 두 개의 엣지, 대표적으로 top과 leading을 다른 뷰에 고정시키는 방식으로 위치를 지정한다.
상하좌우를 다 잡을 필요가 없는 이유는 스택 뷰의 크기를 서브 뷰들에 의해서 자동으로 결정되게 할 수 있기 때문이다. 개발자 문서에서는 스택 뷰의 크기를 결정하는 방법을 다음과 같이 제시한다.
- 스택 축 방향의 크기는 모든 서브 뷰의 축과 평행한 길이 합계 뷰 사이의 간격의 합이다. (예를 들면, 가로 스택의 뷰의 경우 가로의 길이는 모든 서브 뷰의 가로 길이 + 뷰들 사이의 spacing의 합이 된다.)
- 스택 축과 수직인 방향의 크기는 서브 뷰들 중 가장 큰 뷰의 크기가 된다. (예를 들면, 가로 스택 뷰의 경우 세로의 길이는 서브 뷰들 중 가장 긴 세로 길이를 가진 뷰의 길이와 같아진다.)
- 만약
isLayoutMarginsRelativeArrangement프로퍼티를true로 설정하면 스택 뷰의 크기를 마진까지 포함해서 계산한다.
그럼 크기 제약을 없애고, 스택 뷰를 그냥 safe area의 정중앙에 배치해보고, 간격도 약간 추가해보자.
stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor),

뷰들이 선호 크기에 맞게 그려지고, 간격도 지정된 값에 맞게 그려진다. 스택 뷰의 크기가 자동으로 조절된 것이다.
하지만 완전 distribution이 의미가 없는 것도 아니다. 위와 같이 .equalSpacing은 확실히 최소 spacing을 지키면서 스택 뷰에 영향을 끼치게 된다. 아래 캡처는 임의적으로 뷰의 크기를 조절한 것이다.

'iOS > UIKit' 카테고리의 다른 글
| [UIKit] UIResponder + Responder chain 흐름 (0) | 2025.07.21 |
|---|---|
| [UIKit] UIView와 CALayer 사용하기 (0) | 2025.07.17 |