2025년 5월 09일·5분 읽기

예측 가능한 다단계 플로우를 위한 SwiftUI NavigationStack 패턴

예측 가능한 다단계 플로우를 위한 SwiftUI NavigationStack 패턴 — 명확한 라우팅, 안전한 Back 동작, 온보딩/승인 위저드 예제와 실전 팁.

예측 가능한 다단계 플로우를 위한 SwiftUI NavigationStack 패턴

다단계 플로우에서 무엇이 잘못되는가

다단계 플로우는 1단계가 완료되어야 2단계가 의미를 갖는 모든 순서를 뜻합니다. 흔한 예로 온보딩, 승인 요청(검토, 확인, 제출), 여러 화면에 걸쳐 초안을 만드는 위저드형 데이터 입력이 있습니다.

이런 플로우는 Back이 사용자가 기대하는 방식으로 동작할 때만 쉬워 보입니다. Back이 예상과 다른 곳으로 이동하면 사용자는 앱을 신뢰하지 않게 됩니다. 그 결과 잘못된 제출, 중단된 온보딩, “제가 있던 화면으로 돌아갈 수 없어요” 같은 고객지원 문의가 생깁니다.

엉성한 네비게이션은 보통 다음 중 하나로 나타납니다:

  • 앱이 잘못된 화면으로 점프하거나 플로우에서 너무 일찍 빠져나감.
  • 같은 화면이 두 번 나타나서 중복된 푸시가 발생함.
  • Back으로 돌아갔을 때 단계가 리셋되어 사용자가 초안을 잃음.
  • 사용자가 1단계를 완료하지 않아도 3단계에 도달해 무효한 상태가 생성됨.
  • 딥링크나 앱 재시작 후, 올바른 화면은 보이지만 데이터가 잘못되어 있음.

유용한 멘탈 모델: 다단계 플로우는 함께 움직이는 두 가지입니다.

첫째, 사용자가 뒤로 갈 수 있는 화면의 스택. 둘째, 화면이 사라진다고 사라지지 않아야 할 공유 플로우 상태(초안 데이터와 진행 상황).

많은 NavigationStack 설정은 화면 스택과 플로우 상태가 서로 어긋날 때 무너집니다. 예를 들어 온보딩에서 “프로필 생성”을 두 번 푸시하고 초안 프로필이 뷰 내부에 있으면 재렌더링 시 재생성됩니다. 사용자가 Back을 누르면 다른 버전의 폼을 보고 앱이 신뢰할 수 없다고 판단합니다.

예측 가능한 동작은 플로우에 이름을 붙이고, 각 단계에서 Back이 무엇을 해야 하는지 정의하며, 플로우 상태의 단일 거처를 정하는 것에서 시작합니다.

실제로 필요한 NavigationStack 기본

다단계 플로우에는 구형인 NavigationView보다 NavigationStack을 사용하세요. NavigationView는 iOS 버전마다 동작이 달라질 수 있고, 푸시/팝/복원 시 추론하기 어렵습니다. NavigationStack은 네비게이션을 실제 스택처럼 다루는 현대적 API입니다.

NavigationStack은 사용자가 어디를 지나왔는지의 히스토리를 저장합니다. 각 푸시는 스택에 목적지를 추가하고, 각 Back은 하나의 목적지를 팝합니다. 그 단순한 규칙이 플로우를 안정적으로 느껴지게 만듭니다: UI는 명확한 단계 순서를 반영해야 합니다.

스택이 실제로 보관하는 것

SwiftUI는 뷰 객체 자체를 저장하는 것이 아니라 네비게이션에 사용한 데이터(라우트 값)를 저장하고 필요할 때 목적지 뷰를 다시 구성합니다. 이 점은 몇 가지 실무상의 결과를 낳습니다:

  • 중요한 데이터를 유지하려면 뷰가 살아있다고 기대하지 마세요.
  • 화면에 상태가 필요하면 푸시된 뷰 밖에 있는 모델(예: ObservableObject)에 두세요.
  • 같은 목적지를 다른 데이터로 두 번 푸시하면 SwiftUI는 두 개의 다른 스택 항목으로 취급합니다.

