[SwiftUI] NavigationStack 사용 시 화면 전환 안 되는 문제와 title 깨짐 문제 트러블슈팅

2025. 6. 3. 18:52·Apple/SwiftUI
목차
  1. 구현 구조 요약
  2. 문제 1 - 화면 전환이 안 됨
  3. 원인 분석
  4. 해결 방법 - UUID 추가 및 Equatable 커스터마이징
  5. 문제 2 - navigation title 위치가 깨지는 현상
  6. 원인 분석
  7. 처음 시도한 해결 방법 - id()로 뷰 강제 재생성
  8. 최종 해결 방법 - 상태 변화 시 withAnimation 사용
  9. 정리
728x90
반응형

안녕하세요! 피피아노입니다 🎵

 

오늘은 SwiftUI로 앱을 만들던 중 겪은 NavigationStack 관련 트러블슈팅 경험을 작성해보려고 합니다.

 

아기 울음소리를 녹음하고 아기 상태를 분석해주는 기능을 만들면서, 화면전환이 제대로 되지 않거나 navigation title 위치가 깨지는 이상한 버그를 마주했습니다. 

 

구현 구조 요약

우선 제가 구현한 기능의 주요 흐름은 아래와 같습니다.

  1. VoiceRecordView – 분석 시작 화면
  2. CryAnalysisProcessingView – 울음소리를 녹음하고 분석 진행
  3. CryAnalysisResultView – 분석 결과 화면
  4. CryAnalysisResultListView – 분석 기록을 모아보는 리스트 화면

SwiftUI의 NavigationStack을 활용해서 각 화면을 전환하고 있으며, enum 기반 라우팅 구조를 사용하고 있습니다.

enum CryRoute {
    case processing(id: UUID = UUID())
    case result(emotion: EmotionResult, id: UUID = UUID())
}

 

문제 1 - 화면 전환이 안 됨

분석이 끝난 뒤 결과 화면으로 전환하려고 코드에 navigationPath에 결과를 추가 했는데도 아무런 반응 없이 화면은 그대로였습니다. 이 문제가 더 어려웠던 문제는 전환이 될 때도 있고 안 될 때도 있었다는 부분입니다.

 

원인 분석

처음에는 비동기 처리 타이밍이나 뷰 상태 누락이 문제인 줄 알았지만, 로그를 찍어보니 navigationPath에는 정상적으로 경로가 추가되고 있었고, navigationDestination도 잘 호출되고 있었습니다. 또한 어떨 땐 되고, 어떨 땐 안 되는 현상을 보고 해당 문제들은 아닐 것이라고 생각하였습니다.

 

그런데도 화면 전환이 일어나지 않았습니다. 이 현상은 특정 조건에서만 발생했는데, 바로 동일한 분석 결과가 연속적으로 나올 때였습니다.

 

예를 들어, 연속 두 번의 분석 결과가 모두 배고파요(0.95) 같은 동일한 감정이었을 경우, 두 번째 결과 화면이 전환되지 않았습니다. 이 시점에서 SwiftUI 내부 동작에 대한 의문이 생겼고 NavigationStack의 경로 비교 기준을 살펴보았습니다.

 

SwiftUI의 NavigationStack은 내부적으로 Hashable과 Equatable을 통해 경로(navigationPath)를 추적하고, “이전에 이미 같은 화면이 스택에 있었다”고 판단되면 화면 전환을 생략합니다.

 

즉, CryRoute.result(emotion: result)로 넘긴 값이 SwiftUI 입장에서 “이미 push된 적 있는 값”으로 간주되어, 두 번째 전환이 무시된 것이었습니다.

 

해결 방법 - UUID 추가 및 Equatable 커스터마이징

이 문제를 해결하기 위해, 저는 CryRoute.result에 UUID를 함께 포함시켜서 매번 다른 값으로 인식되게 만들었습니다.

navigationPath.append(CryRoute.result(emotion: result, id: UUID()))

 

그리고 enum 비교 기준을 SwiftUI가 원하는 형태로 재정의 하기 위해 Equatable을 명시적으로 구현해주었습니다.

extension CryRoute: Equatable {
    static func == (lhs: CryRoute, rhs: CryRoute) -> Bool {
        switch (lhs, rhs) {
        case (.processing, .processing):
            return true // '진행 중'이라는 의미만 같으면 동일한 경로로 간주
        case (.result(let a, let lhsID), .result(let b, let rhsID)):
            return a.type == b.type &&
                   a.confidence == b.confidence &&
                   lhsID == rhsID // UUID가 다르면 다른 경로로 간주
        default:
            return false
        }
    }
}

 

 

