.onDisappear: 작동 원리 및 SwiftUI에서의 활용

저자: IT Sectr 게시일: 2026-06-26 읽는 시간: 10 분

.onDisappear는 인터페이스 계층 구조에서 View가 제거될 때 클로저를 실행하는 SwiftUI 수정자입니다. 호출은 화면을 닫을 때, 탭을 전환할 때, 모달 창을 해제할 때, 또는 요소가 가시 영역 밖으로 스크롤될 때 발생합니다. Apple Developer Documentation(2026)에 따르면 onDisappear는 크래시 시나리오나 시스템 워치독에 의한 앱 종료 시 호출을 보장하지 않습니다. SwiftUI에 대한 자세한 내용은 SwiftUI 문서를 참조하세요.

핵심 요약

  • .onDisappear — View 사라짐 시 코드 실행을 위한 SwiftUI 수정자.
  • 대칭성 — onDisappear는 onAppear와 쌍을 이루며 역순으로 호출됩니다.
  • 리소스 정리 — 주요 시나리오: 타이머 취소, 연결 종료, 상태 저장.
  • 분석 — onDisappear를 통해 화면 종료, 체류 시간, 세션 종료 이벤트를 전송합니다.
  • 보장 없음 — 앱 크래시 시 onDisappear가 호출되지 않습니다.

.onDisappear란?

.onDisappear는 Void 클로저를 받아 View가 계층 구조에서 제거될 때 실행하는 SwiftUI의 View 수정자입니다. .onAppear와 함께 화면의 완전한 라이프사이클(표시 — 작업 — 사라짐)을 형성합니다. Apple은 SwiftUI와 함께 iOS 13에서 UIKit의 viewDidDisappear에 해당하는 기능으로 onDisappear를 도입했습니다.

구문적으로 .onDisappear는 onAppear와 동일합니다. 모든 View를 수정하고 SwiftUI 렌더러가 View 제거 시 호출하는 클로저를 연결합니다. 전환 애니메이션 완료 후에만 viewDidDisappear가 작동하는 UIKit과 달리, SwiftUI의 onDisappear는 애니메이션이 끝나기 전에 — View가 제거 표시되는 순간에 호출될 수 있습니다.

onDisappear 구문

기본 구문은 onAppear만큼 간결합니다. 수정자는 추가 매개변수를 받지 않으며 메인 스레드에서 동기적으로 실행되는 클로저만 받습니다.

swift
struct DetailView: View {
    var body: some View {
        Text("Detail Screen")
            .onDisappear {
                print("DetailView가 화면에서 사라짐")
            }
    }
}

.onDisappear 작동 방식

호출 메커니즘은 onAppear의 반대입니다: 자식 요소가 먼저 onDisappear를 받고, 그 다음 부모가 받습니다. 이 child-first 규칙은 부모 리소스가 해제되기 전에 자식 리소스가 해제되도록 보장합니다. 자식 요소가 부모 데이터에 의존하는 경우 부모 컨텍스트 없이도 올바르게 종료될 수 있어야 합니다.

SwiftUI는 View가 렌더 그래프에서 제거될 때 onDisappear를 호출합니다. 트리거에는 NavigationStack에서 pop, TabView 전환, 모달 해제(sheet/fullScreenCover), 조건부 렌더링 View(if/switch)가 포함됩니다. List와 ScrollView에서는 셀이 프리페치 버퍼 밖으로 스크롤될 때 onDisappear가 호출됩니다.

Child-first 호출 순서

Child-first 순서는 VStack에 세 개의 자식 View가 있는 경우 각 자식에 대해 역순으로 onDisappear가 먼저 호출되고, 그 다음 부모에 대해 호출됨을 의미합니다. 이는 올바른 정리를 위해 중요합니다: 부모 ViewModel이 공유 리소스를 해제하기 전에 자식 타이머가 취소됩니다.

swift
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와 비교하여 역순으로 호출되어 올바른 정리 체인이 보장됩니다.

.onDisappear 호출 시점

호출 시나리오는 컨테이너 유형에 따라 다릅니다. 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아니오호출되지 않음

.onDisappear 예제

주요 사용 사례는 리소스 정리, 상태 저장 및 추적입니다. onAppear와 달리 onDisappear 작업은 종료 시 실행되며 View가 한 번만 사라지므로 중복 확인이 필요하지 않습니다.

상태 저장 및 타이머 취소

상태 저장은 화면을 떠날 때 데이터를 저장해야 하는 양식에서 특히 유용합니다. 타이머와 Combine 구독은 화면으로 돌아올 때 누수를 방지하기 위해 onDisappear에서 취소됩니다.