플로우가 단순히 한두 번의 고정된 푸시가 아니라면 NavigationPath를 사용하세요. 이는 “우리가 어디로 가고 있는가” 값의 편집 가능한 목록으로 생각하면 됩니다. 앞으로 가려면 route를 append하고, 뒤로 가려면 마지막 route를 제거하거나 전체 경로를 대체해 나중 단계로 점프할 수 있습니다.

위저드형 단계가 필요하거나 완료 후 플로우를 리셋해야 하거나 저장된 상태에서 부분 복원을 원할 때 적합합니다.

예측 가능성이 영리함을 이깁니다. 숨겨진 규칙(자동 점프, 암묵적 팝, 뷰 기반 부작용)이 적을수록 나중에 이상한 Back 스택 버그가 줄어듭니다.

작은 route enum으로 플로우 모델링하기

예측 가능한 네비게이션은 한 가지 결정에서 시작됩니다: 라우팅을 한 곳에 두고, 플로우의 모든 화면을 작고 명확한 값으로 만드세요.

하나의 진실 소스(예: FlowRouter라는 ObservableObject)를 만들어 NavigationPath를 소유하게 하세요. 이렇게 하면 푸시와 팝이 여기저기 흩어지지 않고 일관성을 유지합니다.

간단한 라우터 구조

단계를 나타내는 enum을 사용하세요. 연관 값은 ID 같은 가벼운 식별자만 두고 전체 모델은 두지 마세요.

enum Step: Hashable {
    case welcome
    case profile
    case verifyCode(phoneID: UUID)
    case review(applicationID: UUID)
    case done
}

final class FlowRouter: ObservableObject {
    @Published var path = NavigationPath()

    func go(_ step: Step) { path.append(step) }
    func back() { if !path.isEmpty { path.removeLast() } }
    func reset() { path = NavigationPath() }
}

네비게이션 상태와 플로우 상태를 분리하기

네비게이션은 “사용자가 어디에 있는가”로 취급하고, 플로우 상태는 “지금까지 입력한 것”으로 취급하세요. 플로우 데이터를 OnboardingState 같은 별도의 스토어에 두어 화면이 왔다 갔다 해도 안정적으로 유지되게 하세요.

간단한 규칙:

  • FlowRouter.path에는 오직 Step 값만 둔다.
  • OnboardingState에는 사용자의 입력과 초안 데이터를 둔다.
  • Steps는 데이터를 직접 담지 않고 ID를 통해 조회한다.

이렇게 하면 약한 해싱, 거대한 경로, SwiftUI가 뷰를 재구성할 때 발생하는 놀라운 리셋을 피할 수 있습니다.

단계별: NavigationPath로 위저드 만들기

위저드형 화면에는 스택을 직접 제어하는 것이 가장 단순한 접근입니다. “내가 지금 플로우에서 어디에 있나?”에 대한 진실 소스를 하나로 만들고 앞으로/뒤로 이동하는 방법도 하나로 유지하세요.

NavigationStack(path:)NavigationPath에 바인딩하고 시작하세요. 푸시된 각 화면은 값(보통 enum 케이스)으로 표현되고, 한 번 목적지를 등록합니다.

import SwiftUI

enum WizardRoute: Hashable {
    case profile
    case verifyEmail
    case permissions
    case review
}

struct OnboardingWizard: View {
    @State private var path = NavigationPath()
    @State private var currentIndex = 0

    private let steps: [WizardRoute] = [.profile, .verifyEmail, .permissions, .review]

