SwiftUI 네비게이션 내부: NavigationStack이 실제로 작동하는 방식
Source: Dev.to
Overview
SwiftUI 네비게이션은 겉보기엔 간단해 보이지만—그렇지 않을 때가 많습니다. 흔히 나타나는 증상은 다음과 같습니다:
- 뷰가 예기치 않게 다시 생성됨
- 네비게이션 스택이 초기화됨
- 뒤로 가기 버튼이 사라짐
- 푸시할 때 상태가 사라짐
- 딥링크가 일관되지 않게 동작함
- 큰 흐름에서 성능 저하
근본 원인은 거의 항상 SwiftUI 네비게이션이 내부적으로 어떻게 동작하는지에 대한 오해입니다. 이 글에서는 NavigationStack을 내부부터 살펴보며—식별자, 경로 차분, 라이프사이클, 그리고 상태—현대 SwiftUI 모델을 사용해 설명합니다.
NavigationStack Basics
SwiftUI 네비게이션은 명령형이 아니라 상태‑주도입니다. “뷰를 푸시”하지 않고, 네비게이션 데이터를 제공합니다.
NavigationStack(path: $path) {
RootView()
}
path는 단순히 라우트 배열입니다:
var path: [Route]
SwiftUI는:
- 이전 경로와 새로운 경로를 비교하고
- 차이를 계산하며
- 삽입·제거를 적용합니다
명시적인 푸시 API는 존재하지 않습니다.
Defining Routes
enum Route: Hashable {
case profile(id: String)
case settings
}
Route Identity
SwiftUI는 라우트를 해시하고, 라우트의 식별자가 네비게이션 스택을 제어합니다. 불안정한 값은 네비게이션을 깨뜨립니다.
잘못된 예 (매번 새로운 식별자):
case profile(id: UUID())
올바른 예 (안정적인 식별자):
case profile(id: user.id)
View Identity vs. Route Identity
- 라우트 식별자 → 네비게이션 스택을 제어
- 뷰 식별자 → 상태 보존을 제어
라우트가 바뀌면 SwiftUI는 목적지를 제거하고, 해당 뷰의 식별자를 파괴합니다. 따라서 @State와 @StateObject가 초기화됩니다. 이는 기대되는 동작입니다.
Managing the Path
경로를 설정하면 전체 스택이 교체됩니다:
path = [.profile(id: "123")]
이렇게 하면 스택이 비워지고 새로운 목적지가 푸시되며, 중간 뷰가 파괴되고 상태와 애니메이션이 초기화됩니다.
추가하려면:
path.append(.profile(id: "123"))
Navigation Destination
.navigationDestination(for: Route.self) { route in
ProfileView(id: route.id)
}
- 클로저가 여러 번 호출됩니다.
- 뷰를 보존하지 않습니다.
- 여기서 상태를 저장하지 마세요.
Correct State Placement
.navigationDestination {
ProfileView(id: id)
}
ProfileView 내부:
@StateObject private var vm = ProfileViewModel(id: id)
또는 부모 스코프(예: ViewModel, AppState)에서 안정적인 인스턴스를 주입합니다.
Lifecycle Implications
내부적으로:
- 푸시 → 뷰가 생성됨
- 팝 → 뷰가 파괴됨
- 재‑푸시 → 완전히 새로운 식별자
따라서:
onAppear가 여러 번 실행될 수 있습니다.onDisappear가 해제를 보장하지는 않습니다..task는 자동으로 취소되므로 부수 효과에는onAppear보다 선호됩니다.
Deep Linking
딥링크는 또 다른 경로 할당에 불과합니다:
path = [.profile(id: "999")]
특별한 API가 필요하지 않습니다. 식별자, 상태 초기화, 라이프사이클에 관한 동일한 규칙이 적용되므로, 중앙 AppState와 결합하면 딥링크가 잘 동작합니다.
Debugging Checklist
- 경로가 변경되었나요?
- 교체했나요, 아니면 추가했나요?
- 라우트 식별자가 바뀌었나요?
- 부모 뷰가 다시 생성되었나요?
- 상태가 뷰 내부에 있었나요, 아니면 상위로 올렸나요?
대부분의 네비게이션 버그는 실제로 상태 관리 버그입니다.
Conceptual Flow
NavigationStack
↓
Path (data)
↓
Diff
↓
View lifecycle
↓
State preserved or destroyed
Conclusion
네비게이션을 데이터 차분으로 바라볼 때 모든 것이 맞아떨어집니다. SwiftUI 네비게이션은:
- 결정적이며
- 상태‑주도이고
- 식별자에 민감합니다
정신 모델이 잘못됐을 때만 “버그가 있다”고 느낍니다. 경로 차분, 라우트 식별자, 뷰 라이프사이클, 그리고 상태 소유권을 이해하면 네비게이션을 예측 가능하고, 테스트 가능하며, 대규모 앱에서도 확장 가능하게 만들 수 있습니다.