swift
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에서 차이를 계산하여 세션 시간과 함께 분석 이벤트를 전송합니다.

swift
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)
                    ])
                }
            }
    }
}

.onDisappear vs .onAppear — 차이점

주요 차이점은 호출 순서와 실행 보장입니다. onAppear는 View가 계층 구조에 추가될 때 호출되며 재생성 시 다시 호출될 수 있습니다. onDisappear는 제거 시 호출되며 정상 종료 시에만 작동이 보장되고 크래시 시나리오에서는 보장되지 않습니다.

WWDC 2024에 따르면 Apple은 onDisappear를 정리 포인트로 취급하고 데이터 커밋 포인트로 취급하지 말 것을 권장합니다. 중요한 데이터(결제, 등록)는 View가 사라지는 순간이 아닌 실시간으로 저장해야 합니다. 앱이 최소화될 때 onDisappear가 보장되지 않기 때문입니다.

특성.onAppear.onDisappear
호출 시점View가 계층에 추가View가 계층에서 제거
순서Parent-firstChild-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에서 재연결이 이루어집니다.

onDisappear에서 WebSocket 종료

WebSocket은 화면을 떠날 때 종료해야 하는 일반적인 리소스입니다. 아래 예제에서 onDisappear가 소켓을 연결 해제하고 onAppear가 다시 연결합니다. 여러 화면이 있는 앱에서 상당한 데이터 절약 효과를 제공합니다.

swift
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가 호출되지 않을 수 있나요?

네, .onDisappear는 앱 크래시(crash, watchdog kill), 화면을 닫지 않고 앱 최소화, 또는 백그라운드에서 앱이 종료되는 시스템 시나리오에서 호출을 보장하지 않습니다. 중요한 데이터에는 scenePhase 또는 applicationWillTerminate를 사용하세요.

.onDisappear와 .scenePhase를 어떻게 구분하나요?

.onDisappear는 View가 계층에서 제거될 때 작동하는 특정 화면에 대한 로컬 콜백입니다. .scenePhase(@Environment(\.scenePhase) 통한)는 전체 앱 상태(active, inactive, background)가 변경될 때 작동합니다. 화면 시간 추적에는 onAppear+onDisappear를, 전역 상태 지속성에는 scenePhase를 사용하세요.

.onDisappear가 연속으로 여러 번 호출되는 이유는?

원인은 빠른 탭 전환 또는 동일한 화면의 반복적인 push/pop입니다. SwiftUI가 새 View 인스턴스를 생성하고, 이전 것을 제거하고, 다시 생성할 수 있으며, 매번 onDisappear와 onAppear를 호출합니다. .id(), .equatable()을 사용 중이거나 부모 body에서 View를 재생성하고 있는지 확인하세요.

macOS의 SwiftUI에서 .onDisappear가 작동하나요?

네, .onDisappear는 macOS(10.15+)에서 완전히 지원되며 동일한 동작을 합니다: 창 닫기, split view 패널 제거 또는 모달 해제 시 호출됩니다. macOS에서는 창을 닫을 뿐만 아니라 숨길 때도 onDisappear가 호출되므로 macOS 앱에서 이를 고려하는 것이 중요합니다.

.onDisappear에서 URLSessionTask를 취소하는 방법은?

@State에 URLSessionTask 참조를 저장하고 onDisappear에서 task.cancel()을 호출하세요. 대안으로 View가 사라질 때 async 작업을 자동으로 취소하는 .task 수정자를 사용하세요. .task는 URLSession을 포함한 모든 async 작업에 선호됩니다.

요약

  • .onDisappear — View 사라짐 시 리소스 정리 및 추적을 위한 onAppear와 쌍을 이루는 수정자.
  • Child-first 순서 — 자식 View가 부모보다 먼저 onDisappear를 받아 올바른 정리 체인 형성.
  • 호출 보장 없음 — 크래시, 최소화 또는 시스템 종료 시 onDisappear가 작동하지 않을 수 있음.
  • 주요 사용 사례 — 타이머 취소, WebSocket 종료, 상태 저장, 체류 시간 추적.
  • 분석 — onDisappear가 Date().timeIntervalSince로 계산된 체류 시간과 함께 screen_close 이벤트 전송.
  • 비동기 작업의 경우 — Task {}와 함께 onDisappear 대신 .task 사용 권장. .task가 요청을 자동으로 취소하기 때문.

턴키 방식의 모바일 애플리케이션을 개발해 드립니다

IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.

프로젝트 논의

더 읽어보기