    var body: some View {
        NavigationStack(path: $path) {
            StartScreen {
                goToStep(0) // push first step
            }
            .navigationDestination(for: WizardRoute.self) { route in
                switch route {
                case .profile:
                    ProfileStep(onNext: { goToStep(1) })
                case .verifyEmail:
                    VerifyEmailStep(onNext: { goToStep(2) })
                case .permissions:
                    PermissionsStep(onNext: { goToStep(3) })
                case .review:
                    ReviewStep(onEditProfile: { popToStep(0) })
                }
            }
        }
    }

    private func goToStep(_ index: Int) {
        currentIndex = index
        path.append(steps[index])
    }

    private func popToStep(_ index: Int) {
        let toRemove = max(0, currentIndex - index)
        if toRemove \u003e 0 { path.removeLast(toRemove) }
        currentIndex = index
    }
}

Back을 예측 가능하게 유지하려면 몇 가지 습관을 지키세요. 진행하려면 정확히 하나의 route만 append하고, “다음”은 선형으로 유지(항상 다음 단계만 푸시)하세요. Review에서 “프로필 편집”처럼 점프해야 할 때는 스택을 알려진 인덱스로 잘라내세요.

이렇게 하면 실수로 중복 화면이 생기는 것을 피하고 Back이 사용자가 기대하는 대로 한 번의 탭에 한 단계씩 이동하게 됩니다.

화면이 오가도 데이터 안정성 유지하기

플로우 로직 중앙화
복잡한 비즈니스 규칙을 흩어진 버튼 핸들러 대신 드래그 앤 드롭 프로세스로 전환하세요.
시작하기

각 화면이 자체 상태를 소유하면 플로우가 믿을 수 없게 느껴집니다. 이름을 입력하고 앞으로 갔다가 뒤로 오면 필드가 비어 있는 이유는 뷰가 재생성되었기 때문입니다.

해결책은 간단합니다: 플로우를 하나의 초안 객체로 취급하고 각 단계가 그것을 편집하게 하세요.

SwiftUI에서는 보통 플로우 시작 시 한 번 생성한 공유 ObservableObject를 각 단계에 전달하는 방식이 흔합니다. 각 뷰의 @State에 초안 값을 두지 마세요(해당 화면에만 진짜 속하는 경우 제외).

final class OnboardingDraft: ObservableObject {
    @Published var fullName = ""
    @Published var email = ""
    @Published var wantsNotifications = false

    var canGoNextFromProfile: Bool {
        !fullName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
        \u0026\u0026 email.contains("@")
    }
}

진입점에서 이를 생성한 뒤 @StateObject@EnvironmentObject(또는 명시적 전달)를 사용해 공유하세요. 그러면 스택이 바뀌어도 데이터는 사라지지 않습니다.

Back 내비게이션에서 무엇을 유지할지 결정하기

모든 것을 영구히 유지할 필요는 없습니다. 규칙을 미리 정하면 플로우가 일관성을 유지합니다.

사용자 입력(텍스트 필드, 토글, 선택)은 명시적으로 리셋하지 않는 한 유지하세요. 단계별 UI 상태(로딩 스피너, 임시 알림, 짧은 애니메이션)는 리셋하세요. 민감한 필드(일회성 코드)는 해당 단계를 떠날 때 지우세요. 선택이 이후 단계를 변경하면 그에 의존하는 필드만 지우세요.

검증은 자연스럽게 여기 들어맞습니다. 사용자를 다음 화면으로 보낸 뒤 오류를 보여주기보다, 현재 단계에서 유효할 때까지 머물게 하세요. canGoNextFromProfile 같은 계산 속성으로 버튼을 비활성화하는 것만으로 충분한 경우가 많습니다.

체크포인트 저장: 과하지 않게

어떤 초안은 메모리에만 두면 되고, 어떤 것은 앱 재시작이나 충돌을 견뎌야 합니다. 실용적인 기본값:

  • 사용자가 적극적으로 단계를 진행하는 동안은 메모리에 보관.
  • 계정 생성, 승인 제출, 결제 시작 같은 명확한 이정표에서 로컬에 영속화.
  • 플로우가 길거나 데이터 입력에 1분 이상 걸리면 더 일찍 영속화.

