← 학습일지로 돌아가기
View Layout · 1단계

뷰 레이아웃 (1) — 레이아웃 사이클

뷰 레이아웃 학습 로드맵 1단계. UIKit이 화면을 그리기 전에 도는 3패스(update → layout → display)와 dirty 처리, setNeeds* vs *IfNeeded를 정리한다. 주인님 질문 + 답변 보존.

View Layout UIKit Layout Cycle 자주 나옴

큰 그림 — 매 런루프, 그리기 전에 3패스

상태가 바뀌면 즉시 다시 그리지 않고 dirty(더러움) 표시 → 런루프 끝 무렵 모아서 한 번에 처리한다(성능). 처리 순서는 3패스.

상태 변경 (constant=200 / 버튼 액션) → (자동) dirty 표시 → 런루프 끝 무렵 CoreAnimation commit 시점에 모아서 처리 ① update 패스 : updateConstraints (제약 정리, bottom-up 자식→부모) ② layout 패스 : layoutSubviews (프레임 확정, top-down 부모→자식) → 끝나면 viewDidLayoutSubviews 알림 ③ display 패스: draw(_:) (필요한 뷰만) → 렌더 서버가 화면에 그림
패스뷰 훅 / VC 훅dirty 표시 / 즉시 강제
① Update (제약)updateConstraints() / updateViewConstraints()setNeedsUpdateConstraints() / updateConstraintsIfNeeded()
② Layout (프레임) 🔴layoutSubviews() / viewDidLayoutSubviews()setNeedsLayout() / layoutIfNeeded()
③ Display (그리기)draw(_:)setNeedsDisplay()
실무 철칙

frame/bounds에 의존하는 코드는 viewDidLoad가 아니라 layoutSubviews/viewDidLayoutSubviews에서 읽어라(viewDidLoad 시점엔 frame 미확정).

Q&A

"제약을 바꾼다"가 무슨 말? heightConstraint.constant = 200 이런 거야?

네, 그게 가장 흔한 예. "제약 변경"은 여러 가지를 포함한다:

heightConstraint.constant = 200 // ① 상수 변경 (제일 흔함·쌈) someConstraint.isActive = false // ② 활성/비활성 토글 NSLayoutConstraint.activate([c1, c2]) // ③ 추가/활성화 someConstraint.priority = .defaultHigh // ④ 우선순위 변경

런타임 실무에선 ①constant 변경·②isActive 토글이 대부분.

updateViewConstraints가 snp의 makeConstraints 같은 거야? constant=200 같은 거야?

셋은 서로 다른 층위다.

정체
updateViewConstraints() / updateConstraints()레이아웃 사이클의 update 패스 훅(타이밍). 오버라이드해 그 안에 제약 코드 넣을 수 있음
snp.makeConstraints { }SnapKit DSL — 제약을 새로 생성
snp.updateConstraints { }SnapKit DSL — 기존 제약 constant 갱신 (≈ .constant = 200)
heightConstraint.constant = 200특정 제약 하나의 값 변경

updateViewConstraints()"언제"(타이밍 훅), makeConstraints"무엇"(제약 정의) → 같은 게 아니다. 실무에선 update 패스를 거의 오버라이드 안 함 — makeConstraints로 만들고 .constant =로 바꾸면 됨(대량 조건부 교체 시에만 update 패스 사용).

순서 — 상태 변경 → dirty → 런루프 끝에 처리 → viewDidLayoutSubviews로 frame 적용 → draw, 이게 맞아?

거의 정확하다. 두 군데만 다듬으면 완벽:

  • (A) 제약을 바꾸면 dirty 표시가 자동. constant = 200 하면 UIKit이 알아서 그 뷰를 "layout 필요"로 표시(직접 setNeedsLayout() 불필요)
  • (B) frame을 실제 적용하는 건 layoutSubviews(뷰 레벨, top-down). viewDidLayoutSubviews는 VC 루트뷰 레이아웃이 끝난 뒤의 알림 콜백이지 frame 적용 주체가 아님
정확한 순서

