초급 기준 배치 규칙
- UI 요소 생성/한 번만 할 설정 -> `viewDidLoad`
- 매 진입 시 최신화 필요한 값 -> `viewWillAppear`
- 화면에 보여진 뒤 시작해야 자연스러운 작업 -> `viewDidAppear`
- 종료/중지 처리 -> `viewWillDisappear` / `viewDidDisappear`
이 주제의 핵심은 "메서드 순서를 외우는 것"보다 "어떤 작업을 어떤 시점에 두어야 안정적인가"를 이해하는 것이다.
기록 없음
| 메서드 | 호출 시점 | 주 용도 |
|---|---|---|
| `loadView` | 뷰가 처음 필요할 때 | 루트 뷰를 코드로 생성(특수 케이스) |
| `viewDidLoad` | 뷰 로드 직후(1회성에 가까움) | 초기 바인딩, 정적 UI 설정 |
| `viewWillAppear` | 화면에 나타나기 직전 | 화면 진입 전 데이터/상태 갱신 |
| `viewDidAppear` | 화면 표시 완료 직후 | 애니메이션 시작, 노출 로그, 사용자 상호작용 시작 |
| `viewWillDisappear` | 화면에서 사라지기 직전 | 진행 중 작업 일시 정지, 변경 저장 준비 |
| `viewDidDisappear` | 화면에서 사라진 직후 | 무거운 작업 정리, 관찰 해제 |
네비게이션 push/pop, 모달 present/dismiss, 탭 전환, child view controller 임베딩은 전부 "누가 화면 ownership을 가지는가"가 다르다. 그래서 같은 `viewWillAppear`라도 부모/자식 호출 시점이 엇갈려 보일 수 있다.
| 전환 유형 | 화면 ownership 변화 | 실무에서 보이는 현상 |
|---|---|---|
| Navigation push | 기존 top VC -> 새 top VC로 주 ownership 이동 | 기존 VC `viewWillDisappear`와 새 VC `viewWillAppear`가 전환 애니메이션 구간에서 겹쳐 보임 |
| Navigation pop | 현재 top VC -> 이전 VC로 ownership 복귀 | 이전 VC가 "처음 로드"가 아니라 "재등장"이라 `viewDidLoad` 없이 `viewWillAppear`만 호출될 수 있음 |
| Modal present/dismiss | presented VC가 전면 ownership 획득, dismiss 시 원래 VC 복귀 | presentation style에 따라 presenting VC의 appear/disappear 콜백 여부가 다르게 보일 수 있음 |
| Tab 전환 | 선택된 탭 루트 VC가 active ownership 획득 | 탭별 네비게이션 스택 상태가 남아 있어 "예상보다 적은 콜백"처럼 느껴질 수 있음 |
| Child 임베딩 | 부모 VC가 ownership 유지, child는 영역 단위 ownership만 가짐 | 부모 생명주기와 child 생명주기가 1:1 동기화되지 않아 호출 타이밍이 어긋나 보임 |
// 디버깅용: 부모/자식/전환 대상 VC 모두 같은 포맷으로 로그
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
print("[\(type(of: self))] viewWillAppear")
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print("[\(type(of: self))] viewDidAppear")
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
print("[\(type(of: self))] viewWillDisappear")
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
print("[\(type(of: self))] viewDidDisappear")
}final class FeedViewController: UIViewController {
private var loadTask: Task<Void, Never>?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// 매 진입 시 최신화
loadTask = Task { [weak self] in
guard let self else { return }
await self.reloadFeed()
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// 화면 이탈 시 불필요한 작업 중단
loadTask?.cancel()
loadTask = nil
}
}iOS는 성능 최적화를 위해 뷰를 미리 로드하거나, 화면 전환 애니메이션 전 준비를 할 수 있다. 이때 개발자가 체감하는 "보이기 전 호출"과 실제 호출 타이밍이 다를 수 있다.
생명주기는 순서 암기 과목이 아니라 타이밍 설계 과목이다.
어떤 코드를 어디에 둘지 결정할 때, "이 코드는 1회성인가 / 매 노출마다 필요한가 / 이탈 시 중단돼야 하는가"를 기준으로 분류하면 안정성이 올라간다.
이 섹션은 위에서 나온 질문뿐 아니라, 새로운 개발적 지식과 시니어라면 알아야 할 내용까지 추가한다.