이렇게 하면 화면이 자유롭게 오가도 사용자의 진행이 안정적으로 느껴집니다.

딥링크와 부분적으로 완료된 플로우 복원

딥링크는 실제 플로우가 자주 1단계에서 시작되지 않기 때문에 중요합니다. 사용자는 이메일, 푸시 또는 공유 링크를 눌러 온보딩의 3단계나 최종 승인 화면에 바로 도달하길 기대합니다.

NavigationStack에서는 딥링크를 한 화면으로 점프하는 명령으로 보지 말고, 유효한 경로를 구성하는 지침으로 처리하세요. 플로우의 시작부터 시작해 이 사용자와 세션에 대해 참인 단계만 append하세요.

외부 링크를 안전한 경로 시퀀스로 변환하기

좋은 패턴은 외부 ID를 파싱하고, 필요한 최소 데이터를 로드한 다음 이를 일련의 라우트로 변환하는 것입니다.

enum Route: Hashable {
    case start
    case profile
    case verifyEmail
    case approve(requestID: String)
}

func pathForDeepLink(requestID: String, hasProfile: Bool, emailVerified: Bool) -\u003e [Route] {
    var routes: [Route] = [.start]
    if !hasProfile { routes.append(.profile) }
    if !emailVerified { routes.append(.verifyEmail) }
    routes.append(.approve(requestID: requestID))
    return routes
}

이 검사들이 가드레일 역할을 합니다. 전제 조건이 없으면 사용자를 3단계로 바로 던져 오류만 보는 상황을 만들지 마세요. 누락된 첫 번째 단계로 보내고 Back 스택이 일관된 이야기를 하도록 하세요.

부분적으로 완료된 플로우 복원하기

재시작 후 복원하려면 두 가지를 저장하세요: 마지막 알려진 라우트 상태와 사용자가 입력한 초안 데이터. 그런 다음 사람을 놀라게 하지 않는 방식으로 재개할지 결정하세요.

초안이 최근(몇 분 또는 몇 시간)이라면 명확한 “이어서하기” 선택지를 제공하세요. 오래된 경우 처음부터 시작하되 필드는 미리 채워두는 것이 중간에 바로 복원하는 것보다 덜 놀랍습니다. 요구사항이 변경됐다면 같은 가드레일을 사용해 경로를 재구성하세요.

Push 대 모달: 플로우를 빠져나오기 쉽게 유지하기

네비게이션과 상태 정렬 유지
화면, 데이터, 로직을 한곳에 설계해 Back이 항상 예상대로 동작하게 하세요.
빌드 시작

플로우는 앞으로 가는 하나의 주 경로가 있을 때 예측 가능하게 느껴집니다: 주요 경로에는 push(스택)를 사용하세요. 시트와 full-screen cover는 보조 작업에 사용하세요.

Push(NavigationStack)는 사용자가 Back으로 단계를 되짚을 것으로 기대할 때 적합합니다. 모달(sheet 또는 fullScreenCover)은 사용자가 부수적인 작업을 하거나 빠른 선택을 하거나 위험한 작업을 확인할 때 적합합니다.

간단한 규칙:

  • 주요 경로(1단계, 2단계, 3단계)는 push 사용.
  • 날짜 선택, 국가 선택, 문서 스캔 같은 작은 선택 작업은 sheet 사용.
  • 로그인, 카메라 캡처, 긴 법적 문서 같은 ‘별세계’는 fullScreenCover 사용.
  • 흐름 취소, 초안 삭제, 승인 제출 같은 확인에는 모달 사용.

일반적 실수는 주요 흐름 화면을 시트에 넣는 것입니다. 2단계를 시트로 만들면 사용자가 스와이프해 닫아 문맥을 잃고, 스택은 자신들이 1단계에 있는 반면 데이터는 2단계를 완료한 것으로 어긋날 수 있습니다.

