🟣 좌표를 박으면 왜 깨지나
앞 장까지는 뷰의 위치를 frame에 좌표 숫자로 직접 넣었다. 작은 예제에선 잘 되지만, 실제 앱에서는 금방 깨진다. iPhone SE의 좁은 화면과 큰 Pro Max의 넓은 화면은 폭이 다르고, 가로로 돌리면 또 달라진다. "왼쪽에서 320만큼" 같은 절대 좌표는 특정 기기에서만 맞는다.
그래서 필요한 건 좌표가 아니라 관계다. "이 버튼은 화면 왼쪽에서 20 떨어지고, 오른쪽 끝까지 채운다" 같은 규칙을 걸어 두면, 실제 위치는 기기 크기에 따라 시스템이 계산한다. 이 규칙 기반 배치가 오토레이아웃이다.
처음엔 좌표를 직접 주는 쪽이 훨씬 직관적으로 느껴졌다. 하지만 지원 기기가 늘수록 좌표 방식은 분기가 걷잡을 수 없이 불어났다. "어디에 있는가" 대신 "무엇에 대해 어떻게 놓이는가"로 사고를 바꾸는 것이 오토레이아웃의 출발이었다.
🟣 제약: 뷰 사이의 관계를 선언한다
오토레이아웃의 기본 단위는 제약(constraint)이다. 제약 하나는 "이 뷰의 어떤 변이 저 뷰의 어떤 변에 대해 이런 값을 갖는다"는 하나의 규칙이다. 예를 들어 "내 왼쪽 변 = 슈퍼뷰 왼쪽 변 + 16" 같은 식이다.
요즘은 이 제약을 앵커(anchor) 문법으로 짧게 건다. 각 뷰가 leadingAnchor, topAnchor, widthAnchor 같은 앵커를 갖고, 이들을 서로 연결해 제약을 만든다.
코드로 제약을 걸 때 반드시 함께 나오는 한 줄이 있다.
let box = UIView()
box.translatesAutoresizingMaskIntoConstraints = false // 이 줄이 없으면 충돌한다
view.addSubview(box)
NSLayoutConstraint.activate([
box.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16),
box.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
box.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
box.heightAnchor.constraint(equalToConstant: 120),
])🚀 translatesAutoresizingMaskIntoConstraints는 왜 꺼야 하나
이 긴 프로퍼티를 false로 두는 걸 빠뜨리면 뷰가 엉뚱한 곳에 가거나 제약 충돌 로그가 쏟아진다. 이유는 이렇다. 코드로 만든 뷰는 기본적으로 자신의 frame을 자동으로 제약으로 바꾸려 한다. 이 자동 변환이 우리가 직접 건 제약과 부딪친다. 그래서 직접 제약을 걸 뷰에서는 이 자동 변환을 꺼야 한다. 입문기에 원인을 몰라 가장 오래 헤맨 한 줄이었다.
safeAreaLayoutGuide는 노치나 하단 홈 인디케이터를 피한 안전한 영역이다. 상단·하단 제약을 뷰의 끝이 아니라 이 안전 영역에 걸면, 콘텐츠가 노치에 가리지 않는다.
🟣 우선순위: 다 만족할 수 없을 때
제약을 여러 개 걸다 보면 서로 충돌하는 상황이 생긴다. 예를 들어 레이블을 "가로로 꽉 채우고 싶다"와 "옆 버튼도 항상 보이게 하고 싶다"가 동시에 최고 강도로 걸리면, 둘 다 만족시킬 수 없다.
그래서 각 제약에는 우선순위가 있다. 값은 1부터 1000까지이고, 1000은 반드시 지켜야 하는 필수 제약이다. 충돌이 나면 시스템은 우선순위가 낮은 제약을 양보시켜 해를 찾는다. 위 예에서 "꽉 채우기"를 1000으로, "버튼 보이기"를 그보다 낮게 두면, 공간이 부족할 때 레이블이 줄어들며 버튼 자리를 내준다.
콘텐츠 크기와 관련해서는 두 가지 특수한 우선순위를 자주 만난다. 콘텐츠 허깅(content hugging)은 "내 콘텐츠보다 커지지 않으려는" 저항이고, 압축 저항(compression resistance)은 "내 콘텐츠보다 작아지지 않으려는" 저항이다. 레이블 두 개가 한 줄에 있을 때 어느 쪽이 늘어나고 어느 쪽이 줄어들지는 이 두 값이 정한다. 텍스트가 잘리는 문제의 상당수는 압축 저항 우선순위를 조정해 풀린다.
🟣 스토리보드 없이 코드로 화면 만들기
이제 지금까지의 조각을 모아 스토리보드 없이 화면 하나를 코드로 세워 본다. 흐름은 단순하다. 뷰컨트롤러를 만들고, 그 안에서 서브뷰를 만들어 붙이고, 제약을 걸고, 이 뷰컨트롤러를 윈도우의 첫 화면으로 지정한다.
final class WelcomeViewController: UIViewController {
private let titleLabel = UILabel()
private let startButton = UIButton(type: .system)
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
titleLabel.text = "환영합니다"
titleLabel.font = .boldSystemFont(ofSize: 28)
startButton.setTitle("시작하기", for: .normal)
[titleLabel, startButton].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
view.addSubview($0)
}
NSLayoutConstraint.activate([
titleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
titleLabel.topAnchor.constraint(
equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 80),
startButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
startButton.topAnchor.constraint(
equalTo: titleLabel.bottomAnchor, constant: 24),
])
}
}이 화면을 앱의 첫 화면으로 띄우는 연결은 앞 장에서 본 SceneDelegate의 rootViewController 지정이 그대로다. 스토리보드를 쓰면 이 초기 설정이 프로젝트 설정과 xib 파일 뒤에 숨는데, 코드로 하면 화면이 만들어져 붙기까지의 전 과정이 한눈에 드러난다. 처음엔 손이 많이 가지만, 무엇이 자동으로 처리되고 있었는지를 알게 되는 값이 그만큼 컸다.
코드 기반의 또 다른 이점은 협업에서 나온다. 스토리보드는 XML 한 덩어리라 여러 명이 같은 화면을 고치면 병합 충돌이 험하게 난다. 코드로 짠 UI는 일반 코드처럼 변경 지점이 또렷해 리뷰와 병합이 훨씬 수월하다.
🟣 정리
- 절대 좌표는 기기마다 깨진다. 오토레이아웃은 좌표 대신 뷰 사이의 관계(제약)를 선언해, 실제 위치를 시스템이 계산하게 한다.
- 제약은 앵커 문법으로 건다. 코드로 제약을 걸 뷰는
translatesAutoresizingMaskIntoConstraints를false로 꺼야 자동 변환과 충돌하지 않는다. - 상·하단 제약은
safeAreaLayoutGuide에 걸어 노치와 홈 인디케이터를 피한다. - 제약이 충돌하면 우선순위(1~1000)가 낮은 쪽이 양보한다. 콘텐츠 허깅과 압축 저항이 늘고 줄어드는 순서를 정한다.
- 스토리보드 없이 코드로 짜면 화면 구성의 전 과정이 드러나고, 병합 충돌도 줄어든다.
실무에서 오토레이아웃은 매일 쓰는 도구이자 가장 흔한 버그의 진원지다. "제약 충돌 로그", "텍스트가 잘림", "뷰가 안 보임"의 대부분이 우선순위와 translatesAutoresizingMaskIntoConstraints에서 갈린다. 면접에서 "왜 이 프로퍼티를 꺼야 하는가", "우선순위로 어떻게 잘림을 푸는가"를 원리로 답할 수 있으면 오토레이아웃을 실제로 다뤄 봤다는 신호가 된다.
이제 어떤 화면에서도 깨지지 않는 배치를 잡을 수 있다. 다음 장에서는 이 배치 위에 실제 컴포넌트, 특히 목록을 그리는 테이블뷰를 올리고, 뷰컨트롤러가 비대해지지 않도록 MVC로 역할을 나누는 방법을 본다.