LODY/정리

UIKit로 화면 만들기 / 오토레이아웃과 코드 기반 UI

오토레이아웃과 코드 기반 UI

🟣 좌표를 박으면 왜 깨지나

앞 장까지는 뷰의 위치를 frame에 좌표 숫자로 직접 넣었다. 작은 예제에선 잘 되지만, 실제 앱에서는 금방 깨진다. iPhone SE의 좁은 화면과 큰 Pro Max의 넓은 화면은 폭이 다르고, 가로로 돌리면 또 달라진다. "왼쪽에서 320만큼" 같은 절대 좌표는 특정 기기에서만 맞는다.

그래서 필요한 건 좌표가 아니라 관계다. "이 버튼은 화면 왼쪽에서 20 떨어지고, 오른쪽 끝까지 채운다" 같은 규칙을 걸어 두면, 실제 위치는 기기 크기에 따라 시스템이 계산한다. 이 규칙 기반 배치가 오토레이아웃이다.

처음엔 좌표를 직접 주는 쪽이 훨씬 직관적으로 느껴졌다. 하지만 지원 기기가 늘수록 좌표 방식은 분기가 걷잡을 수 없이 불어났다. "어디에 있는가" 대신 "무엇에 대해 어떻게 놓이는가"로 사고를 바꾸는 것이 오토레이아웃의 출발이었다.


🟣 제약: 뷰 사이의 관계를 선언한다

오토레이아웃의 기본 단위는 제약(constraint)이다. 제약 하나는 "이 뷰의 어떤 변이 저 뷰의 어떤 변에 대해 이런 값을 갖는다"는 하나의 규칙이다. 예를 들어 "내 왼쪽 변 = 슈퍼뷰 왼쪽 변 + 16" 같은 식이다.

요즘은 이 제약을 앵커(anchor) 문법으로 짧게 건다. 각 뷰가 leadingAnchor, topAnchor, widthAnchor 같은 앵커를 갖고, 이들을 서로 연결해 제약을 만든다.

슈퍼뷰안전 영역(safeAreaLayoutGuide)내 뷰네 모서리 앵커 연결노치·홈바 피해 배치

코드로 제약을 걸 때 반드시 함께 나오는 한 줄이 있다.

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는 노치나 하단 홈 인디케이터를 피한 안전한 영역이다. 상단·하단 제약을 뷰의 끝이 아니라 이 안전 영역에 걸면, 콘텐츠가 노치에 가리지 않는다.


🟣 우선순위: 다 만족할 수 없을 때

제약을 여러 개 걸다 보면 서로 충돌하는 상황이 생긴다. 예를 들어 레이블을 "가로로 꽉 채우고 싶다"와 "옆 버튼도 항상 보이게 하고 싶다"가 동시에 최고 강도로 걸리면, 둘 다 만족시킬 수 없다.

레이블을 꽉 채우기옆 버튼도 보이기레이블 하나제약 충돌(둘 다 1000)동시엔 불가

그래서 각 제약에는 우선순위가 있다. 값은 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),
        ])
    }
}

이 화면을 앱의 첫 화면으로 띄우는 연결은 앞 장에서 본 SceneDelegaterootViewController 지정이 그대로다. 스토리보드를 쓰면 이 초기 설정이 프로젝트 설정과 xib 파일 뒤에 숨는데, 코드로 하면 화면이 만들어져 붙기까지의 전 과정이 한눈에 드러난다. 처음엔 손이 많이 가지만, 무엇이 자동으로 처리되고 있었는지를 알게 되는 값이 그만큼 컸다.

코드 기반의 또 다른 이점은 협업에서 나온다. 스토리보드는 XML 한 덩어리라 여러 명이 같은 화면을 고치면 병합 충돌이 험하게 난다. 코드로 짠 UI는 일반 코드처럼 변경 지점이 또렷해 리뷰와 병합이 훨씬 수월하다.


🟣 정리

  • 절대 좌표는 기기마다 깨진다. 오토레이아웃은 좌표 대신 뷰 사이의 관계(제약)를 선언해, 실제 위치를 시스템이 계산하게 한다.
  • 제약은 앵커 문법으로 건다. 코드로 제약을 걸 뷰는 translatesAutoresizingMaskIntoConstraintsfalse로 꺼야 자동 변환과 충돌하지 않는다.
  • 상·하단 제약은 safeAreaLayoutGuide에 걸어 노치와 홈 인디케이터를 피한다.
  • 제약이 충돌하면 우선순위(1~1000)가 낮은 쪽이 양보한다. 콘텐츠 허깅과 압축 저항이 늘고 줄어드는 순서를 정한다.
  • 스토리보드 없이 코드로 짜면 화면 구성의 전 과정이 드러나고, 병합 충돌도 줄어든다.

실무에서 오토레이아웃은 매일 쓰는 도구이자 가장 흔한 버그의 진원지다. "제약 충돌 로그", "텍스트가 잘림", "뷰가 안 보임"의 대부분이 우선순위와 translatesAutoresizingMaskIntoConstraints에서 갈린다. 면접에서 "왜 이 프로퍼티를 꺼야 하는가", "우선순위로 어떻게 잘림을 푸는가"를 원리로 답할 수 있으면 오토레이아웃을 실제로 다뤄 봤다는 신호가 된다.

이제 어떤 화면에서도 깨지지 않는 배치를 잡을 수 있다. 다음 장에서는 이 배치 위에 실제 컴포넌트, 특히 목록을 그리는 테이블뷰를 올리고, 뷰컨트롤러가 비대해지지 않도록 MVC로 역할을 나누는 방법을 본다.