확인 화면을 반대로 푸시로 넣으면(예: “정말 제출하시겠습니까?”) 위저드 스택을 어지럽히고 루프를 만들 수 있습니다(3 -> Confirm -> Back -> 3 -> Back -> Confirm).

“완료” 후 모든 것을 깔끔하게 닫는 방법

먼저 “완료”가 무엇을 의미하는지 결정하세요: 홈 화면으로 돌아가기, 목록으로 돌아가기, 성공 화면 표시 등.

플로우가 push로 표시되어 있다면 NavigationPath를 비워 스택을 초기 상태로 팝하세요. 모달로 표시되어 있다면 환경의 dismiss()를 호출하세요. 모달 내부에 NavigationStack이 있는 경우에는 각 푸시된 화면을 하나하나 닫지 말고 모달을 닫으세요. 성공적으로 제출한 후에는 초안 상태도 지워서 다시 연 플로우가 새로 시작되도록 하세요.

Back 버튼 동작과 “정말 나가시겠습니까?” 순간

이 패턴들을 실무에 적용하기
여기에 설명한 신뢰할 수 있는 다단계 경험 패턴을 노코드 플랫폼으로 직접 적용해 보세요.
AppMaster 사용해보기

대부분의 다단계 플로우에서는 시스템 Back 버튼(및 스와이프 백 제스처)을 그대로 두는 것이 최선입니다. 사용자가 기대하는 동작과 일치하고, UI가 한 가지를 말하는데 네비게이션 상태가 다른 버그를 피할 수 있습니다.

가로채는 것은 돌아가면 실제로 해를 끼치는 경우(긴 미저장 폼을 잃음, 되돌릴 수 없는 작업 중단)일 때만 가치가 있습니다. 사용자가 안전하게 돌아가서 계속할 수 있다면 마찰을 추가하지 마세요.

실용적 접근법은 시스템 네비게이션을 유지하되, 화면이 “더티”(편집됨)할 때만 한 번 확인을 추가하는 것입니다. 자체 Back 동작을 제공하고 명확한 탈출 경로를 제시하세요.

@Environment(\.dismiss) private var dismiss
@State private var showLeaveConfirm = false
let hasUnsavedChanges: Bool

var body: some View {
  Form { /* fields */ }
    .navigationBarBackButtonHidden(hasUnsavedChanges)
    .toolbar {
      if hasUnsavedChanges {
        ToolbarItem(placement: .navigationBarLeading) {
          Button("Back") { showLeaveConfirm = true }
        }
      }
    }
    .confirmationDialog("Discard changes?", isPresented: $showLeaveConfirm) {
      Button("Discard", role: .destructive) { dismiss() }
      Button("Keep Editing", role: .cancel) {}
    }
}

이것이 함정이 되지 않게 하세요:

  • 결과를 한 문장으로 설명할 수 있을 때만 물어보세요.
  • 안전한 옵션(취소, 편집 유지)과 명확한 종료(버리기, 떠나기)를 제공하세요.
  • 명백한 Back 또는 Close로 대체하지 않는 한 Back 버튼을 숨기지 마세요.
  • 모든 곳에서 네비게이션을 막지 말고 되돌릴 수 없는 작업(예: 승인) 자체를 확인하는 편이 낫습니다.

Back 제스처와 자주 싸우고 있다면 자동 저장, 저장된 초안, 또는 더 작은 단계로 나눌 필요가 있다는 신호입니다.

이상한 Back 스택을 만드는 흔한 실수들

대부분의 “왜 저기로 돌아갔지?” 버그는 SwiftUI가 랜덤해서가 아닙니다. 네비게이션 상태를 불안정하게 만드는 패턴에서 기인합니다. 예측 가능한 동작을 위해 Back 스택을 앱 데이터처럼 취급하세요: 안정적이고, 테스트 가능하며, 한 곳에서 소유하세요.