이렇게 함으로써, emotion이 완전히 같더라도 UUID가 다르기 때문에 SwiftUI가 다른 경로라고 인식을 하게 만들어서 화면을 정상적으로 전환할 수 있도록 하였습니다.

 

문제 2 - navigation title 위치가 깨지는 현상

또 다른 문제는 결과 리스트 화면에서 발생했습니다.

 

화면 상단에는 "분석 결과"라는 타이틀이 navigation bar에 나타나야 하는데, 사용자가 뒤로가기 제스처를 시도하다가 중간에 취소하면, 타이틀이 제자리에 있지 않고 결과 리스트의 상단 왼쪽에 크게 고정되는 현상이 발생했습니다.

발생한 문제

 

원인 분석

이 문제는 SwiftUI 내부의 레이아웃 계산 타이밍과 관련된 것으로 보였습니다. 직접 상황을 재현해보고 로그를 찍어보면서 다음과 같은 단서들을 확인할 수 있었습니다.

    • 제스처 중 취소했을 때만 발생
      • 화면을 정상적으로 전환했을 때는 문제 없음
    • 상태 변화가 동시에 발생
      • 이 화면에서는 "선택" 모드를 구현하기 위해 isSelectionMode 상태를 @State로 관리하고 있었음
      • 조건에 따라 .toolbar 내부 UI도 바뀌고 있었고, 이게 애니메이션 도중 겹치면서 레이아웃이 꼬였다고 판단
    • Form 사용
      • CryAnalysisResultListView는 Form 안에 Section과 ForEach로 구성되어 있었는데,
      • Form은 UIKit의 UITableView를 래핑하기 때문에 navigation bar와의 연동이 민감할 수 있다는 글을 봄

 

처음 시도한 해결 방법 - id()로 뷰 강제 재생성

처음에는 navigationTitle이 잘못된 위치에 머무는 현상이 뷰가 제대로 다시 렌더링되지 않아서 발생한 것이라 판단했어요.

SwiftUI에서는 뷰 트리 내부 상태가 변경되더라도, 변화가 크지 않다고 판단되면 기존 레이아웃을 그대로 유지하는 경우가 있기 때문에, 이를 강제로 초기화하기 위해 .id(...)를 사용해봤습니다.

 

이렇게 하면 SwiftUI는 isSelectionMode가 바뀔 때마다 다른 ID로 인식해서 뷰를 새로 생성하게 됩니다.

그 결과, layout 캐시가 초기화되며 navigation title이 잘못된 위치에 남는 문제는 사라졌습니다.

 

하지만 다른 문제가 발생 했습니다.

"선택" 버튼을 눌러 체크 아이콘이 등장할 때, 자연스럽게 슬라이드되던 애니메이션이 사라져버린 것입니다.

이 문제는 .id(...)가 뷰를 완전히 새로 만들기 때문에 애니메이션 컨텍스트가 초기화되기 때문이었습니다.

또한 이런 방식은 상태 관리, 뷰 간 데이터 공유 면에서도 예상치 못한 사이드 이펙트를 낳을 수 있기 때문에 장기적으로 유지하기 어려운 방법이었습니다.

 

최종 해결 방법 - 상태 변화 시 withAnimation 사용

결국 뷰 자체를 새로 만드는 게 아니라, 기존 뷰 안에서 부드럽게 상태만 전환하는 방식으로 가야 한다는 결론을 내렸습니다.

그래서 .id(...)는 제거하고, isSelectionMode 상태가 바뀌는 부분에 withAnimation을 명시적으로 감싸주었습니다.

Button("선택") {
    withAnimation {
        isSelectionMode = true
    }
}

Button("취소") {
    withAnimation {
        selectedItems.removeAll()
        isSelectionMode = false
    }
}

 

또한 navigationTitle과 navigationBarTitleDisplayMode는 항상 명시적으로 고정시켜두었습니다.

.navigationTitle("분석 결과")
.navigationBarTitleDisplayMode(.inline)

 

이렇게 수정을 하니 더 이상 title이 아래로 밀려 내려오지 않고, "선택" 상태 전환 시 체크 아이콘 등장 애니메이션도 문제 없이 동작하게 되었습니다.

 

정리

 이번 이슈들을 겪으면서 SwiftUI의 NavigationStack은 단순한 화면 전환 도구가 아니라, 내부적으로 상태 추적과 비교 연산에 매우 민감한 구조라는 걸 다시 한 번 느꼈습니다. 딱히 큰 실수를 한 것 같지는 않았는데, 내부 동작을 정확하게 이해하지 못하면 왜 안 되는지 해결책을 생각하는 것도 어렵다는 것도 다시 한 번 느꼈습니다.

 