상태 변경 → (자동)dirty → 런루프 끝 commit 시 ① updateConstraints(bottom-up) → ② layoutSubviews로 프레임 확정(top-down) → viewDidLayoutSubviews 알림 → ③ draw → 렌더.

updateConstraints() / updateViewConstraints() 깊게 (2026-06-16)

둘 다 레이아웃 사이클 ① Update 패스의 훅 — "제약을 갱신하는 전용 자리". updateConstraints()UIView, updateViewConstraints()UIViewController(루트뷰 담당) 버전.

updateConstraints()updateViewConstraints()
소속UIViewUIViewController
대상그 뷰 자신의 제약VC 루트뷰(self.view)의 제약
방향bottom-up (서브뷰 → superview)VC 레벨 훅
super반드시 맨 마지막에 호출

핵심 질문 — setNeedsUpdateConstraints()가 호출되면 더티로만 만들고, 런루프 끝 레이아웃 패스에서 updateConstraints()가 발생하는 거야?

거의 정확. 두 군데만 정밀화하면 완벽.

  • setNeedsUpdateConstraints() = 더티 마킹만. 즉시 updateConstraints()를 부르지 않음 (그 뷰를 "제약 갱신 필요"로 표시 + 상위 체인/윈도우를 needs-layout 등록)
  • ✅ 그 뷰의 updateConstraints()다음 레이아웃 패스에서 실행
  • 🔧 타이밍 — "다음 런루프 사이클"보다는 지금 런루프가 한 바퀴 다 돌고 idle 직전(CATransaction commit 시점). 보통 같은 런루프 반복의 끝, 그리고 화면 그리기 직전이다.
  • 🔧 구조 — '레이아웃 패스'는 사실 3개 서브패스. updateConstraints()는 그 첫 번째이지 layoutSubviews와 같은 단계가 아니다.
런루프 끝(commit) 시 한 번에: Update Constraints // updateConstraints() — 더티인 것만, bottom-up Layout // layoutSubviews() — frame 계산, top-down Display // draw(_:) — 그리기
지금 당장 동기로 강제하려면

updateConstraintsIfNeeded() → ① 서브패스만 즉시 · layoutIfNeeded() → ①+② 전부 즉시

그럼 updateConstraints()를 오버라이드해서 거기 제약을 넣어야 해?

대부분 아니다. Apple 권고: "제자리에서 제약을 바꾸는 게 너무 느리거나, 한 사이클에 중복 변경이 많을 때만 오버라이드하라."

  • 보통은 viewDidLoad/init에서 제약 한 번 세팅 → 변하는 건 .constant 바꾸거나 .isActive 토글
  • 오버라이드하면 멱등(idempotent)해야 — 회전·trait 변경 등으로 생각보다 자주 호출됨. 매번 addConstraint하면 중복 누적 → 충돌
  • 이득: 여러 곳에서 setNeedsUpdateConstraints()를 불러도 실제 재계산은 다음 ① 패스에 한 번으로 모임(coalescing)
final class CardView: UIView { private var didSetup = false // 1회용 플래그 var isExpanded = false { didSet { setNeedsUpdateConstraints() } } // 예약만 override func updateConstraints() { if !didSetup { // ← 최초 1회만 add /* expanded/collapsed 제약 생성 */ didSetup = true } expanded.isActive = isExpanded // 매번: 토글만 (중복 없음) collapsed.isActive = !isExpanded super.updateConstraints() // ← 반드시 마지막 } }

핵심 패턴 2개 (실무 단골)

의미타이밍
setNeedsLayout()"더럽다" 표시만다음 사이클(지연, 여러 변경 합쳐 1번)
layoutIfNeeded()지금 강제 실행즉시(동기)

제약 애니메이션layoutIfNeeded의 대표 용도:

heightConstraint.constant = 200 // 값만 바꿈 (아직 화면 안 변함) UIView.animate(withDuration: 0.3) { self.view.layoutIfNeeded() // 블록 안에서 강제 배치 → 부드럽게 전환 }

"constraint 바꿨는데 애니메이션이 끊긴다/안 된다"의 정답이 이것.

다음 단계 (로드맵)

관련 문서: Auto Layout Update Cycle · UIViewController Lifecycle