실수로 생기는 추가 스택들

흔한 함정은 자신도 모르게 둘 이상의 NavigationStack을 만들고 마는 것입니다. 예를 들어 각 탭이 루트 스택을 가지고 있는데 자식 뷰가 플로우 내부에서 또 다른 스택을 추가하면 결과는 혼란스러운 Back 동작, 사라진 내비게이션 바, 또는 기대한 대로 팝되지 않는 화면입니다.

다른 빈번한 문제는 NavigationPath를 너무 자주 재생성하는 것입니다. 경로가 재렌더링되는 뷰 내부에서 생성되면 상태 변화로 경로가 리셋되어 사용자가 필드에 입력한 뒤 갑자기 1단계로 점프할 수 있습니다.

이상한 스택을 만드는 실수들은 대개 단순합니다:

  • 탭이나 시트 콘텐츠 내부에 NavigationStack을 중첩함
  • 뷰 업데이트 중 NavigationPath()를 재초기화해 경로가 리셋됨
  • 변경되는 모델 같은 불안정한 값을 라우트에 넣어 Hashable이 깨짐
  • 버튼 핸들러 곳곳에 네비게이션 결정을 흩어놓아 “다음”이 무엇인지 아무도 설명할 수 없게 됨
  • 여러 주체(예: 뷰 모델과 뷰)가 동시에 path를 변경함

단계 간 데이터를 전달해야 하면 라우트에는 안정적인 식별자(IDs, step enums)를 사용하고 실제 폼 데이터는 공유 상태에 두세요.

구체적 예: 라우트가 .profile(User)이고 User가 사용자가 입력할 때마다 변경되면 SwiftUI는 이를 다른 라우트로 보고 스택을 재연결할 수 있습니다. 라우트는 .profile로 두고 초안 프로필 데이터는 공유 상태에 두세요.

예측 가능한 네비게이션을 위한 빠른 체크리스트

정돈된 위저드 구조 배포
단계와 진행 상태의 단일 소스를 사용해 온보딩 및 승인 위저드를 만드세요.
플로우 구성

플로우가 어색하게 느껴지면 대부분 Back 스택이 사용자의 이야기와 일치하지 않아서입니다. UI를 다듬기 전에 네비게이션 규칙을 빠르게 점검하세요.

실제 기기에서 테스트하고 프리뷰만 보지 마세요. 느린 탭과 빠른 탭을 모두 시도하세요. 빠른 탭은 중복 푸시와 누락된 상태를 드러냅니다.

  • 마지막 화면에서 첫 화면까지 한 단계씩 뒤로 가 보세요. 각 화면이 사용자가 이전에 입력한 동일한 데이터를 보여주는지 확인하세요.
  • 각 단계(첫 단계와 마지막 포함)에서 취소를 트리거해 보세요. 항상 합리적인 장소로 돌아오는지 확인하세요.
  • 플로우 중 강제 종료 후 재실행해 보세요. 경로를 복원하거나 알려진 단계에서 저장된 데이터로 안전하게 재시작할 수 있는지 확인하세요.
  • 딥링크나 앱 바로가기로 플로우를 열어 보세요. 목적지가 유효한 단계인지 확인하고, 필요한 데이터가 없다면 이를 수집할 수 있는 가장 이른 단계로 리디렉션하세요.
  • 완료 후 Done으로 마무리하고 플로우가 깔끔하게 제거되는지 확인하세요. 사용자가 Back을 눌러 완료된 위저드로 다시 들어갈 수 있어서는 안 됩니다.

테스트하는 간단한 방법: 프로필, 권한, 확인의 세 화면 온보딩 위저드를 상상해 보세요. 이름을 입력하고 앞으로 갔다가 뒤로 와서 수정한 다음 딥링크로 확인 단계로 점프하세요. 확인에 옛 이름이 보이거나 Back이 중복된 프로필 화면으로 데려간다면 경로 업데이트가 일관되지 않은 것입니다.

