SwiftUI 네비게이션 내부: NavigationStack이 실제로 작동하는 방식

발행: (2025년 12월 21일 오전 10:55 GMT+9)
6 min read
원문: Dev.to

Source: Dev.to

Overview

SwiftUI 네비게이션은 겉보기엔 간단해 보이지만—그렇지 않을 때가 많습니다. 흔히 나타나는 증상은 다음과 같습니다:

  • 뷰가 예기치 않게 다시 생성됨
  • 네비게이션 스택이 초기화됨
  • 뒤로 가기 버튼이 사라짐
  • 푸시할 때 상태가 사라짐
  • 딥링크가 일관되지 않게 동작함
  • 큰 흐름에서 성능 저하

근본 원인은 거의 항상 SwiftUI 네비게이션이 내부적으로 어떻게 동작하는지에 대한 오해입니다. 이 글에서는 NavigationStack을 내부부터 살펴보며—식별자, 경로 차분, 라이프사이클, 그리고 상태—현대 SwiftUI 모델을 사용해 설명합니다.

SwiftUI 네비게이션은 명령형이 아니라 상태‑주도입니다. “뷰를 푸시”하지 않고, 네비게이션 데이터를 제공합니다.

NavigationStack(path: $path) {
    RootView()
}

path는 단순히 라우트 배열입니다:

var path: [Route]

SwiftUI는:

  1. 이전 경로와 새로운 경로를 비교하고
  2. 차이를 계산하며
  3. 삽입·제거를 적용합니다

명시적인 푸시 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"))
.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 네비게이션은:

  • 결정적이며
  • 상태‑주도이고
  • 식별자에 민감합니다

정신 모델이 잘못됐을 때만 “버그가 있다”고 느낍니다. 경로 차분, 라우트 식별자, 뷰 라이프사이클, 그리고 상태 소유권을 이해하면 네비게이션을 예측 가능하고, 테스트 가능하며, 대규모 앱에서도 확장 가능하게 만들 수 있습니다.

Back to Blog

관련 글

더 보기 »

SwiftUI 제스처 시스템 내부

!Sebastien Latohttps://media2.dev.to/dynamic/image/width=50,height=50,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%...

SwiftUI View Diffing 및 Reconciliation

SwiftUI는 화면을 “다시 그리”는 것이 아닙니다. 뷰 트리를 차이(diff)합니다. SwiftUI가 무엇이 변경되고 무엇이 동일하게 유지되는지를 어떻게 결정하는지 이해하지 못한다면, 불필요한 …

SwiftUI 렌더링 파이프라인 설명

SwiftUI는 렌더링과 관련해서 신비롭게 느껴질 수 있습니다. 단일 상태 변화가 뷰를 다시 렌더링하고, 애니메이션을 재시작하며, 레이아웃을 재계산하고, 그리고 …