뷰 레이아웃 전체 과정 심화 — update → layout → display
UIKit이 화면을 그리기까지의 전체 레이아웃 과정을, "bottom-up / top-down이 뭔지"부터 VC·뷰 훅 순서까지 처음 보는 사람도 이해하도록 아주 자세히 정리한다. Apple 공식 문서·WWDC 기반.
- Apple — Auto Layout Guide: Changing Constraints (deferred layout pass)
- WWDC15 — Mysteries of Auto Layout, Part 2 (update=bottom-up, layout=top-down 출처)
- WWDC18 — High Performance Auto Layout
- Apple Docs — layoutSubviews() · setNeedsLayout() · layoutIfNeeded()
⭐ 가장 먼저 — 더티 플래그는 3종이다 (헷갈림의 99% 원인)
setNeeds… 3개는 서로 다른 플래그·패스·메서드다. 하나가 다른 걸 자동으로 부르지 않는다. 이걸 안 헷갈리면 레이아웃의 절반은 끝.
| 트리거(예약) | set되는 더티 | 그 패스에서 불리는 것 | 하는 일 |
|---|---|---|---|
setNeedsUpdateConstraints() | needsUpdateConstraints | ① updateConstraints() | 제약 재구성(추가/제거) |
setNeedsLayout() | needsLayout | ② layoutSubviews() | frame 재배치 |
setNeedsDisplay() | needsDisplay | ③ draw(_:) | 비트맵 다시 그리기 |
- 제약 값만 바꾸면(재구성 아님) → ② layout 패스가 다시 풀어 frame만 냄.
updateConstraints()(①)는 안 불림 - 그래서 "
setNeedsUpdateConstraints가 있어야updateConstraints가 불린다"(✅참)와 "직접 바꿀 땐 그걸 안 불러도 된다"(✅참)는 모순이 아니다 — 경로가 다를 뿐 updateConstraints()를 직접 오버라이드해 거기 제약을 모아둘 때만setNeedsUpdateConstraints()를 부른다(예약). 실무 99%는 ②만 씀
→ 자세한 문답은 2026-06-16 문답 Q15·Q16 참고.
0. 먼저 "bottom-up / top-down"이 뭐냐 (이게 핵심)
뷰들은 트리(나무) 구조로 쌓여 있다. 맨 위에 UIWindow(뿌리), 그 아래 VC의 view, 그 아래 컨테이너 뷰들, 맨 아래에 라벨·버튼 같은 잎(leaf) 뷰가 있다.
| 용어 | 방향 | 쉬운 말 |
|---|---|---|
| bottom-up | 잎(자식) → 윈도우(부모) | "아래(작은 뷰)에서 위(큰 뷰)로 올라감" |
| top-down | 윈도우(부모) → 잎(자식) | "위(큰 뷰)에서 아래(작은 뷰)로 내려감" |
각 단계가 "누구를 먼저 처리하느냐"가 다르기 때문. 제약 정리는 자식부터(bottom-up), 프레임 계산은 부모부터(top-down). 이유는 3번 섹션에서 직관적으로 설명.
1. 왜 "즉시"가 아니라 "모아서(deferred)" 처리하나
제약을 하나 바꿀 때마다 즉시 다시 계산·다시 그리면 같은 프레임에 수십 번 재계산이 일어난다. 그래서 UIKit은:
Apple 표현: "the deferred layout pass actually involves two passes through the view hierarchy" — 지연된 레이아웃은 뷰 계층을 여러 번 훑는 패스로 이뤄진다. 우리가 보통 말하는 3패스(아래)다.
1.5 Core Animation & 렌더 파이프라인 (commit의 정체)
Core Animation이란 / CALayer와의 관계
Core Animation(QuartzCore) = 화면 콘텐츠를 합성(compositing)해서 그리고 애니메이션하는 엔진. 이름과 달리 본질은 렌더링 시스템이며 UIKit 아래에 깔려 있다.
- CALayer = Core Animation이 다루는 기본 단위. 비트맵(
contents), 기하(frame/bounds/transform), 시각속성(backgroundColor/cornerRadius/opacity)을 가진 가벼운 객체 - 모든 UIView 뒤엔 CALayer가 있다(
view.layer). UIView = layer(그리기) + 이벤트 처리. 실제 그리기·합성은 layer가 함
"화면이 레이어 트리로 표현된다"
뷰가 계층(뷰→서브뷰)을 이루듯 레이어도 평행한 트리(layer→sublayer)를 이룬다. 화면 = 이 CALayer 트리를 겹쳐 합성한 결과. (내부적으론 model/presentation/render 3 트리가 있으나, "보이는 건 레이어 트리 합성 결과"만 알면 충분)
implicit CATransaction
CATransaction = 레이어 변경들을 묶어 한 번에 적용하는 단위. 내가 begin()/commit()으로 직접 묶으면 explicit, 안 묶고 그냥 속성을 바꾸면 시스템이 현재 런루프용 트랜잭션을 자동 생성해 런루프 끝에 commit하는 게 implicit. (독립 CALayer는 이때 기본 0.25초 암묵적 애니메이션도 붙음)
commit 4단계 (Core Animation Pipeline)
| 단계 | 하는 일 | 주체 |
|---|---|---|
| ① Layout | needs-layout 뷰의 layoutSubviews 호출, Auto Layout 풀어 frame 확정 | 앱(CPU) |
| ② Display | draw(_:) 오버라이드한 뷰만 콘텐츠를 비트맵(backing store)에 그림 | 앱(CPU) |
| ③ Prepare | 이미지 디코딩, GPU 텍스처 변환 등 렌더 준비 | 앱 |
| ④ Commit | 레이어 트리+애니메이션을 인코딩(패키징)해 Render Server로 IPC 전송 → 앱 일 끝 | 앱 |
commit 이후 애니메이션 매 프레임 보간은 Render Server(GPU)가 한다. 그래서 commit만 끝나면 앱 메인스레드가 잠깐 바빠도 애니메이션은 부드럽게 굴러간다. (출처: WWDC14 Advanced Graphics and Animations for iOS Apps)
"한 런루프"는 어떻게 판단? viewDidLoad 코드는 모두 한 런루프?
런루프 = 메인 스레드가 이벤트를 처리하고 없으면 자는 무한 루프. 한 바퀴 = 깨어남 → 이벤트 처리 → (자기 직전) 옵저버 발동 = Core Animation commit → 잠.
- viewDidLoad의 동기 코드 = 같은 런루프 한 바퀴. 그 코드가 끝나야 제어가 런루프로 돌아가고, 그제서야 commit(layout/display)이 일어남 → 줄마다 즉시 반영 아님
- 비동기는 다른 바퀴:
DispatchQueue.main.async/Task/ 완료 핸들러 / 네트워크 콜백은 나중 런루프 바퀴에서 실행 - viewDidLoad에서 즉시 frame이 필요하면
view.layoutIfNeeded()로 commit을 기다리지 않고 지금 layout 강제
1.6 ⭐ "그리기"는 두 번 일어난다 (draw vs render server)
가장 헷갈리는 지점. draw(앱) 와 render server(GPU 합성) 는 서로 다른 그리기다.
| ① Display 단계 "draw" | ② Render Server "합성" | |
|---|---|---|
| 누가 | 앱 (CPU, Core Graphics) | 별도 프로세스 (GPU) |
| 뭘 | 각 레이어가 자기 내용을 자기 비트맵에 그림 | 그 비트맵들을 겹쳐 최종 화면 한 장으로 |
| 결과 | 메모리 속 그림(화면 아님) | 실제 화면 픽셀 |
① 배우들이 각자 투명필름에 자기 그림을 그린다(draw) → ② 사진사가 필름들을 겹쳐 한 장으로 합성한다(render server). 화면에 보이는 건 ②.
backgroundColor만 있는 뷰 → ①(draw) 안 함, ②에서 GPU가 색 사각형만 칠함draw(_:)오버라이드(커스텀 드로잉) → ① + ②- → draw 안 하는 게 더 가볍다(backing store 비트맵 안 만듦)
commit vs dirty (반대 개념)
- dirty 표시 = "할 일 있음" 마킹(
setNeedsLayout/setNeedsDisplay) — 변경하는 그 순간 - commit = 런루프 끝에 dirty들을 실제 처리(Layout→Display→Prepare)하고 render server로 전송 — 나중
- → dirty(표시) 먼저, commit(처리+전송) 나중. commit은 dirty를 푸는 단계
"draw=그린다"는 맞지만 "화면에"가 아님
Layout=frame(숫자) 확정 → Display(draw)=그 크기의 비트맵에 내용을 그림(메모리 속) → 화면 반영은 그 뒤 render server 합성 → VSync. "자기 비트맵에 그린다"이지 "화면에 그린다"가 아니다.
layer.contents가 뭐냐
layer.contents = 그 레이어가 보여줄 비트맵(이미지, 보통 CGImage). draw로 만든 비트맵, 또는 UIImageView의 이미지가 여기 담겨 render server가 텍스처로 합성.
IPC 더 자세히
- 앱·render server는 메모리 분리된 별도 프로세스 → 직접 공유 불가
- mach message: 레이어 트리 구조·명령(위치·opacity 등) 전달
- IOSurface(공유 메모리): 큰 픽셀 데이터(비트맵)는 복사하면 비싸니 공유 버퍼에 두고 render server가 같은 메모리를 GPU 텍스처로 참조
- → "구조/명령은 mach message, 픽셀은 IOSurface 공유메모리"로 효율 전달 (XPC도 IPC의 일종)
VSync & ProMotion
- VSync: 디스플레이가 고정 주기로 새로고침하는 타이밍 신호(60Hz=16.7ms). 렌더 프레임을 이 타이밍에 교체(중간 교체=화면 찢김). 마감 초과=프레임 드롭=버벅임
- ProMotion: 애플 적응형(가변) 주사율 디스플레이(iPhone Pro/iPad Pro). 10~120Hz 자동 조절 — 스크롤/애니는 120Hz로 부드럽게, 정지 화면은 낮춰 배터리 절약. VSync 주기가 가변
backing layer
뷰를 뒤에서 받쳐주는(backing) 단 하나의 CALayer = view.layer. 뷰의 시각적 실체(그리기·합성 담당). 뷰당 1개.
layoutIfNeeded() 다음 줄은 갱신된 frame?
네. layoutIfNeeded()는 그 자리에서 동기적으로 layout 패스를 실행하므로 직후 frame이 갱신됨.
2. 3패스 상세
① Update Constraints 패스 — 제약 정리 (bottom-up)
- 방향: bottom-up (잎 뷰 → 윈도우). WWDC15: "every view that needs it will receive
updateConstraints, and that runs from the leaf-most views up toward the window." - 호출 메서드: 뷰
updateConstraints()/ 뷰컨트롤러updateViewConstraints() - 트리거:
setNeedsUpdateConstraints()(dirty 표시, 지연) /updateConstraintsIfNeeded()(즉시 강제) - 하는 일: 이 단계에서 제약을 추가·갱신할 수 있음. "the update pass updates the constraints, as necessary."
Apple 권고 직접 인용: "It is almost always cleaner and easier to update a constraint immediately after the affecting change has occurred. Deferring these changes to a later method makes the code more complex and harder to understand." → updateConstraints() 오버라이드는 ① 제약 변경이 너무 느릴 때 ② 같은 제약을 중복 변경할 때 ③ 성능 측정으로 필요성이 확인됐을 때만. 평소엔 그냥 변경 시점에 constant = / isActive = 하면 됨.
② Layout 패스 — 프레임 확정 (top-down) 🔴
- 방향: top-down (윈도우 → 잎). WWDC15: "this runs the opposite direction, starting from the window going down toward the leaves."
- 호출 메서드: 뷰컨트롤러
viewWillLayoutSubviews()→ 뷰layoutSubviews()→ 뷰컨트롤러viewDidLayoutSubviews() - 트리거:
setNeedsLayout()(dirty, 지연) /layoutIfNeeded()(즉시 강제) - 하는 일: Auto Layout 엔진이 제약을 풀어 계산한 사각형으로 각 서브뷰의 frame을 실제로 세팅. Apple: "By default, the
layoutSubviewsmethod updates the frame of each subview with the rectangle calculated by the Auto Layout engine."
frame은 이 패스에서 확정되므로 viewDidLoad가 아니라 layoutSubviews / viewDidLayoutSubviews에서 읽어야 정확하다.
③ Display 패스 — 그리기 (top-down)
- 방향: top-down
- 호출 메서드: 뷰
draw(_ rect:) - 트리거:
setNeedsDisplay()/setNeedsDisplay(_ rect:) - 하는 일: Core Graphics로 뷰 내용을 그림.
draw(_:)를 오버라이드한 뷰만 해당(비용 큼 → 필요할 때만).
"제약 정리=bottom-up, 레이아웃=top-down"은 WWDC15 Part 2의 공식 설명이다. 일부 문서 요약은 이를 모호하게 적지만, Apple 세션의 정확한 표현은 위와 같다.
3. 왜 update는 bottom-up, layout은 top-down인가 (직관)
부모의 크기는 종종 자식의 내용 크기(intrinsic content size)에 의존한다. 예: 스택뷰 높이는 그 안의 라벨 높이 합. 그래서 자식들의 제약 정보를 먼저 모아 올라가야 부모가 완전한 정보로 제약을 정리할 수 있다. → 잎 → 윈도우.
자식의 frame(위치·크기)은 부모의 frame(원점·크기)이 정해져야 계산된다. 부모 안 어디에 놓일지는 부모 크기를 알아야 하니까. → 윈도우 → 잎.
한 줄: "크기 정보는 아래에서 위로 모으고(update), 위치 배치는 위에서 아래로 내린다(layout)."
4. 전체 순서 타임라인 (VC 훅 포함)
A. 최초 화면 표시
B. 이미 화면에 있는 상태에서 제약 변경 (예: constant = 200)
viewWillLayoutSubviews→layoutSubviews(frame 확정) →viewDidLayoutSubviews순서로 VC 훅이 뷰의 layoutSubviews를 감싼다- 4~7번 레이아웃 패스는 한 번이 아니라 여러 번 호출될 수 있다(회전·제약 변경·스크롤 등) → 무거운 작업 넣지 말 것
5. setNeeds* vs *IfNeeded
| 메서드 | 의미 | 타이밍 |
|---|---|---|
setNeedsUpdateConstraints() | 제약 갱신 필요 표시 | 다음 사이클(지연) |
updateConstraintsIfNeeded() | 제약 갱신 즉시 | 즉시(동기) |
setNeedsLayout() | "다시 배치해" 표시 | 다음 사이클(지연, 여러 변경 합쳐 1번) |
layoutIfNeeded() | 지금 배치 | 즉시(동기) |
setNeedsDisplay() | "다시 그려" 표시 | 다음 사이클(지연) |
Apple: "setNeedsLayout() invalidates the current layout ... triggers a layout update during the next update cycle. layoutIfNeeded() lays out the subviews immediately, if layout updates are pending."
대표 용도 — 제약 애니메이션
"제약 바꿨는데 애니메이션이 끊긴다/안 된다"의 정답이 이것. (값 변경을 블록 밖에서, layoutIfNeeded()를 블록 안에서)
6. 실무 함정 체크
viewDidLoad에서view.frame/bounds읽기 ❌ → 미확정.viewDidLayoutSubviews에서 읽어라layoutSubviews/viewDidLayoutSubviews에 네트워크·무거운 연산 넣기 ❌ → 여러 번 호출됨layoutSubviews안에서 또setNeedsLayout유발 ❌ → 무한 레이아웃 루프- 제약 변경 후 즉시 frame 읽기 → 아직 반영 전. 즉시 필요하면
layoutIfNeeded()호출 후 읽기 draw(_:)불필요하게 오버라이드 ❌ → display 패스 비용. 대부분 레이어/서브뷰로 해결
FAQ (세부 질문)
| 질문 | 답 |
|---|---|
| layoutSubviews와 layoutSublayers는 각각 따로? | 연결됨. layer가 layoutSublayers 처리 시 delegate(=UIView)의 메서드를 부르고, 그게 layoutSubviews()를 호출. 뷰는 layoutSubviews만 오버라이드 |
| "frame 확정"은 그리는 것? | 아님. frame(원점+크기 숫자)만 계산. 픽셀 그리기는 Display/Render 단계 |
| draw 오버라이드 안 하면 비트맵에 안 그려? | "내가 CPU로 커스텀 비트맵 만드는 일을 안 함". backgroundColor 등은 GPU가 직접, UIImageView는 layer.contents에 직접 꽂음. 둘 다 없으면 backing store 자체가 없을 수 있음(더 쌈) |
imageView.image =의 디코딩도 prepare? | 대체로 그렇다. 압축이미지 디코딩은 표시 직전 prepare/commit에서 메인스레드에 발생(그래서 큰 이미지는 백그라운드 사전 디코딩/다운샘플) |
| GPU 텍스처 변환? | 디코딩된 비트맵(CPU 메모리)을 GPU 메모리에 GPU용 형식으로 올리는 것. layer.contents가 텍스처가 되어 합성됨 |
| IPC? | Inter-Process Communication. 앱과 Render Server는 다른 프로세스라 OS 메커니즘(mach msg/공유메모리/XPC)으로 데이터 전달 |
| VSync? | 화면 갱신 신호. 60Hz=~16.7ms, 120Hz=~8.3ms마다. 렌더를 여기 맞춰 표시(찢김 방지). 마감 넘기면 프레임 드롭=버벅임 |
| commit 전엔 그리기 전? | 맞다. commit 전엔 모델 상태만 변경, 화면엔 이전 프레임 유지. commit→렌더→VSync에서 교체 |
| QuartzCore? | Core Animation이 실제로 담긴 프레임워크(CALayer/CAAnimation/CATransaction). "Core Animation"은 개념명, "QuartzCore"는 프레임워크명 |
| UIView 뒤 CALayer는 하나만? | backing layer는 1:1로 하나(view.layer). 단 그 아래 서브레이어 여러 개 추가 가능, layerClass로 종류 변경 가능 |
| 비트맵 / 텍스처? | 비트맵=픽셀 격자(CPU 메모리 이미지). 텍스처=그 비트맵을 GPU 메모리에 올린 GPU판 이미지 |
다음 런루프면 차이 적은데 왜 layoutIfNeeded()? | 시간 절약이 아니라 ① frame을 같은 코드에서 즉시 읽어야 할 때 ② 애니 시작점을 현재 상태로 확정해야 할 때 ③ 한 프레임 깜빡임 방지. 평소엔 안 부름(batching이 효율적) |
| Core Graphics = Core Animation? | ❌ 다름. Core Graphics(Quartz 2D)=CPU로 비트맵에 그리는 드로잉 도구(draw에서 사용). Core Animation(QuartzCore)=레이어 합성·애니 엔진(GPU). CG로 그린 비트맵을 CA의 layer.contents로 합성 |
| 각 레이어의 비트맵은 CALayer에 있나? | ✅ 뷰가 그린 비트맵(backing store) = 그 뷰의 layer.contents |
| 텍스처 변환 = GPU용 메모리 구조로 바꾸는 것? | ✅ 비트맵(CPU 메모리)을 GPU 메모리에 올리고 GPU용 포맷/배치로 맞추는 것 |
| commit에서 dirty 표시? Layout/Display에서 frame 할당? | ⚠️ 순서 반대. dirty 표시는 commit 전 네 코드에서 즉시. commit은 dirty를 처리. 또 입력(constant)과 결과(frame) 구분: constant 변경=입력+dirty(즉시), frame 할당은 commit의 Layout 패스(나중) |
| setNeedsLayout = layout/display 생략하고 commit만? | ❌ 생략 아님 예약. "다음 commit 때 layout 패스 돌려라" 마킹. commit은 단계가 아니라 런루프 끝에 layout·display·전송을 다 하는 이벤트. display는 setNeedsDisplay가 따로 예약 |
| CALayer가 CGImage 타입? contents는 보여줄 내용? | CALayer는 CGImage 아님(레이어 객체). layer.contents 프로퍼티가 보통 CGImage(보여줄 비트맵). contents=그 레이어가 보여줄 내용 ✅ |
draw(앱)가 각 layer.contents(비트맵)를 만든다. render server는 그 contents들을 가져다 겹쳐 최종 화면 프레임버퍼를 만든다. → contents=합성의 입력, render server 결과=화면(contents 아님).
layer.contents = 각 레이어 개별 그림(합성 재료). 합성 결과물(화면 프레임버퍼)과는 다름.
setNeedsLayout→layoutSubviews(frame 재배치)만 /setNeedsDisplay→draw(_:)(내용 다시 그리기)만 — 별개 dirty 플래그- 위치/크기만 바뀌면(layout) 비트맵은 그대로 두고 render server가 그 텍스처를 새 위치에 다시 합성만 → draw 불필요
- draw가 필요한 건 "내용 자체가 바뀔 때"(커스텀 draw가 의존하는 상태 변경). 이동만 하는데 매번 다시 그리면 낭비라 분리
- 예외:
contentMode = .redraw면 bounds 변경 시 자동 setNeedsDisplay. 기본값에선 layout ≠ display
런루프 끝 전체 이벤트 = "commit transaction". 그 안 단계 = Layout→Display→Prepare→Commit(마지막 단계도 Commit=패키징+전송). 큰 commit(이벤트) ⊇ 작은 Commit(4단계).
dirty(표시) 먼저, frame 할당·그리기는 나중(commit). 바꾸는 건 입력(constant), frame은 commit에서 계산.
📅 2026-06-16 문답 — 더티 등록·layout 패스·framebuffer·VSync
"view에서 setNeedsUpdateConstraints가 호출되면 bottom-up으로 부모뷰·VC에서도 setNeedsUpdateConstraints가 호출되고, 런루프 마지막 layout 패스에서 이 뷰들·VC를 더티로 만들고 display에서 draw. 이는 CPU로 비트맵에 그린 것이고 IOSurface로 GPU 메모리 구조로 바꿔 GPU가 비트맵을 그린다."
- ❌ 전파되는 건
setNeeds…가 아니다.setNeedsUpdateConstraints()는 그 뷰 하나만 더티 표시 + 윈도우에 패스 예약. 부모의setNeeds…를 연쇄 호출하는 게 아님 - ❌ VC엔
setNeedsUpdateConstraints가 없다(UIView 메서드). update 패스에서updateViewConstraints()가 호출됨 - ❌ 인과 반대. 더티는
setNeeds…시점에 이미 만들어진 것. layout 패스는 더티를 만드는 게 아니라 소비·클리어 - ❌ 레이아웃 패스 = 3서브패스(① update → ② layout → ③ display). 주인님 정리엔 ②(frame 확정)가 빠짐
- ❌ GPU는 "그리는" 게 아니라 "합성(compositing)"한다. 비트맵 그리기(draw)는 CPU, 최종 화면 구성은 render server + GPU 합성
- 🔧 "isc" → IOSurface. 빠진 용어: backing store / render server / CATransaction commit / 프레임버퍼 / VSync
교정한 전체 흐름 (A → B → C)
용어 교정 표 (내 표현 → 정확한 용어)
| 내(주인님) 표현 | 정확한 용어 |
|---|---|
| "isc" | IOSurface (CPU·GPU가 공유하는 메모리 버퍼) |
| (뷰가 들고 있는) "비트맵" | backing store / backing layer (CALayer의 contents) |
| "GPU가 비트맵을 그린다" | GPU가 텍스처를 합성(compositing)한다 |
| (빠짐) | render server(별도 프로세스), CATransaction commit, 프레임버퍼, VSync |
2. "윈도우를 다음 update 사이클에 등록"이 무슨 말? 뷰를 등록하는 게 아니라?
둘 다 일어난다. 역할이 다르다.
- 뷰: 그 뷰에 "제약 갱신 필요" 더티 플래그 ON → 어떤 뷰를 처리할지
- 윈도우(트리 루트)/런루프: "이번 런루프 끝(commit)에 레이아웃 패스를 한 번 돌려라" 예약 → 언제 트리거할지
윈도우 등록 = 그 뷰 대신 윈도우를 처리한다는 뜻이 아니라 레이아웃 패스를 깨우는 스케줄. 패스가 돌 때 시스템은 윈도우(루트)부터 트리를 순회하며 더티 뷰를 찾아 처리. → 진입점=윈도우, 대상=더티 뷰.
3. 더티 표시하면 update 패스 때 그 뷰의 updateConstraints가 호출되고, 부모뷰까지 호출돼? VC는 updateViewConstraints?
순회 방향은 맞고, "무조건 연쇄 호출"은 아니다.
- update 패스는 더티로 마킹된 뷰의
updateConstraints()를 bottom-up(자식→부모→윈도우) 순서로 호출 - 부모의
updateConstraints()는 그 부모도 더티일 때 호출. 모든 부모가 매번 자동으로 불리는 건 아님 — "bottom-up"은 호출 순서가 자식→부모 방향이라는 뜻 - ✅ VC는 루트뷰 레벨에서
updateViewConstraints()호출 (super는 맨 마지막)
4. layout 패스 단계를 자세히
레이아웃 패스(②) = frame(위치·크기) 확정 단계. 그리기 아님.
- 더티 표시(
setNeedsLayout)된 뷰에layoutSubviews()호출 - 방향: top-down(부모→자식). 부모가 bounds 안에서 자식 frame을 정하고, 그 다음 자식이 자기 자식 배치
- Auto Layout 사용 시:
layoutSubviews()기본 구현이 엔진(NSISEngine, Cassowary)이 푼 제약 해를 각 서브뷰frame에 적용 - 커스텀:
layoutSubviews()오버라이드해 직접 frame 세팅(super호출 후) - VC 훅:
viewWillLayoutSubviews()→ (루트뷰layoutSubviews) →viewDidLayoutSubviews() - frame 의존 작업(
cornerRadius·그림자 path)은 여기서. 강제=layoutIfNeeded()/ 예약=setNeedsLayout() - ①과의 관계: ①에서 제약 갱신·수집 → 엔진 solve → ②에서 결과 frame 적용
5. 프레임버퍼(framebuffer)가 뭐야?
GPU가 합성을 끝낸 "화면 한 장"(픽셀 배열)이 저장되는 메모리 영역.
- 한 프레임 전체의 픽셀 색 데이터(RGBA per pixel)
- 디스플레이 컨트롤러가 이걸 읽어 패널에 출력
- 더블/트리플 버퍼링: GPU가 한 버퍼에 그리는 동안 디스플레이는 다른 버퍼를 읽음 → 테어링 방지, VSync에 버퍼 swap
layer.contents(레이어별 비트맵) = 합성 재료 · 프레임버퍼 = 합성 결과(최종 화면)6. layout 패스에서 CPU로 CALayer 콘텐츠(비트맵)에 그리는 거야?
❌ 그리기는 layout 패스(②)가 아니라 display 패스(③)다.
- ② layout = frame(숫자) 확정, 그리기 아님
- ③ display =
draw(_:)시점에 CPU가 Core Graphics로 backing store(비트맵 =layer.contents)에 그림 draw(_:)를 오버라이드한 뷰만 CPU 비트맵 그리기.backgroundColor·UIImageView.image등은 draw 없이 GPU 직접/layer.contents로 처리
→ 주인님 문장의 "layout 패스에서" → "display 패스에서"로 고치면 정확.
7. VSync 설명
디스플레이가 다음 프레임을 화면에 표시하기 시작하는 경계 신호(수직동기).
- 주사율대로 갱신: 60Hz=16.7ms, 120Hz(ProMotion)=8.3ms마다
- 이 경계에 맞춰 GPU 프레임버퍼 swap → 화면 중간에 두 프레임 섞임(테어링) 방지
- iOS: render server가 VSync에 맞춰 합성·표시.
CADisplayLink도 VSync에 콜백 동기화(애니 타이밍 정석) - 한 프레임 작업이 간격(16.7ms) 안에 못 끝나면 프레임 드롭 = 버벅임(jank)
8. VC에 v1, 그 안에 v2가 있고 v2에서 setNeedsUpdateConstraints 호출. layout 패스 순서가 viewWillLayoutSubviews → v1.layoutSubviews → v2.layoutSubviews → viewDidLayoutSubviews 인가?
거의 맞고 두 가지만 보강. 계층: VC.view(루트) → v1 → v2
(1) 그 전에 ① update constraints 패스가 먼저 (bottom-up, 더티인 것만):
(2) 그 다음 ② layout 패스 (top-down) — 빠진 건 루트뷰(VC.view) 자신의 layoutSubviews가 v1보다 먼저:
viewWillLayoutSubviews/viewDidLayoutSubviews는 루트뷰 레이아웃을 감싸는 알림이고, v1·v2의 layoutSubviews는 will~did 사이에 일어난다. top-down이라 부모(v1)가 자식(v2)보다 먼저 — 이 부분은 정확.
9. layoutIfNeeded()는 그 시점에 더티 표시된 애들 모두에 updateConstraints가 호출돼? 해당 뷰만?
해당 뷰만이 아니다. layoutIfNeeded()는 receiver에서 조상 체인을 따라 올라가 가장 위의 더티 조상부터 그 아래 서브트리 전체의 pending 레이아웃을 즉시 수행(보통 윈도우 트리 단위).
- layout을 하려면 제약이 최신이어야 하므로 pending update constraints도 함께 풀린다 → 그 트리의 더티 뷰들
updateConstraints()가 같이 호출 - "receiver 한 뷰만" ❌ / "그 뷰가 속한 트리의 더티한 것 전부" ✅
- 딱 그 뷰 서브트리 제약만 즉시 풀려면
updateConstraintsIfNeeded()
10. updateConstraints()에서는 무슨 일을 하나?
"이 뷰의 제약 집합을 최신 상태로 만드는 것" — 그게 전부.
- 제약 생성 /
activate·deactivate/constant·priority변경 / 동적 제약 갱신 super.updateConstraints()를 맨 마지막 호출- ⚠️ 여기서 frame을 계산하지 않는다. 제약을 푸는(solve) 건 Auto Layout 엔진, 실제 frame 배치는 ② layout 패스. updateConstraints는 그 입력(제약)을 준비하는 자리
11. draw를 오버라이드 안 한 애 — 예: 뷰 색깔 변경도 GPU가 직접 layer.contents에 그리나?
아니다. backgroundColor는 layer.contents(비트맵)에 그리는 게 아니다.
- 배경색은
layer.backgroundColor(CGColor) 속성으로 저장 → render server/GPU가 합성 때 그 사각형을 단색으로 채운다(fill) - 비트맵(contents) 생성이 없다 → CPU draw 없음, 메모리 거의 안 씀(제일 쌈)
layer.contents에 실제 비트맵이 들어가는 건:draw(_:)오버라이드(CPU가 그린 결과),UIImageView(이미지를 contents에 꽂음), 텍스트 렌더 등- 정리: 색 변경 = "그리기" 아니라 속성 변경 → GPU가 합성 때 칠함
12. 한 VC의 viewDidLoad에 UI 코드가 엄청 많을 때, 그게 layout 패스·GPU 렌더를 VSync 내에 못 하면 버벅인다는 거야?
핵심은 "코드 줄 수"가 아니라 "메인 스레드가 한 프레임 예산(16.7ms)을 넘기느냐".
viewDidLoad는 메인 스레드에서 동기 1회 실행. 무거운 동기 작업(큰 이미지 디코딩, 동기 디스크/네트워크, 대량 Auto Layout)을 하면 → 첫 프레임/화면 전환이 마감을 못 맞춰 끊김(hitch)- 이건 진입 시 일시적 hitch. 지속 버벅임은 스크롤·애니 중 매 프레임 layout/draw가 무거울 때
- 예산은 두 군데가 따로: CPU(메인) 이벤트+layout+draw+commit이 16.7ms 초과 → 드롭 / GPU 합성이 무거우면(오프스크린 렌더·과한 블러·그림자·마스크) → 드롭
- 해법: 무거운 작업을 백그라운드/지연(lazy)/사전 디코딩·다운샘플로 빼고 viewDidLoad는 가볍게
13. 이해가 안 됨 — setNeedsUpdateConstraints가 더티 표시하고 layout 패스에서 frame도 set하는데, updateConstraints가 뭐하러 필요해?
직관이 맞다 — 대부분 필요 없다. 핵심은 "제약"과 "frame"이 다른 일이라는 것.
| 정체 | 누가 만드나 | |
|---|---|---|
| 제약(constraint) | "규칙" (v2.top = v1.top + 20) | 사람이 코드로 만들어야 함 |
| frame | 규칙을 푼 "결과 숫자" | 엔진이 자동 계산 (layout 패스) |
layout 패스는 제약을 만들지 않는다. 이미 있는 제약을 풀어서(solve) frame을 낼 뿐. 제약은 보통 viewDidLoad에서 미리 만들어둔다.
updateConstraints=레시피 고치는 시간 / layout 패스=레시피대로 요리하는 시간. 레시피(제약) 안 바꾸면 고치는 시간 불필요 — 요리(layout)만 하면 됨.그럼 언제 쓰나? 제약 자체가 동적으로 통째로 바뀌고(가로/세로·확장/축소로 제약 세트 교체) 그 재구성이 비쌀 때만. 이유 둘:
- 배칭 — 한 런루프에 상태가 5번 바뀌어
setNeedsUpdateConstraints가 5번 불려도 실제 재구성은 update 패스에서 딱 1번. 매번 즉시activate/deactivate하면 같은 일 5번=낭비 - 제약 추가/제거는 비싸다 —
.constant변경보다 무거움(엔진이 제약 그래프 재구성). 자주 토글되면 모아서가 이득
→ 제약 고정이면 updateConstraints 불필요. 자주 재구성 + 비쌀 때만 "모아서 처리하는 최적화 자리".
14. 동기 비용이 길다 = 메인 스레드의 layout 패스(update·layout·draw)와 GPU 렌더가 VSync에 못 맞춰 버벅이니 동기 비용을 줄이자는 거야?
맞다. 메인 스레드는 매 프레임 런루프 한 바퀴를 동기로 돈다:
이 전부가 16.7ms(60Hz) 안에 안 끝나면 다음 VSync에 새 프레임을 못 줘서 프레임 드롭=버벅임. 그래서 이 동기 비용을 줄이자는 게 맞다.
정밀화 2가지:
- "그리는 작업"만이 아니다 — draw뿐 아니라 네 일반 코드·제약 solve·layout 계산 전부가 메인 스레드 동기 비용 (draw=CPU Core Graphics도 메인)
- GPU 렌더는 메인 스레드가 아니다 — 합성은 render server/GPU에서 별도로 돈다. 예산이 두 군데 따로:
- CPU(메인): 네 코드+update/layout/draw+commit이 16.7ms 초과 → 드롭
- GPU: 합성이 무거우면(오프스크린·블러·그림자) VSync 못 맞춰 → 드롭
- 둘 중 하나라도 늦으면 버벅
→ 주로 줄이려는 건 메인 스레드(CPU) 동기 비용이고 그게 핵심 맞음. GPU 쪽도 따로 예산이 있다는 것만 추가.
15. 버튼 눌렀을 때 a의 오토레이아웃을 변화시키면, 그때 setNeedsUpdateConstraints가 호출되는 거 아냐?
보통은 아니다. 흔한 오해. 핸들러에서 제약을 직접 바꾸면 UIKit이 자동으로 더티 표시한다 — setNeedsUpdateConstraints()를 직접 부를 필요 없음.
constant = / isActive = / activate·deactivate → 전부 자동 더티 → 다음 layout 패스에서 반영. 실무 99%는 그냥 바꾸고 끝.
setNeedsUpdateConstraints를 직접 부르는 건 — updateConstraints()를 오버라이드한 경우에만:
| 방식 | setNeedsUpdateConstraints? |
|---|---|
핸들러에서 직접 constant=·isActive= 변경 (흔함) | 불필요 (자동 더티) |
updateConstraints() 오버라이드해서 거기서 변경 | 필요 (핸들러는 예약만) |
→ 버튼 → a 제약 변경은 보통 직접 바꾸면 되고 setNeedsUpdateConstraints는 안 부른다. 그건 Q13의 "제약을 updateConstraints에 모아 처리하는 특수 패턴"일 때만 등장.
16. "setNeedsUpdateConstraints가 호출돼야 updateConstraints가 불린다"면서, 직접 바꾸면 안 불러도 된다니 모순 아냐?
모순 아니다. 더티 플래그가 2개라서 그렇다 — Q15에서 내가 "자동 더티"라고만 써서 어느 더티인지 모호했던 게 원인.
| 트리거 | set되는 더티 | 그 패스에서 불리는 것 |
|---|---|---|
setNeedsUpdateConstraints() | needsUpdateConstraints | ① updateConstraints() |
setNeedsLayout() | needsLayout | ② layoutSubviews() |
이 둘은 별개 플래그·별개 패스·별개 메서드. "setNeedsUpdateConstraints가 있어야 updateConstraints가 불린다"는 ✅ 맞다(①을 깨우는 조건). 그런데 constant 변경은 ① 경로를 안 탄다.
왜 ①을 안 거쳐도 되나? updateConstraints()는 "제약을 새로 만들/추가·제거·재구성"하는 자리. constant=는 기존 제약의 값만 바꾼 것(재구성 아님) → 엔진이 ② layout 패스에서 다시 풀어 frame만 내면 끝.
| 상황 | 어떤 더티 | updateConstraints 불림? |
|---|---|---|
setNeedsUpdateConstraints() 직접 호출 | needsUpdateConstraints (①) | ✅ 불림 |
constant=·isActive= 직접 변경 | needsLayout (②) | ❌ 안 불림 (layoutSubviews만) |
더티 표시(setNeeds…, 전파 아님) → 레이아웃 패스 3서브패스(update bottom-up → layout top-down → display) → CATransaction commit으로 render server에 IPC → GPU 합성 → 프레임버퍼 → VSync에 디스플레이 출력. "그리기는 두 번": draw=CPU 비트맵 생성, render=GPU 합성.
setNeedsUpdateConstraints→updateConstraints()(①, 제약 재구성) · setNeedsLayout→layoutSubviews()(②, frame) · setNeedsDisplay→draw(_:)(③, 그리기). constant=는 ②만 자동, draw 의존 상태 변경은 ③.
📝 복습노트
2026-06-18 — 내 정리 채점 (82/100)
시나리오: VC.view에 a1·a3, a1 안에 a2. a2.setNeedsLayout() 호출 시 전체 흐름.
a2에 setNeedsLayout이 호출되면 필요에 따라 부모뷰(a1)·VC.view에 setNeedsLayout이 호출되고(a3는 X), 런루프 끝 레이아웃 패스에서 더티된 뷰들에 대해 VC.viewWillLayoutSubviews → VC.view.layoutSubviews → a1 → a2 → VC.viewDidLayoutSubviews 순으로 호출(=frame set). 그다음 부모뷰부터 draw override한 곳에서 top-down으로 draw(=CPU가 layer.contents 비트맵에 그림). 그리고 GPU의 render server가 VSync에 맞게 화면에 그린다.
✅ 맞은 것
- 레이아웃 패스 순서
viewWillLayoutSubviews → VC.view → a1 → a2 → viewDidLayoutSubviews(top-down) - a3는 호출 안 됨(더티 아님) ·
layoutSubviews=frame set · draw는 top-down·override한 곳만·CPU가 비트맵에
🔧 교정 4가지
- "부모뷰에 setNeedsLayout이 호출된다" ❌ (가장 중요) — 전파되는 건
setNeeds…호출이 아니다.a2.setNeedsLayout()은 a2만 더티 + 윈도우에 패스 예약. 조상의 setNeedsLayout을 줄줄이 부르지 않음. (오타: "부모뷰인 a2"→a2의 부모는 a1) - 조상 layoutSubviews는 "조상도 더티일 때만" —
a2.setNeedsLayout()은a2.layoutSubviews()만 보장. VC.view·a1의 layoutSubviews(및 viewWillLayoutSubviews)는 그들도 무효화됐을 때만. 제약 변경이 상위에 영향 주면 윈도우 레벨로 무효화돼 조상도 도는 경우가 많을 뿐 - layout 다음 draw가 "항상" 오는 건 아니다 —
setNeedsLayout은 frame만 무효화. draw(③)는needsDisplay가 따로 켜졌을 때만. 위치·크기만 바뀌면 비트맵 그대로 두고 render server가 기존 텍스처를 새 위치에 재합성만(draw 불필요). layout ≠ display (예외:contentMode = .redraw) - "GPU의 render server" → 용어 2개: render server는 GPU 일부가 아니라 별도 프로세스("GPU를 써서" 합성). 빠진 단계 = CATransaction commit으로 레이어 트리를 IPC로 render server에 전달 → GPU로 합성(compositing) → 프레임버퍼 → VSync 출력. "그린다"보다 "합성"
a2만 더티 표시(전파 아님) → 패스에서 더티인 뷰만 layoutSubviews(frame) → 내용 바뀐 것만 draw(CPU 비트맵) → commit으로 render server(별도 프로세스)에 IPC → GPU 합성 → 프레임버퍼 → VSync.
2026-06-18 — 정확한 전체 순서 (a2.setNeedsLayout부터 화면까지)
시나리오: VC.view ▸ [a1 ▸ a2] + a3. a2.setNeedsLayout() 호출 시.
2026-06-19 — 세부 6문답 (frame 변경·런루프·updateConstraints·layoutSubviews 정체)
Q1. "a2의 frame까지 바뀌어야 하는 변경" = Auto Layout도 뜻하나?
✅ 그렇다. Auto Layout에서 a2 frame 변경은 보통 제약 변경(constant·isActive)이나 intrinsic content size 변화(라벨 글자 증가 등)로 일어남. 이게 부모(a1)·VC.view 레이아웃에도 영향 → 위쪽도 무효화.
Q2. "런루프가 대기로 들어가기 직전"이 무슨 말? 런루프가 대기해?
런루프=메인 스레드 무한 루프. 이벤트 처리하다 처리할 게 없으면 잠들어(sleep/대기) CPU를 안 쓰고 다음 입력 대기. Core Animation은 런루프 옵저버로 "잘 거야(BeforeWaiting)" 직전에 commit 실행 → 그때 레이아웃 패스가 돔.
Q3. updateConstraints 관련 4가지
- (a) layout 전? 후? → 전. 레이아웃 패스 = ① update constraints → ② layout → ③ display. ①이 ②보다 먼저
- (b) 오버라이드 안 하면 호출 안 됨? → 시스템 기본 구현은 호출됨(특별한 일 없을 뿐). 내 코드가 돌 일이 없는 것
- (c) setNeeds는 예약, updateConstraints는 바로 실행? → 둘 다 즉시 아님.
setNeedsUpdateConstraints()=예약 → 다음 commit ① 패스에서 시스템이updateConstraints()호출(내가 부르는 게 아님). 즉시 강제는updateConstraintsIfNeeded() - (d) 방향? → ✅ ① update=bottom-up(자식→부모), ② layout=top-down(부모→자식)
Q4. layoutSubviews는 frame set까지? draw/render까지?
frame set(배치)까지만. draw(③ display)·GPU render는 그 이후 별도 단계. layoutSubviews는 서브뷰 frame 숫자 계산·할당만.
Q5. ⭐ a1.layoutSubviews()는 a1의 frame? a2의 layoutSubviews 호출?
이름대로 "subviews를 layout" — a1이 자기 서브뷰(a2)의 frame을 정함. a1 자신 frame 아님.
| 호출 | 누구의 frame을 정하나 |
|---|---|
VC.view.layoutSubviews() | a1, a3의 frame |
a1.layoutSubviews() | a2의 frame |
a2.layoutSubviews() | a2의 서브뷰들 frame |
X.layoutSubviews() = X의 자식들 frame을 정함. X 자신 frame은 X의 부모가 정함. a2의 layoutSubviews는 a1이 직접 부르는 게 아니라 시스템이 top-down 순회로 이어서 호출.
Q6. a2만 layoutSubviews 호출되면 viewDidLayoutSubviews가 안 불릴 수 있어?
✅ 가능. viewWillLayoutSubviews/viewDidLayoutSubviews는 VC.view(루트뷰)의 layoutSubviews가 돌 때만 불림. a2 frame은 그대로고 a2 내부 서브뷰 배치만 무효화되면 a2.layoutSubviews만 돌고 VC 콜백은 안 불림. a2 frame이 바뀌어 상위까지 무효화되면 불림.
Q7. setNeedsUpdateConstraints도 setNeedsLayout처럼 런루프 때 호출돼?
✅ 그렇다. 둘 다 예약이고 실제 메서드는 다음 런루프 끝(commit)에 호출. 같은 commit 안에서 순서가 정해짐 — 예약하는 패스만 다름(① vs ②).
Q8. setNeedsUpdateConstraints는 bottom→top? updateConstraints도?
updateConstraints()호출은 bottom-up(자식→부모) ✅- 하지만
setNeedsUpdateConstraints()자체는 방향이 없다 — 특정 뷰 하나를 더티로 찍는 예약일 뿐, 트리 순회가 아님. 방향은 실제 호출(① 패스)에만 있음
| 예약 (방향 없음) | 실제 호출 (방향 있음) |
|---|---|
setNeedsUpdateConstraints() | updateConstraints() → bottom-up |
setNeedsLayout() | layoutSubviews() → top-down |
setNeedsDisplay() | draw(_:) → top-down |
updateConstraints의 호출 순서가 bottom-up.Q9. setNeedsUpdateConstraints·setNeedsLayout·setNeedsDisplay 모두, 자식이 했을 때 부모도 필요시 될 수 있어?
공통 원칙: 셋 다 setNeeds… 호출 자체는 부모로 전파 안 함(receiver만 더티). 단 "부모도 무효화되냐"는 셋이 다름:
| 자식 호출 → 부모도? | 이유 | |
|---|---|---|
| setNeedsDisplay (③) | ❌ 절대 아님 | 그 뷰 내용만 다시 그림. 부모가 다시 그릴 이유 없음 |
| setNeedsLayout (②) | △ 변경 성격 따라 가능 | 자식 크기 변화가 부모 배치에 영향 주면 부모도 무효화 필요 |
| setNeedsUpdateConstraints (①) | △ 변경 성격 따라 가능 | 제약이 부모까지 걸쳐 있으면 부모도 갱신 필요 |
setNeedsLayout/setNeedsUpdateConstraints를 자식이 불러도 부모의 setNeeds를 부르는 게 아니다. 부모 무효화는 변경의 원인(intrinsic·제약)이 부모 배치에 영향 줄 때 그 경로로 일어남.
왜 display(③)만 부모와 무관? 그리기는 각 레이어가 자기 비트맵(layer.contents)에 독립적으로 함. 자식이 다시 그려도 부모 비트맵 그대로 → render server가 합성 때 겹쳐 갱신, 부모는 draw 다시 안 함. 반면 레이아웃(①②)은 본질적으로 트리 의존(자식 크기 ↔ 부모 배치)이라 위로 번질 수 있음.
Q10. 더티 후 그 런루프 끝? 다음 런루프 끝? 패스 3개가 각각 끝나야 다음으로? render server 합성이 패스마다 끝나야 다음 패스?
- 그 (현재) 런루프 끝 — 다음 런루프 아님. 더티 찍은 이벤트 처리 끝 → 현재 런루프가 idle 직전(BeforeWaiting) commit에서 레이아웃 패스 시작. ("next update cycle"=다음 런루프가 아니라 이 사이클 끝의 그리기 단계)
- 패스 3개 ✅ ① update constraints → ② layout → ③ display. render server 합성은 여기 포함 X(3패스 뒤 별도)
- 패스 단위 순차 ✅ ①을 트리 전체(bottom-up) 끝 → ② 전체(top-down) → ③. 뷰 하나가 ①②③ 다 돌고 다음 뷰로 가는 게 아니라 패스별 전체 처리(②는 ①결과에 의존)
- render server 합성이 패스마다? ❌ ①②③ 전부 앱(메인 스레드)에서 끝남. render server는 3패스 끝난 뒤 commit에서 1회만 관여, 게다가 비동기(앱은 합성 완료 안 기다리고 다음 런루프로)
Q11. "backgroundColor만 있는 뷰 → draw 안 함, ②에서 GPU가 색 사각형 칠함" — 이때 render server가 하는 거지?
✅ render server가 맞다. 단 "②에서"는 틀림 — 색칠은 패스 ①②③ 중 어디도 아니고, 그 뒤 commit → render server 합성 단계에서 GPU가 한다.
- ②(layout)는 frame 숫자만 정함 — 픽셀/색 칠하는 일 없음
backgroundColor는 비트맵(contents) 아니라 layer 속성 → CPU draw도 비트맵도 불필요 → render server가 합성 때 직접 fill(제일 쌈)
Q12. updateConstraints 이후에 필요에 따라 layoutSubviews도 자동으로 호출돼?
✅ 호출된다. ① update constraints와 ② layout은 같은 commit 안에서 ①→② 순서로 이어짐.
- "필요에 따라"가 정확 — ①에서 제약이 실제 frame에 영향 주는 변경일 때만 ②가 돔. frame 결과가 같으면 생략 가능
- 별개 더티 플래그(needsUpdateConstraints ① / needsLayout ②)인데, 제약 변경이 needsLayout을 자동으로 켜줘서 둘이 연결됨
- 방향 주의: ①→② 자동만. 역방향 X —
layoutSubviews가updateConstraints를 부르지 않음
Q13. updateConstraints는 "콘텐츠 크기", layoutSubviews는 "위치·크기" — 둘 다 크기인데 뭐가 달라?
같은 "크기"가 아니다. 하나는 희망(입력), 하나는 확정(출력).
| ① intrinsic content size | ② frame.size | |
|---|---|---|
| 무엇 | 제약 규칙 + 콘텐츠가 원하는 희망 크기 | 제약을 다 푼 실제 확정 크기 |
| 성격 | 입력(요청·선호) | 출력(결과) |
| 위치 | 없음 (크기 희망만) | 있음 (원점+크기) |
| 맥락 | 자기 콘텐츠만 (부모 모름) | 부모 좌표계 안에서 결정 |
| 방향 | bottom-up | top-down |
- ① 요청서: 라벨이 "글자 때문에 최소 120pt 필요해요" 희망을 위로 보고(bottom-up). 확정 아님·위치 없음
- ② 최종 배치: 엔진이 종합해 "라벨은 (x=20,y=10,w=120,h=44)에 놓자" 확정(top-down)
자식이 희망 크기를 아니까 위로 모으고(① bottom-up), 부모 크기가 정해져야 자식 자리가 나오니 아래로 내린다(② top-down).
Q14. 레이아웃 패스(④~⑦)에서 그럼 setNeedsUpdateConstraints·setNeedsLayout은 언제 한 거야? viewDidLoad에서 제약 수정 코드 읽을 때 자동으로?
✅ 맞다. "더티 표시"와 "레이아웃 패스"는 다른 시점. 패스(④~⑦)는 런루프 끝 commit. setNeeds…(더티 표시)는 그보다 먼저, 네 코드가 뷰/제약을 바꾸는 그 순간(동기) 자동으로 켜진다.
| 네가 쓴 코드 | UIKit이 자동으로 켜는 더티 |
|---|---|
view.addSubview(label) | 부모 view에 needsLayout |
NSLayoutConstraint.activate([...]) | needsLayout |
constraint.constant = 100 / .isActive = true | needsLayout |
라벨 text 변경 → intrinsic size 변화 | invalidateIntrinsicContentSize → ① update constraints(intrinsic 제약 갱신) → 이어서 ② layout (자세히 Q15) |
게다가 최초 화면 표시 전엔 레이아웃 패스가 한 번 보장되므로, viewDidLoad에서 제약만 걸어두면 알아서 첫 레이아웃이 돈다.
그럼 setNeeds…를 직접(수동) 부르는 건 언제? 거의 안 부른다. UIKit이 모르는 "내 커스텀 상태"가 레이아웃에 영향 줄 때만 — 보통 manual layout(직접 frame 계산):
setNeedsLayout()수동 →layoutSubviews오버라이드해 수동 배치할 때setNeedsUpdateConstraints()수동 →updateConstraints오버라이드해 제약 로직 둘 때- Auto Layout 쓰면 대부분 자동 → 수동 setNeeds는 manual layout 쪽에서 주로 등장
Q15-1. "레이아웃 패스가 여러 번 호출될 수 있다"가 한 런루프에 여러 번이라는 뜻이야?
주로 "여러 런루프 사이클에 걸쳐 반복"이라는 뜻. 기본은 한 런루프 사이클당 패스 1회(쌓인 더티 모아서).
- 회전 → 그 사이클에 1회 / 스크롤 → 거의 매 프레임(매 런루프) 1회 / 제약 변경 → 그때마다 1회
- "한 번 그리고 끝"이 아니라 앱이 도는 동안 이 사이클이 수없이 반복 → 그래서
layoutSubviews에 무거운 작업 넣으면 스크롤 중 매 프레임 비용 → 버벅 - 한 런루프 안 여러 번도 가능(부차적):
layoutIfNeeded()명시적 여러 번 호출, layout 중 재무효화. 하지만 정상 흐름 기본은 사이클당 1회
Q15-2. ⭐ intrinsic size 바뀌면 setNeedsLayout 아니라 setNeedsUpdateConstraints 아냐?
주인님 말이 더 정확하다. (Q14 표의 "→ needsLayout"은 단순화로 덜 정확 — 교정)
핵심: intrinsic content size는 Auto Layout 엔진 안에서 "content size constraint"라는 제약(입력)으로 표현된다. 그게 바뀌면 제약을 갱신해야 하니 ① update constraints 경로로 들어간다.
- 주인님 직관(intrinsic=제약 입력 → update constraints 쪽)이 옳다
- 다만 결국 ②(layout)도 따라온다 — 제약 바뀌면 frame을 다시 풀어야 하니까. 그래서 "needsLayout도 켜진다"가 완전 틀린 건 아니지만 일차 경로는 ① update constraints
Q16. 위치만 바꾸면 updateConstraints 안 호출되고 layoutSubviews만 호출되겠네?
✅ 맞다. 단 정확한 기준은 "위치라서"가 아니라 "제약 값(constant)만 바꿔서(재구성 아니라서)".
| 무엇을 바꿨나 | ① update | ② layout |
|---|---|---|
위치 제약 constant (leading.constant=) | ❌ | ✅ |
크기 제약 constant (width.constant=) | ❌ | ✅ |
| 제약 추가/제거/활성화 | ✅ 재구성 | ✅ |
| intrinsic 변화 (라벨 text) | ✅ content size 제약 갱신 | ✅ |
크기를 바꿔도 constant면 ① 안 거치고 ②만. 반대로 위치가 아니어도 제약 add/remove나 intrinsic(text) 변화면 ① 거침. → 결정 요인은 위치/크기가 아니라 "기존 제약 값만 손댔나 vs 제약 구조/입력이 바뀌었나".
Q17. "제약 애니메이션 = 값 변경은 블록 밖, layoutIfNeeded는 블록 안" — 이게 왜 중요해?
핵심: UIView.animate가 보간하는 건 frame이지 constant가 아니다.
constraint.constant = 200은 frame을 즉시 안 바꿈 — needsLayout 더티만, 실제 frame은 다음 layout 패스에서- 게다가 constant는 애니메이션 대상 속성이 아님. animate가 잡는 건 frame
- 그래서 frame이 "블록 실행 중에" 옛→새로 변해야 animate가 캡처 → 그 변화를 강제하는 게 블록 안 layoutIfNeeded()
왜 중요?
- Auto Layout에선 frame 직접 애니 불가 —
view.frame=로 애니해도 다음 layout 패스가 제약대로 덮어써서 충돌. 제약 뷰는 반드시 이 패턴 - 모르면 "왜 애니 안 되지/왜 툭 튀지" 대표 삽질 버그
- 견고하게: 시작 상태도
layoutIfNeeded()로 확정(pending layout이 시작점 튀게 하는 것 방지)
Q18. 레이아웃 설정 방법 전체 리스트 (오토레이아웃 외엔 뭐가 있나)
큰 갈래 4개: ① Manual(frame) · ② Autoresizing mask · ③ Auto Layout · ④ SwiftUI. 오토레이아웃은 ③(표준).
| 방법 | 예시 | 비고 |
|---|---|---|
| ① Manual / frame | label.frame = CGRect(...) / layoutSubviews 직접 계산 | 완전 수동. 성능·커스텀 영역 |
| ② Autoresizing mask | view.autoresizingMask = [.flexibleWidth, ...] | 오토레이아웃 이전 방식(스프링&스트럿). 표현력 제한 |
| ③ Auto Layout | (아래 작성법 여러 개) | 제약 기반 표준 |
| ④ SwiftUI | VStack { ... }.frame(...) | 선언형, 별개 엔진 |
③ Auto Layout 작성법:
| 작성법 | 예시 | 비고 |
|---|---|---|
| NSLayoutAnchor ⭐ | a.leadingAnchor.constraint(equalTo: v.leadingAnchor, constant: 20) | 현재 코드 표준 |
| NSLayoutConstraint(원형) | NSLayoutConstraint(item:attribute:...) | verbose, 잘 안 씀 |
| VFL | "H:|-20-[a]-20-|" | 문자열, 컴파일 체크 X, 거의 안 씀 |
| UIStackView ⭐ | UIStackView(arrangedSubviews:[a,b]) | 선형 배치 자동, 내부는 Auto Layout |
| Interface Builder | Storyboard/XIB GUI | 코드 없이 |
| SnapKit 등 DSL | a.snp.makeConstraints { ... } | Anchor 위 래퍼, 간결 |
보조 — 레이아웃 가이드(제약 기준점): safeAreaLayoutGuide(노치·홈 인디케이터 피함) · layoutMarginsGuide(기본 여백) · readableContentGuide(가독 폭) · UILayoutGuide(보이지 않는 가상 사각형, 더미뷰 대체).
Q19. layoutSubviews는 사실 CALayer.layoutSublayers에서 시작된다? (delegate 체인)
✅ 맞다. UIView는 CALayer 위에 얹힌 래퍼라, 레이아웃은 레이어가 하고 UIView는 그 layer의 delegate로서 콜백을 받는다.
체인:
관찰 예시:
트리(VC.view ▸ a1 ▸ a2) — 레이어 트리 기준 top-down:
CALayer.layoutSublayers → layoutSublayersOfLayer: → UIView.layoutSubviews · 그리기 CALayer.display → drawLayer:inContext: → UIView.draw(_:)- UIView=CALayer 래퍼. 위치·크기·렌더·애니는 전부 layer가 처리, UIView는 delegate로 저수준 콜백을 UIKit 메서드로 변환
- 그래서
layoutSubviews/draw(_:)는 우리가 부르는 진입점이 아니라 레이어 메커니즘에 얹힌 콜백 - 순수 CALayer(delegate 없음)는 layoutSublayers에서 layoutSubviews로 안 감 — UIView가 layer.delegate일 때만의 동작
📅 2026-06-30 — layoutSubviews 호출 순서 (Top-Down)
Q. 부모가 자식 frame 설정 → 자식 layoutSubviews 호출?
✅ 맞다. 부모 먼저, 자식 나중 (Top-Down):
예시 코드
전체 흐름 (재귀)
→ 자식은 자신의 bounds(부모가 설정한 frame 기반)를 알고 있는 상태에서 손자들 배치 가능
| 순서 | 내용 |
|---|---|
| 1 | 자신의 frame/bounds 확정 (부모가 설정) |
| 2 | 자신의 layoutSubviews()에서 subview frame 설정 |
| 3 | subview도 같은 과정 반복 (재귀) |
📅 2026-07-07 — bounds vs frame · Autoresizing Mask · layoutSubviews 호출 순서
Q20. Autoresizing Mask란?
Auto Layout 이전의 레이아웃 방식 (iOS 6 이전, 지금도 존재). 부모 크기가 바뀔 때 자식이 어떻게 반응할지 "스프링 & 스트럿"으로 지정.
| 옵션 | 의미 |
|---|---|
.flexibleWidth | 너비가 부모 따라 늘어남 |
.flexibleHeight | 높이가 부모 따라 늘어남 |
.flexibleLeftMargin | 왼쪽 여백이 유동적 (오른쪽 고정) |
.flexibleRightMargin | 오른쪽 여백이 유동적 |
.flexibleTopMargin | 위 여백이 유동적 |
.flexibleBottomMargin | 아래 여백이 유동적 |
한계: 비율 지정, 뷰 간 관계(A 옆에 B) 등 복잡한 건 못함 → Auto Layout으로 대체됨.
= false 필수. 기본값 true면 autoresizing mask가 제약으로 자동 변환 → 내가 건 제약과 충돌.Q21. bounds vs frame 차이
| frame | bounds | |
|---|---|---|
| 좌표계 | 부모 기준 | 자기 기준 |
| origin | 부모 안에서 내 위치 | 내 콘텐츠 시작점 (보통 0,0) |
| size | 회전 없으면 동일 | |
| 용도 | 부모가 나를 배치할 때 | 내가 자식을 배치할 때 |
회전(transform) 있을 때:
Q22. layoutSubviews에서 왜 bounds를 써야 하나?
자식을 배치하는 건 "내 좌표계 안"에서 하는 일. 내 좌표계 = bounds.
- frame.width를 쓰면? 보통은 같지만, 회전(transform) 있으면 달라짐 → 자식이 삐져나감
- 회전 없어도 bounds 쓰는 게 개념적으로 맞는 습관
frame / 내가 자식을 배치할 때 = boundsQ23. ⭐ super.layoutSubviews()가 자식 layoutSubviews까지 부르나?
아니다. super.layoutSubviews()는 자식 frame만 설정하고, 자식의 layoutSubviews()는 안 부른다. 시스템이 top-down으로 순회하며 각 뷰의 layoutSubviews()를 순차적으로 호출한다.
왜 이렇게 설계?
실제론: 내 코드가 frame을 바꾼 후에 시스템이 childView.layoutSubviews()를 부름 → grandchild가 새 frame(200x100) 기준으로 배치됨.
Q24. layoutSubviews 안에서 childView.frame을 바꾸면 또 layout pass 도나?
아니다. frame = ... 직접 대입은 setNeedsLayout()을 호출하지 않는다.
| 동작 | needsLayout 자동 ON? |
|---|---|
view.addSubview(child) | ✅ |
constraint.constant = 100 | ✅ |
NSLayoutConstraint.activate([...]) | ✅ |
child.frame = CGRect(...) | ❌ 안 켜짐 |
layoutSubviews 안에서 frame 직접 설정 = "지금 이 pass 안에서" frame을 확정하는 것. 새 pass를 예약하는 게 아님.