체크리스트를 문제 없이 통과하면 사용자가 떠나고 돌아와도 플로우는 차분하고 예측 가능하게 느껴집니다.

현실적인 예와 다음 단계

비용 청구 승인 플로우를 떠올려 보세요. 검토, 편집, 확인, 영수증의 네 단계가 있습니다. 사용자는 한 가지를 기대합니다: Back은 항상 이전 단계로 가고, 과거에 방문한 무작위 화면으로 가지 않습니다.

간단한 route enum으로 이것을 예측 가능하게 유지하세요. NavigationPath에는 라우트와 상태를 다시 불러오는 데 필요한 작은 식별자들(예: expenseID, mode)만 저장하세요. 큰 가변 모델을 path에 넣으면 복원과 딥링크가 취약해집니다.

작업 초안은 뷰 바깥의 단일 진실 소스(예: @StateObject 플로우 모델 또는 스토어)에 두세요. 각 단계는 그 모델을 읽고 쓰므로 화면이 나타나고 사라져도 입력은 유지됩니다.

최소한으로 추적할 것은 세 가지입니다:

  • 라우트(예: review(expenseID), edit(expenseID), confirm(expenseID), receipt(expenseID))
  • 데이터(라인 항목과 메모가 있는 초안 객체, plus pending, approved, rejected 같은 상태)
  • 위치(플로우 모델의 초안, 서버의 정식 레코드, 로컬에 저장된 소규모 복원 토큰: expenseID + 마지막 단계)

엣지 케이스는 플로우가 신뢰를 얻느냐 잃느냐를 가릅니다. 예컨대 매니저가 확인 단계에서 거부하면 Back이 편집으로 돌아가 수정하게 할지 아니면 플로우를 종료할지 결정하세요. 나중에 돌아오면 저장된 토큰에서 마지막 단계를 복원하고 초안을 다시 로드하세요. 다른 기기로 전환하면 서버를 진실로 삼아 서버 상태로부터 경로를 재구성하세요.

다음 단계: route enum을 문서화하고(각 케이스가 무슨 의미인지, 언제 사용되는지), 경로 구성과 복원 동작에 대한 몇 가지 기본 테스트를 추가하며 한 가지 규칙을 지키세요: 뷰가 네비게이션 결정을 소유하지 않게 하세요.

플로우를 처음부터 직접 작성하지 않고 동일한 종류의 다단계 플로우를 만들고 있다면 AppMaster (appmaster.io) 같은 플랫폼도 동일한 분리를 적용합니다: 단계 네비게이션과 비즈니스 데이터를 분리해 화면이 바뀌어도 사용자의 진행이 깨지지 않도록 합니다.

자주 묻는 질문

다단계 SwiftUI 플로우에서 Back 버튼을 어떻게 예측 가능하게 유지하나요?

NavigationStack을 사용하고 하나의 NavigationPath를 중앙에서 제어하세요. “다음” 동작마다 정확히 하나의 route만 append하고, Back 동작마다 정확히 하나를 pop하도록 하세요. Review에서 “프로필 편집”처럼 점프해야 할 때는 스택을 잘라서 알려진 단계로 이동하고, 무작위로 화면을 더 쌓지 마세요.

내가 뒤로 가면 폼 데이터가 사라지는 이유는 뭔가요?

SwiftUI는 뷰 인스턴스를 그대로 보관하는 것이 아니라 라우트 값을 사용해 목적지 뷰를 재생성합니다. 뷰의 @State에 폼 데이터를 두면 뷰가 재생성될 때 리셋될 수 있습니다. 초안 데이터는 푸시된 뷰 바깥에 있는 공유 모델(예: ObservableObject)에 보관하세요.

스택에 같은 화면이 두 번 보이는 이유는 무엇인가요?

