반응형

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보다 더 예측 가능하고 안전한 네비게이션을 구성할 수 있다.
반응형
Posted by 까칠코더
,