# SwiftUI NavigationStack 면접 질문: 2026 내비게이션 패턴 > NavigationStack, NavigationPath, 그리고 최신 SwiftUI 내비게이션 패턴에 관한 핵심 질문으로 iOS 면접을 준비합니다. - Published: 2026-02-24 - Updated: 2026-04-29 - Author: SharpSkill - Tags: swiftui, ios, navigation, interview, navigationstack - Reading time: 12 min --- 내비게이션은 모든 iOS 애플리케이션의 기본 기둥을 구성합니다. iOS 16부터 `NavigationStack`이 `NavigationView`를 대체하며, 내비게이션 스택에 대한 완전한 프로그래밍 제어를 제공합니다. 채용 담당자는 기술 면접에서 이러한 개념의 숙련도를 정기적으로 평가합니다. > **가이드 구성** > > 각 질문은 실제 기술 면접 형식을 그대로 재현하며, 자세한 답변과 동작하는 코드를 함께 제공합니다. 개념은 기초부터 고급까지 단계적으로 진행됩니다. ## NavigationStack의 기초 ### 질문 1: NavigationView와 NavigationStack의 차이점은 무엇입니까? `NavigationView`(iOS 16부터 더 이상 권장되지 않음)는 중첩된 `NavigationLink`를 기반으로 하는 암시적 내비게이션을 만들었습니다. `NavigationStack`은 명시적이며 프로그래밍 방식으로 수정 가능한 내비게이션 스택을 갖춘 선언적 접근 방식을 도입합니다. ```swift // NavigationComparison.swift // ❌ Old pattern with NavigationView (deprecated) struct OldNavigation: View { var body: some View { NavigationView { NavigationLink("Details", destination: DetailView()) } } } // ✅ New pattern with NavigationStack struct ModernNavigation: View { // Navigation stack is explicit and controllable @State private var path = NavigationPath() var body: some View { NavigationStack(path: $path) { List { // NavigationLink with typed value NavigationLink("User 1", value: User(id: 1, name: "Alice")) NavigationLink("User 2", value: User(id: 2, name: "Bob")) } // Destination defined by value type .navigationDestination(for: User.self) { user in UserDetailView(user: user) } } } } ``` 주된 장점은 링크 선언과 목적지를 분리할 수 있다는 점에 있으며, 이를 통해 중앙 집중적이고 테스트 가능한 내비게이션이 가능해집니다. ### 질문 2: NavigationPath는 어떻게 작동합니까? `NavigationPath`는 내비게이션 값을 저장하는 타입 소거 컨테이너입니다. 화면의 정확한 타입을 알지 못해도 스택을 조작할 수 있도록 해주며, 동시에 컴파일 타임의 타입 안전성을 유지합니다. ```swift // NavigationPathBasics.swift struct ContentView: View { // NavigationPath can contain different Hashable types @State private var path = NavigationPath() var body: some View { NavigationStack(path: $path) { VStack(spacing: 20) { Button("View user profile") { // Adds a User to the stack path.append(User(id: 1, name: "Alice")) } Button("View settings") { // Adds a Settings enum to the stack path.append(SettingsRoute.notifications) } Button("Back to root") { // Clears the entire stack path.removeLast(path.count) } } .navigationDestination(for: User.self) { user in UserDetailView(user: user) } .navigationDestination(for: SettingsRoute.self) { route in SettingsView(route: route) } } } } // Types must be Hashable struct User: Hashable { let id: Int let name: String } enum SettingsRoute: Hashable { case notifications case privacy case account } ``` > **타입 소거 그러나 타입 안전** > > `NavigationPath`는 내부적으로 타입 소거를 사용하지만, `navigationDestination`을 통해 컴파일 타임에 타입을 검증합니다. 대응되는 목적지가 없는 값은 런타임에 조용한 오류를 일으킵니다. ### 질문 3: 완전한 프로그래밍 방식의 내비게이션은 어떻게 구현합니까? 프로그래밍 방식의 내비게이션은 사용자의 직접적인 상호작용 없이도 코드의 어느 지점에서나 스택을 제어할 수 있게 해줍니다. 딥 링크, 인증 후 리다이렉션, 다단계 흐름에는 필수적입니다. ```swift // ProgrammaticNavigation.swift // Centralized router to manage navigation @Observable class NavigationRouter { var path = NavigationPath() // Navigate to a specific screen func navigateTo(_ destination: AppRoute) { path.append(destination) } // Go back one level func goBack() { guard !path.isEmpty else { return } path.removeLast() } // Return to root func popToRoot() { path.removeLast(path.count) } // Navigate to a complete stack (deep link) func navigateToPath(_ routes: [AppRoute]) { popToRoot() for route in routes { path.append(route) } } } // Enum defining all app routes enum AppRoute: Hashable { case userList case userDetail(userId: Int) case userEdit(userId: Int) case settings case settingsDetail(SettingsSection) } enum SettingsSection: String, Hashable { case notifications, privacy, account } // Usage in main view struct MainView: View { @State private var router = NavigationRouter() var body: some View { NavigationStack(path: $router.path) { HomeView() .navigationDestination(for: AppRoute.self) { route in destinationView(for: route) } } .environment(router) } @ViewBuilder private func destinationView(for route: AppRoute) -> some View { switch route { case .userList: UserListView() case .userDetail(let userId): UserDetailView(userId: userId) case .userEdit(let userId): UserEditView(userId: userId) case .settings: SettingsView() case .settingsDetail(let section): SettingsDetailView(section: section) } } } ``` 이 패턴은 모든 내비게이션 로직을 중앙으로 모아 단위 테스트와 유지 보수를 더 쉽게 만듭니다. ## 고급 내비게이션 패턴 ### 질문 4: NavigationStack으로 딥 링크는 어떻게 구현합니까? 딥 링크는 외부 URL에서 특정 화면으로 애플리케이션을 직접 열 수 있게 해줍니다. `NavigationStack`을 사용하면 스택을 프로그래밍 방식으로 재구성할 수 있습니다. ```swift // DeepLinkHandler.swift @Observable class DeepLinkHandler { var router: NavigationRouter init(router: NavigationRouter) { self.router = router } // Parse a URL and navigate to destination func handle(url: URL) { guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true), let host = components.host else { return } // Build navigation stack according to URL let routes = parseRoutes(host: host, path: components.path, queryItems: components.queryItems) router.navigateToPath(routes) } private func parseRoutes(host: String, path: String, queryItems: [URLQueryItem]?) -> [AppRoute] { // myapp://users/42/edit → [.userList, .userDetail(42), .userEdit(42)] switch host { case "users": return parseUserPath(path) case "settings": return parseSettingsPath(path) default: return [] } } private func parseUserPath(_ path: String) -> [AppRoute] { let segments = path.split(separator: "/").map(String.init) var routes: [AppRoute] = [.userList] if let userIdString = segments.first, let userId = Int(userIdString) { routes.append(.userDetail(userId: userId)) // /users/42/edit if segments.count > 1 && segments[1] == "edit" { routes.append(.userEdit(userId: userId)) } } return routes } private func parseSettingsPath(_ path: String) -> [AppRoute] { var routes: [AppRoute] = [.settings] let segments = path.split(separator: "/").map(String.init) if let sectionString = segments.first, let section = SettingsSection(rawValue: sectionString) { routes.append(.settingsDetail(section)) } return routes } } // In the main App @main struct MyApp: App { @State private var router = NavigationRouter() @State private var deepLinkHandler: DeepLinkHandler? var body: some Scene { WindowGroup { MainView() .environment(router) .onOpenURL { url in // Handle deep links deepLinkHandler?.handle(url: url) } .onAppear { deepLinkHandler = DeepLinkHandler(router: router) } } } } ``` ### 질문 5: 내비게이션 상태를 어떻게 영구 저장하고 복원합니까? 내비게이션 상태의 영구 저장은 앱이 다시 시작된 후에도 사용자의 위치를 복원할 수 있게 해줍니다. `NavigationPath`는 직렬화를 위해 `Codable`을 지원합니다. ```swift // NavigationPersistence.swift // Extension to make NavigationPath persistable extension NavigationPath { // Encode path to Data func encoded() -> Data? { guard let representation = self.codable else { return nil } return try? JSONEncoder().encode(representation) } // Decode from Data static func decoded(from data: Data) -> NavigationPath? { guard let representation = try? JSONDecoder().decode( NavigationPath.CodableRepresentation.self, from: data ) else { return nil } return NavigationPath(representation) } } // Router with persistence @Observable class PersistentNavigationRouter { var path: NavigationPath { didSet { saveState() } } private let storageKey = "navigation_path" init() { // Restore state at startup if let data = UserDefaults.standard.data(forKey: storageKey), let restored = NavigationPath.decoded(from: data) { self.path = restored } else { self.path = NavigationPath() } } private func saveState() { if let data = path.encoded() { UserDefaults.standard.set(data, forKey: storageKey) } } func clearPersistedState() { UserDefaults.standard.removeObject(forKey: storageKey) } } ``` > **Codable 호환성** > > `NavigationPath.codable`이 동작하려면 경로에 추가되는 모든 타입이 `Hashable` 외에도 `Codable`이어야 합니다. 그렇지 않으면 `codable` 프로퍼티는 `nil`을 반환합니다. ### 질문 6: 인증 흐름과 함께 내비게이션을 어떻게 관리합니까? 인증 흐름은 종종 로그인 후 보호된 화면으로 리다이렉션하거나, 로그아웃 후 로그인 화면으로 돌아가는 동작을 필요로 합니다. 다음 패턴은 이러한 전환을 깔끔하게 처리합니다. ```swift // AuthNavigationFlow.swift enum AuthState { case unauthenticated case authenticated(User) } @Observable class AuthManager { var state: AuthState = .unauthenticated var pendingDeepLink: URL? func login(email: String, password: String) async throws { // Simulated authentication let user = try await AuthService.shared.login(email: email, password: password) state = .authenticated(user) } func logout() { state = .unauthenticated } } struct RootView: View { @State private var authManager = AuthManager() @State private var router = NavigationRouter() var body: some View { Group { switch authManager.state { case .unauthenticated: // Separate navigation stack for auth AuthNavigationStack(authManager: authManager) case .authenticated: // Main app stack MainNavigationStack(router: router, authManager: authManager) } } .onChange(of: authManager.state) { oldState, newState in handleAuthStateChange(from: oldState, to: newState) } } private func handleAuthStateChange(from oldState: AuthState, to newState: AuthState) { switch (oldState, newState) { case (.unauthenticated, .authenticated): // Login successful: process pending deep link if let pendingURL = authManager.pendingDeepLink { DeepLinkHandler(router: router).handle(url: pendingURL) authManager.pendingDeepLink = nil } case (.authenticated, .unauthenticated): // Logout: reset navigation router.popToRoot() default: break } } } struct AuthNavigationStack: View { let authManager: AuthManager @State private var authPath = NavigationPath() var body: some View { NavigationStack(path: $authPath) { LoginView(authManager: authManager) .navigationDestination(for: AuthRoute.self) { route in switch route { case .register: RegisterView(authManager: authManager) case .forgotPassword: ForgotPasswordView() } } } } } enum AuthRoute: Hashable { case register case forgotPassword } ``` ### 질문 7: NavigationStack으로 모달 내비게이션은 어떻게 구현합니까? 모달과 시트는 자체적인 내비게이션 컨텍스트가 필요합니다. `NavigationStack`을 모달 표시와 결합하려면 분리된 상태 관리가 요구됩니다. ```swift // ModalNavigation.swift struct ParentView: View { @State private var mainPath = NavigationPath() @State private var showSettings = false @State private var showUserProfile: User? var body: some View { NavigationStack(path: $mainPath) { ContentView() .toolbar { Button("Settings") { showSettings = true } } .navigationDestination(for: MainRoute.self) { route in MainRouteView(route: route) } } // Sheet with its own NavigationStack .sheet(isPresented: $showSettings) { SettingsSheet() } // Conditional sheet based on item .sheet(item: $showUserProfile) { user in UserProfileSheet(user: user) } } } // Each sheet has its own NavigationStack struct SettingsSheet: View { @Environment(\.dismiss) private var dismiss @State private var settingsPath = NavigationPath() var body: some View { NavigationStack(path: $settingsPath) { SettingsListView() .navigationTitle("Settings") .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Close") { dismiss() } } } .navigationDestination(for: SettingsSection.self) { section in SettingsDetailView(section: section) } } } } // Extension to make User identifiable for sheet(item:) extension User: Identifiable {} ``` ## 상태 관리와 테스트 가능성 ### 질문 8: 내비게이션의 단위 테스트는 어떻게 작성합니까? 테스트 가능성은 `NavigationStack`의 중요한 장점입니다. 라우터를 분리하면 단위 테스트가 UI 없이 내비게이션 로직을 검증할 수 있습니다. ```swift // NavigationTests.swift // Protocol to abstract the router protocol NavigationRouterProtocol { var pathCount: Int { get } func navigateTo(_ destination: AppRoute) func goBack() func popToRoot() } // Concrete implementation @Observable class AppNavigationRouter: NavigationRouterProtocol { var path = NavigationPath() var pathCount: Int { path.count } func navigateTo(_ destination: AppRoute) { path.append(destination) } func goBack() { guard !path.isEmpty else { return } path.removeLast() } func popToRoot() { path.removeLast(path.count) } } // Unit tests import XCTest final class NavigationRouterTests: XCTestCase { var router: AppNavigationRouter! override func setUp() { router = AppNavigationRouter() } func testNavigateToAddsToPath() { // Given XCTAssertEqual(router.pathCount, 0) // When router.navigateTo(.userDetail(userId: 42)) // Then XCTAssertEqual(router.pathCount, 1) } func testGoBackRemovesLastItem() { // Given router.navigateTo(.userList) router.navigateTo(.userDetail(userId: 1)) XCTAssertEqual(router.pathCount, 2) // When router.goBack() // Then XCTAssertEqual(router.pathCount, 1) } func testPopToRootClearsPath() { // Given router.navigateTo(.userList) router.navigateTo(.userDetail(userId: 1)) router.navigateTo(.userEdit(userId: 1)) XCTAssertEqual(router.pathCount, 3) // When router.popToRoot() // Then XCTAssertEqual(router.pathCount, 0) } func testGoBackOnEmptyPathDoesNothing() { // Given XCTAssertEqual(router.pathCount, 0) // When router.goBack() // Then XCTAssertEqual(router.pathCount, 0) // No crash } } ``` ### 질문 9: 여러 탭이 있는 복잡한 내비게이션 상태는 어떻게 관리합니까? `TabView`를 갖춘 애플리케이션은 탭마다 별도의 내비게이션 상태가 필요합니다. 각 탭은 자신만의 독립적인 스택을 유지합니다. ```swift // TabBasedNavigation.swift // Navigation state for each tab @Observable class TabNavigationState { var homePath = NavigationPath() var searchPath = NavigationPath() var profilePath = NavigationPath() var selectedTab: Tab = .home enum Tab: Hashable { case home, search, profile } func resetCurrentTab() { switch selectedTab { case .home: homePath.removeLast(homePath.count) case .search: searchPath.removeLast(searchPath.count) case .profile: profilePath.removeLast(profilePath.count) } } func resetAllTabs() { homePath.removeLast(homePath.count) searchPath.removeLast(searchPath.count) profilePath.removeLast(profilePath.count) } } struct TabRootView: View { @State private var tabState = TabNavigationState() var body: some View { TabView(selection: $tabState.selectedTab) { // Home tab NavigationStack(path: $tabState.homePath) { HomeView() .navigationDestination(for: HomeRoute.self) { route in HomeRouteView(route: route) } } .tabItem { Label("Home", systemImage: "house") } .tag(TabNavigationState.Tab.home) // Search tab NavigationStack(path: $tabState.searchPath) { SearchView() .navigationDestination(for: SearchRoute.self) { route in SearchRouteView(route: route) } } .tabItem { Label("Search", systemImage: "magnifyingglass") } .tag(TabNavigationState.Tab.search) // Profile tab NavigationStack(path: $tabState.profilePath) { ProfileView() .navigationDestination(for: ProfileRoute.self) { route in ProfileRouteView(route: route) } } .tabItem { Label("Profile", systemImage: "person") } .tag(TabNavigationState.Tab.profile) } .environment(tabState) } } ``` ### 질문 10: NavigationStack의 성능 문제는 어떻게 피합니까? 큰 내비게이션 스택이나 복잡한 목적지는 성능에 영향을 줄 수 있습니다. 여러 기법이 렌더링과 메모리 사용을 최적화합니다. ```swift // NavigationPerformance.swift struct OptimizedNavigationStack: View { @State private var path = NavigationPath() var body: some View { NavigationStack(path: $path) { LazyContentView() // ✅ Lazy-loaded destinations .navigationDestination(for: HeavyRoute.self) { route in // View only created on navigation HeavyDetailView(route: route) } } } } // ✅ View with lazy content loading struct LazyContentView: View { @State private var items: [Item] = [] var body: some View { // LazyVStack only creates visible views ScrollView { LazyVStack(spacing: 12) { ForEach(items) { item in NavigationLink(value: HeavyRoute.detail(item.id)) { ItemRow(item: item) } } } } .task { items = await loadItems() } } } // ✅ Detail with progressive loading struct HeavyDetailView: View { let route: HeavyRoute @State private var data: DetailData? var body: some View { Group { if let data { DetailContent(data: data) } else { ProgressView() } } .task { // Load data only when view appears data = await loadDetailData(for: route) } } } // ❌ Avoid: eager creation of heavy views struct BadNavigationStack: View { let allItems: [Item] var body: some View { NavigationStack { List(allItems) { item in // Creates all destinations immediately NavigationLink { HeavyDetailView(item: item) // ❌ Created upfront } label: { ItemRow(item: item) } } } } } ``` > **성능 모범 사례** > > 인라인 목적지를 가진 `NavigationLink` 대신 항상 `navigationDestination(for:)`를 사용해야 합니다. 전자의 패턴은 내비게이션 시점에만 목적지 뷰를 로드합니다. ## 결론 `NavigationStack`은 완전한 프로그래밍 제어를 제공함으로써 SwiftUI의 내비게이션 관리를 변화시킵니다. 기본 내비게이션부터 딥 링크와 상태 영구 저장까지 이러한 패턴을 숙달하는 것은 면접에서 경험 많은 iOS 개발자를 돋보이게 합니다. ### 점검 체크리스트 - ✅ `NavigationView`와 `NavigationStack`의 차이를 이해합니다 - ✅ 프로그래밍 방식의 내비게이션을 위해 `NavigationPath`를 활용할 수 있습니다 - ✅ 내비게이션을 위한 중앙 집중식 라우터를 구현합니다 - ✅ 스택 재구성을 통해 딥 링크를 처리합니다 - ✅ `Codable`로 내비게이션 상태를 영구 저장하고 복원합니다 - ✅ 인증 흐름을 메인 내비게이션과 분리합니다 - ✅ 모달과 `NavigationStack`을 올바르게 결합합니다 - ✅ 내비게이션 로직에 대한 단위 테스트를 작성합니다 - ✅ `TabView`로 다중 탭 내비게이션을 관리합니다 - ✅ 큰 내비게이션 스택의 성능을 최적화합니다 --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/ko/blog/ios/swiftui-navigationstack-interview-questions