같은 화면이 스택에 두 번 보이는 건 동일한 route를 여러 번 append했기 때문입니다(종종 빠른 탭이나 여러 코드 경로에서 네비게이션을 트리거할 때 발생). Next 버튼을 네비게이팅 중이거나 검증/로딩 중일 때 비활성화하고, 네비게이션 변형은 한 곳에서만 수행해 단계당 한 번만 append되도록 하세요.

라우트 enum에 무엇을 저장하고 무엇을 빼야 하나요?

라우트에는 작고 안정적인 값(예: enum 케이스와 경량 ID)만 두세요. 변경 가능한 데이터(초안)는 별도의 공유 객체에 보관하고, 필요하면 ID로 조회하세요. 큰 가변 모델을 path에 밀어넣으면 Hashable 기대가 깨지고 목적지 매칭이 어긋날 수 있습니다.

네비게이션 상태와 플로우 상태를 깔끔하게 분리하려면 어떻게 하나요?

네비게이션은 “사용자가 어디에 있는가”이고 플로우 상태는 “사용자가 입력한 것”입니다. 라우트는 라우터(또는 최상위 상태)가 하나로 소유하고, 초안은 별도의 ObservableObject가 소유하게 하세요. 각 화면은 초안을 편집하고, 라우터는 오직 단계 전환만 담당합니다.

위저드의 3단계로 들어오는 딥링크를 가장 안전하게 처리하는 방법은?

딥링크를 단순히 한 화면으로 텔레포트하는 명령으로 보지 말고, 유효한 단계 시퀀스를 구성하는 지침으로 처리하세요. 외부 ID를 파싱하고 최소한의 데이터를 로드한 뒤, 이 사용자와 세션에 유효한 전제 조건을 검사해 필요한 선행 단계를 먼저 append한 다음 목표 단계를 append하세요.

앱 재시작 후에 부분적으로 완료된 플로우를 어떻게 복원하나요?

두 가지를 저장하세요: 마지막 의미 있는 라우트(또는 단계 식별자)와 초안 데이터. 재실행 시 딥링크와 동일한 전제 조건 검사로 경로를 재구성하고 초안을 로드합니다. 초안이 오래되었다면 필드만 채워서 처음부터 시작하는 것이 중간에 그대로 복원하는 것보다 덜 놀라울 수 있습니다.

다단계 플로우에서 언제 push하고 언제 모달을 사용해야 하나요?

주요 단계별 경로에는 push를 사용해 Back이 자연스럽게 단계를 추적하게 하세요. 선택적 부가 작업에는 sheet를, 로그인이나 카메라 캡처 같은 별세계에는 fullScreenCover를 사용하세요. 핵심 단계를 모달로 넣으면 스와이프 해제가 상태와 UI를 비동기화시킬 수 있습니다.

‘정말 나가시겠습니까?’ 같은 다이얼로그로 Back 제스처를 덮어써야 하나요?

기본적으로는 시스템 Back 동작을 가로채지 마세요. 떠날 때 저장되지 않은 큰 작업을 잃게 되는 등 실제 해가 있을 때만 확인 대화상자를 추가하세요. 화면이 '더티'할 때만 자체 Back 동작을 제공하고 한 번만 물어보며, 취소(편집 유지)와 명확한 종료(버리기, 떠나기)를 함께 제공하세요.

이상한 Back 스택 버그를 만드는 가장 흔한 실수는 무엇인가요?

일반적인 원인은 여러 NavigationStack을 중첩하거나, 뷰 업데이트 중 NavigationPath를 재생성하거나, 경로를 여러 주체가 동시에 변경하는 것입니다. 플로우당 하나의 스택을 유지하고, 경로는 장수명 상태(@StateObject 또는 라우터)에 보관하며, push/pop 로직은 한 곳에 중앙화하세요.

쉬운 시작
멋진만들기

무료 요금제로 AppMaster를 사용해 보세요.
준비가 되면 적절한 구독을 선택할 수 있습니다.

시작하다