특히 아래 2가지는 기억해두면 좋겠다고 생각했습니다.

  • NavigationStack은 값이 같으면 화면 전환을 생략한다: UUID 같은 고유 값을 넘겨서 새 화면처럼 보이게 만들어야 한다
  • 상태가 바뀌는 시점과 뷰 전환 타이밍이 겹치면 레이아웃이 꼬일 수 있다: 무조건 .id()로 강제 리셋하지 말고 withAnimation 같은 방식으로 자연스럽게 전환하는 게 낫다

이번 경험을 통해 단순히 문제를 고치는 것을 넘어서, 왜 SwiftUI가 이런 방식으로 동작하는지,

그리고 어떻게 하면 뷰 상태와 전환 흐름을 예측 가능한 방식으로 구성할 수 있을지 더 깊이 고민하게 되었습니다.

 

SwiftUI는 뭔가 자동으로 다 해줄 것 같지만, 제대로 동작하게 하려면 개발자가 명확한 기준을 잡아줘야 한다는 점도 다시 느꼈습니다.

똑같은 문제로 오래 고민하시는 분들이 있다면 이 글이 조금이나마 도움이 되면 좋겠습니다!


감사합니다.

 

잘못된 내용이 있거나 더 좋은 내용 피드백은 언제나 환영합니다!

궁금하신 부분은 댓글로 질문 부탁드립니다!

728x90
반응형

'Apple > SwiftUI' 카테고리의 다른 글

[SwiftUI] SwiftUI 상태 동기화 트러블슈팅  (1) 2025.06.25
[SwiftUI] CADisplayLink를 활용한 부드러운 ProgressView 애니메이션 구현 트러블슈팅  (6) 2025.06.06
[SwiftUI] SwiftUI로 카메라 기능 구현하기  (2) 2025.03.01
[SwiftUI] ProgressView 생성하기  (15) 2025.01.15
[SwiftUI] 애니메이션과 전환 간단하게 알아보기  (8) 2024.10.02
  1. 구현 구조 요약
  2. 문제 1 - 화면 전환이 안 됨
  3. 원인 분석
  4. 해결 방법 - UUID 추가 및 Equatable 커스터마이징
  5. 문제 2 - navigation title 위치가 깨지는 현상
  6. 원인 분석
  7. 처음 시도한 해결 방법 - id()로 뷰 강제 재생성
  8. 최종 해결 방법 - 상태 변화 시 withAnimation 사용
  9. 정리
'Apple/SwiftUI' 카테고리의 다른 글
  • [SwiftUI] SwiftUI 상태 동기화 트러블슈팅
  • [SwiftUI] CADisplayLink를 활용한 부드러운 ProgressView 애니메이션 구현 트러블슈팅
  • [SwiftUI] SwiftUI로 카메라 기능 구현하기
  • [SwiftUI] ProgressView 생성하기
P_Piano
P_Piano
Apple 생태계 개발자가 되기 위한 학습과 경험의 기록
    반응형
    250x250
  • P_Piano
    피피아노의 개발 일지
    P_Piano
  • 전체
    오늘
    어제
    • 분류 전체보기 (209)
      • Apple (127)
        • iOS (22)
        • visionOS (4)
        • Swift (68)
        • UIKit (2)
        • SwiftUI (24)
        • RxSwift (2)
        • Xcode (5)
      • C언어 (5)
      • C++ (8)
      • Dart (1)
      • Python (3)
      • JavaScript (17)
      • Git (1)
      • CS (39)
        • 디자인 패턴 (6)
        • 네트워크 (20)
        • 운영체제 (8)
        • Database (5)
        • 자료구조 (0)
      • IT 지식 (2)
      • IT 뉴스 (4)
      • 출처 표기 (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    visionOS
    스위프트
    Apple
    SWIFT
    티스토리챌린지
    swiftUI
    오블완
    배열
    운영체제
    옵셔널
    연산자
    Vision Pro
    비동기
    이니셜라이저
    변수
    디자인패턴
    프로퍼티 래퍼
    코딩테스트
    네트워크
    UIKit
    자바스크립트
    Xcode
    Initializers
    combine
    함수
    ios
    클래스
    프로세스
    제어문
    프로그래머스
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.1
P_Piano
[SwiftUI] NavigationStack 사용 시 화면 전환 안 되는 문제와 title 깨짐 문제 트러블슈팅

개인정보

  • 티스토리 홈
  • 포럼
  • 로그인
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.