.onDisappear는 인터페이스 계층 구조에서 View가 제거될 때 클로저를 실행하는 SwiftUI 수정자입니다. 호출은 화면을 닫을 때, 탭을 전환할 때, 모달 창을 해제할 때, 또는 요소가 가시 영역 밖으로 스크롤될 때 발생합니다. Apple Developer Documentation(2026)에 따르면 onDisappear는 크래시 시나리오나 시스템 워치독에 의한 앱 종료 시 호출을 보장하지 않습니다. SwiftUI에 대한 자세한 내용은 SwiftUI 문서를 참조하세요.
핵심 요약
.onDisappear는 Void 클로저를 받아 View가 계층 구조에서 제거될 때 실행하는 SwiftUI의 View 수정자입니다. .onAppear와 함께 화면의 완전한 라이프사이클(표시 — 작업 — 사라짐)을 형성합니다. Apple은 SwiftUI와 함께 iOS 13에서 UIKit의 viewDidDisappear에 해당하는 기능으로 onDisappear를 도입했습니다.
구문적으로 .onDisappear는 onAppear와 동일합니다. 모든 View를 수정하고 SwiftUI 렌더러가 View 제거 시 호출하는 클로저를 연결합니다. 전환 애니메이션 완료 후에만 viewDidDisappear가 작동하는 UIKit과 달리, SwiftUI의 onDisappear는 애니메이션이 끝나기 전에 — View가 제거 표시되는 순간에 호출될 수 있습니다.
기본 구문은 onAppear만큼 간결합니다. 수정자는 추가 매개변수를 받지 않으며 메인 스레드에서 동기적으로 실행되는 클로저만 받습니다.
struct DetailView: View {
var body: some View {
Text("Detail Screen")
.onDisappear {
print("DetailView가 화면에서 사라짐")
}
}
}
호출 메커니즘은 onAppear의 반대입니다: 자식 요소가 먼저 onDisappear를 받고, 그 다음 부모가 받습니다. 이 child-first 규칙은 부모 리소스가 해제되기 전에 자식 리소스가 해제되도록 보장합니다. 자식 요소가 부모 데이터에 의존하는 경우 부모 컨텍스트 없이도 올바르게 종료될 수 있어야 합니다.
SwiftUI는 View가 렌더 그래프에서 제거될 때 onDisappear를 호출합니다. 트리거에는 NavigationStack에서 pop, TabView 전환, 모달 해제(sheet/fullScreenCover), 조건부 렌더링 View(if/switch)가 포함됩니다. List와 ScrollView에서는 셀이 프리페치 버퍼 밖으로 스크롤될 때 onDisappear가 호출됩니다.
Child-first 순서는 VStack에 세 개의 자식 View가 있는 경우 각 자식에 대해 역순으로 onDisappear가 먼저 호출되고, 그 다음 부모에 대해 호출됨을 의미합니다. 이는 올바른 정리를 위해 중요합니다: 부모 ViewModel이 공유 리소스를 해제하기 전에 자식 타이머가 취소됩니다.
struct ParentView: View {
var body: some View {
VStack {
ChildView(id: "A")
ChildView(id: "B")
}
.onDisappear {
print("Parent onDisappear — 마지막")
}
}
}
struct ChildView: View {
let id: String
var body: some View {
Text("Child \(id)")
.onDisappear {
print("Child \(id) onDisappear")
}
}
}
콘솔 출력: Child B onDisappear, Child A onDisappear, Parent onDisappear — 마지막. onAppear와 비교하여 역순으로 호출되어 올바른 정리 체인이 보장됩니다.
호출 시나리오는 컨테이너 유형에 따라 다릅니다. NavigationStack에서는 pop-to-root, 일반 pop-back 또는 스와이프(interactivePopGestureRecognizer)에 의한 화면 해제 시 onDisappear가 작동합니다. TabView에서 탭을 전환하면 숨겨진 탭에 onDisappear가, 표시되는 탭에 onAppear가 호출됩니다 — 두 수정자가 거의 동시에 작동합니다.
Sheet 및 fullScreenCover에서는 프로그래매틱 해제(@Environment(\.dismiss) 통한) 또는 아래로 스와이프 제스처 시 onDisappear가 호출됩니다. 중요한 세부 사항: sheet가 열렸지만 사용자가 앱을 전환한 경우, 실제 닫힐 때까지 onDisappear가 호출되지 않습니다.
| 시나리오 | onDisappear 호출 | 비고 |
|---|---|---|
| NavigationStack Pop | 예 | 애니메이션 직후 |
| TabView 탭 전환 | 예 | 현재 탭 |
| Sheet 해제 | 예 | 애니메이션 완료 전 |
| List 스크롤 | 예 | 셀이 프리페치 영역 이탈 |
| 앱 최소화 | 아니오 | 호출 보장 없음 |
| 크래시/watchdog kill | 아니오 | 호출되지 않음 |
주요 사용 사례는 리소스 정리, 상태 저장 및 추적입니다. onAppear와 달리 onDisappear 작업은 종료 시 실행되며 View가 한 번만 사라지므로 중복 확인이 필요하지 않습니다.
상태 저장은 화면을 떠날 때 데이터를 저장해야 하는 양식에서 특히 유용합니다. 타이머와 Combine 구독은 화면으로 돌아올 때 누수를 방지하기 위해 onDisappear에서 취소됩니다.
struct FormView: View {
@State private var draftText = ""
@State private var timer: Timer?
var body: some View {
TextField("Enter text", text: $draftText)
.onAppear {
timer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true) { _ in
saveDraft()
}
}
.onDisappear {
timer?.invalidate()
timer = nil
saveDraft()
}
}
private func saveDraft() {
UserDefaults.standard.set(draftText, forKey: "draft")
}
}
타이머 취소를 통해 화면이 이미 닫힌 후 코드 실행을 방지합니다. 취소하지 않으면 타이머가 더 이상 현재 View에 속하지 않는 @State를 업데이트하려고 시도하여 런타임 경고가 발생할 수 있습니다.
화면 체류 시간은 클래식한 추적 시나리오입니다. onAppear에서 시간을 기록하고 onDisappear에서 차이를 계산하여 세션 시간과 함께 분석 이벤트를 전송합니다.
struct TrackedView: View {
@State private var appearTime: Date?
var body: some View {
Text("Tracked Screen")
.onAppear {
appearTime = Date()
Analytics.shared.logEvent("screen_view", params: ["screen": "TrackedView"])
}
.onDisappear {
if let start = appearTime {
let duration = Date().timeIntervalSince(start)
Analytics.shared.logEvent("screen_close", params: [
"screen": "TrackedView",
"duration_ms": Int(duration * 1000)
])
}
}
}
}
주요 차이점은 호출 순서와 실행 보장입니다. onAppear는 View가 계층 구조에 추가될 때 호출되며 재생성 시 다시 호출될 수 있습니다. onDisappear는 제거 시 호출되며 정상 종료 시에만 작동이 보장되고 크래시 시나리오에서는 보장되지 않습니다.
WWDC 2024에 따르면 Apple은 onDisappear를 정리 포인트로 취급하고 데이터 커밋 포인트로 취급하지 말 것을 권장합니다. 중요한 데이터(결제, 등록)는 View가 사라지는 순간이 아닌 실시간으로 저장해야 합니다. 앱이 최소화될 때 onDisappear가 보장되지 않기 때문입니다.
| 특성 | .onAppear | .onDisappear |
|---|---|---|
| 호출 시점 | View가 계층에 추가 | View가 계층에서 제거 |
| 순서 | Parent-first | Child-first |
| 보장 | 높음 | 크래시 시 없음 |
| 주요 작업 | 초기화 | 정리 |
| 재호출 | View 재생성 시 | 사라짐당 한 번 |
권장사항: onDisappear는 중요하지 않은 정리 및 추적에만 사용하세요. 데이터 지속성에는 AppDelegate의 scenePhase 또는 applicationWillTerminate 알림을 사용하세요.
시나리오 1: YouTube 스타일 재생. 비디오 상세 화면에서 onDisappear가 재생 위치를 UserDefaults에 저장합니다. 다시 열면 onAppear가 UserDefaults에서 위치를 복원하여 연속적인 시청 경험을 제공합니다.
시나리오 2: Combine 구독 취소. ViewModel이 Combine 퍼블리셔를 사용하는 경우 onDisappear가 cancellable?.cancel()을 통해 구독을 취소합니다. 이는 화면을 떠난 후 UI 업데이트를 방지하며, 페이지네이션이 있는 목록 및 검색 쿼리에서 특히 중요합니다.
시나리오 3: WebSocket 종료. 실시간 연결(메신저, 시세 피드)이 있는 앱에서 onDisappear가 배터리와 데이터를 절약하기 위해 WebSocket 연결을 종료합니다. 화면으로 돌아올 때 onAppear에서 재연결이 이루어집니다.
WebSocket은 화면을 떠날 때 종료해야 하는 일반적인 리소스입니다. 아래 예제에서 onDisappear가 소켓을 연결 해제하고 onAppear가 다시 연결합니다. 여러 화면이 있는 앱에서 상당한 데이터 절약 효과를 제공합니다.
struct ChatView: View {
@StateObject private var socket = WebSocketManager()
var body: some View {
ChatListView(messages: socket.messages)
.onAppear {
socket.connect()
}
.onDisappear {
socket.disconnect()
}
}
}
중요: TabView 탭 간 전환 시 현재 탭의 onDisappear와 다음 탭의 onAppear가 거의 동시에 작동합니다. WebSocket의 경우 불필요한 부하를 생성하는 연결 해제-재연결 사이클이 발생할 수 있습니다. 해결책은 지연을 사용하거나 다음 화면에서 소켓이 필요한지 확인하는 것입니다.
자주 묻는 질문
네, .onDisappear는 앱 크래시(crash, watchdog kill), 화면을 닫지 않고 앱 최소화, 또는 백그라운드에서 앱이 종료되는 시스템 시나리오에서 호출을 보장하지 않습니다. 중요한 데이터에는 scenePhase 또는 applicationWillTerminate를 사용하세요.
.onDisappear는 View가 계층에서 제거될 때 작동하는 특정 화면에 대한 로컬 콜백입니다. .scenePhase(@Environment(\.scenePhase) 통한)는 전체 앱 상태(active, inactive, background)가 변경될 때 작동합니다. 화면 시간 추적에는 onAppear+onDisappear를, 전역 상태 지속성에는 scenePhase를 사용하세요.
원인은 빠른 탭 전환 또는 동일한 화면의 반복적인 push/pop입니다. SwiftUI가 새 View 인스턴스를 생성하고, 이전 것을 제거하고, 다시 생성할 수 있으며, 매번 onDisappear와 onAppear를 호출합니다. .id(), .equatable()을 사용 중이거나 부모 body에서 View를 재생성하고 있는지 확인하세요.
네, .onDisappear는 macOS(10.15+)에서 완전히 지원되며 동일한 동작을 합니다: 창 닫기, split view 패널 제거 또는 모달 해제 시 호출됩니다. macOS에서는 창을 닫을 뿐만 아니라 숨길 때도 onDisappear가 호출되므로 macOS 앱에서 이를 고려하는 것이 중요합니다.
@State에 URLSessionTask 참조를 저장하고 onDisappear에서 task.cancel()을 호출하세요. 대안으로 View가 사라질 때 async 작업을 자동으로 취소하는 .task 수정자를 사용하세요. .task는 URLSession을 포함한 모든 async 작업에 선호됩니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.