UIKit Lifecycle Guide

UIViewController 생명주기 순서

이 주제의 핵심은 "메서드 순서를 외우는 것"보다 "어떤 작업을 어떤 시점에 두어야 안정적인가"를 이해하는 것이다.

Lifecycle order Timing-safe work Knowledge item #23
학습 날짜

기록 없음

기본 순서

메서드호출 시점주 용도
`loadView` 뷰가 처음 필요할 때 루트 뷰를 코드로 생성(특수 케이스)
`viewDidLoad` 뷰 로드 직후(1회성에 가까움) 초기 바인딩, 정적 UI 설정
`viewWillAppear` 화면에 나타나기 직전 화면 진입 전 데이터/상태 갱신
`viewDidAppear` 화면 표시 완료 직후 애니메이션 시작, 노출 로그, 사용자 상호작용 시작
`viewWillDisappear` 화면에서 사라지기 직전 진행 중 작업 일시 정지, 변경 저장 준비
`viewDidDisappear` 화면에서 사라진 직후 무거운 작업 정리, 관찰 해제

초급 기준 배치 규칙

  • UI 요소 생성/한 번만 할 설정 -> `viewDidLoad`
  • 매 진입 시 최신화 필요한 값 -> `viewWillAppear`
  • 화면에 보여진 뒤 시작해야 자연스러운 작업 -> `viewDidAppear`
  • 종료/중지 처리 -> `viewWillDisappear` / `viewDidDisappear`

실무에서 자주 틀리는 포인트

  • `viewDidLoad`를 "매번 호출"로 오해
  • `viewWillAppear`에서 중복 네트워크 요청 발생
  • `viewDidDisappear`에서 해제 안 해서 observer/task 누수
  • 컨테이너 구조에서 호출 순서를 단일 화면처럼 가정

난이도 높은 설명 1: 컨테이너/전환에서 순서가 달라 보이는 이유

네비게이션 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")
}
디버깅 팁: 전환 버그가 나면 대상 VC 하나만 보지 말고, 부모 컨테이너 + 현재 VC + 전환 대상 VC의 콜백 로그를 동시에 찍어야 원인이 보인다.
정리: 생명주기 메서드는 "내 VC 하나의 시간축"이 아니라, 컨테이너 계층 전체에서 ownership이 이동하는 시간축으로 읽어야 정확하다.

난이도 높은 설명 2: 비동기 작업과 생명주기 결합

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
    }
}

난이도 높은 설명 3: preload / offscreen 렌더와 오해

iOS는 성능 최적화를 위해 뷰를 미리 로드하거나, 화면 전환 애니메이션 전 준비를 할 수 있다. 이때 개발자가 체감하는 "보이기 전 호출"과 실제 호출 타이밍이 다를 수 있다.

결론: 생명주기 메서드는 "보장된 계약"으로 사용하고, 사용자 가시성(실제로 보였는지) 판단은 `viewDidAppear` 기준으로 두는 것이 안전하다.

점검 체크리스트

  • 초기 1회 설정과 매 진입 갱신이 분리돼 있는가?
  • 비동기 task/observer가 이탈 시점에 정리되는가?
  • 전환 방식(push/present/tab)에 따라 중복 호출 가드를 뒀는가?
  • 로그/분석 이벤트가 실제 노출 시점(`viewDidAppear`)에 찍히는가?

한 줄 결론

생명주기는 순서 암기 과목이 아니라 타이밍 설계 과목이다.

어떤 코드를 어디에 둘지 결정할 때, "이 코드는 1회성인가 / 매 노출마다 필요한가 / 이탈 시 중단돼야 하는가"를 기준으로 분류하면 안정성이 올라간다.

Q

이 섹션은 위에서 나온 질문뿐 아니라, 새로운 개발적 지식과 시니어라면 알아야 할 내용까지 추가한다.