SwiftUI Study – NavigationStack을 안정적으로 설계하는 법 (중복 Push·잘못된 경로·상태 꼬임 방지)
Dev Study/SwiftUI 2025. 12. 9. 10:02반응형
SwiftUI Study – NavigationStack을 안정적으로 설계하는 법 (중복 Push·잘못된 경로·상태 꼬임 방지)
1. 왜 중요한가 (문제 배경)
NavigationStack은 SwiftUI 4 이후 기본 네비게이션 구조가 되었지만, 다음과 같은 오류가 자주 발생합니다.
- 화면이 두 번 push 되는 문제
- NavigationPath가 꼬여서 뒤로 가기 동작이 비정상
- 상태 변화로 인해 원치 않는 화면 이동 발생
- 옵저버나 task 실행 타이밍 때문에 중복 push 혹은 pop
- 여러 곳에서 네비게이션을 제어하여 화면 구조가 불안정
문제의 핵심은 다음과 같습니다.
NavigationStack은 “상태 기반 내비게이션”이므로,
화면 이동을 트리거하는 상태가 명확히 한 곳에서만 관리되어야 한다.
Push/pop을 직접 호출하는 imperative 방식과 달리,
SwiftUI 네비게이션은 상태 변화 = 화면 이동 구조이므로 설계를 잘못하면 쉽게 꼬입니다.
2. 잘못된 패턴 예시
❌ 예시 1: 여러 곳에서 NavigationPath를 동시에 수정
struct WrongNavView: View {
@State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
VStack {
Button("A로 이동") {
path.append("A") // ❌ View에서 직접 조작
}
Button("B로 이동") {
moveToB() // ❌ 함수에서 또 조작
}
}
}
}
func moveToB() {
path.append("B")
}
}
문제점
- path를 여러 곳에서 변경하므로 push 타이밍 충돌
- 같은 시점에 두 개 append가 발생하면 중복 Push 오류 가능
- pop 시점도 예측 불가
❌ 예시 2: onAppear에서 화면 이동 발생
.onAppear {
path.append("Detail") // ❌ onAppear는 여러 번 호출될 수 있음
}
문제점
- onAppear는 View 렌더링마다 재실행될 수 있어 무한 Push 발생
- 특히 리스트 셀이나 조건문 안의 뷰에서는 매우 위험
❌ 예시 3: NavigationLink에 동시에 tag와 path push 사용
NavigationLink("Go", value: "A") // ❌ path 방식
NavigationLink("Go", destination: Detail()) // ❌ tag 방식
문제점
- SwiftUI의 두 내비게이션 시스템이 충돌
- 상태와 UI가 서로 다른 기준으로 push/pop 발생
- 특히 iOS 17~18에서 예측 불가한 동작 다수 보고됨
3. 올바른 패턴 예시
✅ 예시 1: 네비게이션은 한 곳에서만 상태로 제어
struct CorrectNavView: View {
@State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
VStack {
Button("A로 이동") {
path.append(Screen.a)
}
Button("B로 이동") {
path.append(Screen.b)
}
}
.navigationDestination(for: Screen.self) { screen in
switch screen {
case .a:
AView()
case .b:
BView()
}
}
}
}
}
enum Screen: Hashable {
case a, b
}
장점
- push/pop 규칙이 명확
- 화면 이동이 상태에 의해 결정되므로 일관적
- 한 곳에서만 path를 관리하므로 꼬일 일이 없음
✅ 예시 2: onAppear 내부에서 push는 금지 → 버튼·Task·ObservableObject로 이동
잘못된 방식:
.onAppear { path.append("A") }
올바른 방식:
.task {
if shouldNavigate {
path.append("A")
}
}
또는 ViewModel 트리거로 이동:
.onReceive(viewModel.$nextScreen) { screen in
if let screen { path.append(screen) }
}
✅ 예시 3: enum + destination 매핑으로 깔끔한 구조 유지
enum Route: Hashable {
case profile(userID: Int)
case settings
}
struct Root: View {
@State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
HomeView(
goProfile: { id in path.append(Route.profile(userID: id)) },
goSettings: { path.append(Route.settings) }
)
.navigationDestination(for: Route.self) { route in
switch route {
case .profile(let id):
ProfileView(id: id)
case .settings:
SettingsView()
}
}
}
}
}
장점
- 타입 안전한 내비게이션
- 화면 전환 로직이 한 곳에서만 존재
- 유지보수 매우 쉬움 (특히 TCA 사용 시 시너지 큼)
4. 실전 적용 팁
✔ 팁 1 – NavigationPath는 반드시 한 곳에서만 관리
여러 곳에서 append/pop 하면 반드시 꼬인다.
✔ 팁 2 – 버튼을 통한 명확한 트리거만 push로 사용
onAppear / onChange 등 생명주기 이벤트에서 push는 금지.
✔ 팁 3 – 화면 이동 조건이 있다면 Task에서 평가
- Task는 뷰가 안정적으로 나타난 뒤 1회 실행
- onAppear보다 훨씬 안정적
✔ 팁 4 – NavigationLink(tag/selection)와 path를 섞어 쓰지 말 것
둘은 완전히 다른 시스템.
✔ 팁 5 – enum 기반 라우팅이 가장 안정적
문자열 기반 route보다 타입 안전 + 유지보수성 모두 우수.
5. 정리
- SwiftUI NavigationStack은 상태 기반이므로 push/pop을 여러 곳에서 제어하면 100% 문제 발생
- NavigationPath는 단일 Source of Truth로 관리해야 한다.
- onAppear에서의 push는 중복 트리거 위험이 커서 금지해야 한다.
- enum 기반 라우팅은 유지보수와 안정성이 가장 뛰어난 구조이다.
- 올바르게 설계하면 UIKit NavigationController보다 더 예측 가능하고 안전한 네비게이션을 구성할 수 있다.
반응형
'Dev Study > SwiftUI' 카테고리의 다른 글
| SwiftUI Study – View 내부에 비즈니스 로직을 넣지 않고 도메인/UseCase로 분리하는 실전 설계 패턴 (0) | 2025.12.09 |
|---|---|
| SwiftUI Study – List 성능과 안정성을 높이는 ForEach·id 선택 기준 (셀 깜빡임·재배치·중복 렌더링 방지) (0) | 2025.12.09 |
| SwiftUI Study – @EnvironmentObject 안전하게 사용하기 (크래시 방지·전역 상태 오염 방지) (0) | 2025.12.09 |
| SwiftUI Study – @StateObject와 @ObservedObject를 정확하게 선택하는 법 (뷰 재생성·초기화 문제 방지) (0) | 2025.12.09 |
| SwiftUI Study – 이미지 로딩·캐싱·리사이징을 효율적으로 처리하는 실전 패턴 (0) | 2025.12.09 |
| SwiftUI Study – ScrollView 안에서 LazyVStack을 사용해 성능을 최적화하는 방법 (0) | 2025.12.09 |
| SwiftUI Study – body 안에서 무거운 연산을 실행하지 않는 이유와 해결법 (0) | 2025.12.09 |
| SwiftUI Study – AnyView 남용을 피하고 타입 안정성을 유지하는 방법 (0) | 2025.12.09 |

