[TCA] 04. TCA의 네비게이션 방식에 대해 알아보자.

2026. 1. 19. 23:30·iOS/TCA
728x90

TCA 공식 문서에 스택 기반 네비게이션 가이드가 있으니 참고하면 좋을 것 같다.

 

Documentation

 

pointfreeco.github.io

 

StackState?

StackState는 TCA에서 네비게이션 스택을 표현하기 위한 상태 타입이다. SwiftUI의 NavigationStack과 함께 사용되어 상태 기반 네비게이션을 구현한다. 쉽게 설명하자면, SwiftUI의 NavigationStack을 상태로 옮겨놓은 것이라고 생각하면 된다.

 

1. 배열과 유사한 구조

var path: StackState<Path.State> = .init()

state.path.append(.detail(.init()))      // push
state.path.removeLast()                  // pop
state.path.removeAll()                   // pop to root
state.path.count                         // 스택 깊이

 

2. 타입 안전성

@Reducer
public enum Path {
  case detail(DetailFeature)
  case settings(SettingsFeature)
  case profile(ProfileFeature)
}

var path: StackState<Path.State> = .init()

state.path.append(.detail(.init()))

state.path.append(.unknown(.init()))  // 정의되지 않은 타입은 컴파일에러 발생

 

왜 StackState를 사용할까?

struct ContentView: View {
  @State private var isDetailActive = false

  var body: some View {
    NavigationStack {
      VStack {
        Button("상세로 이동") {
          isDetailActive = true
        }
      }
      .navigationDestination(isPresented: $isDetailActive) {
        DetailView()
      }
    }
  }
}

기존의 SwiftUI 네비게이션 구조는 View에서 Push와 Pop을 하는, 상태와 네비게이션이 섞이는 구조이다. 이 구조에는 몇 가지 문제가 있다.

 

첫 번째로, 현재 어떤 화면들이 네비게이션 스택에 쌓여있는지 명확하게 파악하기 어렵다. View 계층 구조를 일일이 확인해야만 현재 상태를 확인할 수 있고, 이는 디버깅을 어렵게 만든다. 

@ObservableState
struct State {
  var path: StackState<Path.State> = .init()
  // path = [.detail1, .detail2, .settings] 이 배열만 보면 현재 네비게이션 상태를 정확히 알 수 있음
}

StackState 기반 네비게이션 구조는 네비게이션을 하나의 명확한 상태로 관리하기 때문에, State 안에 있는 path 배열만 확인하면 현재 어떤 화면들이 어떤 순서로 스택에 쌓여있는지 파악할 수 있다. 

 

@Test
func testNavigation() {
  var state = HomeFeature.State()
  
  // Given: 초기 상태
  XCTAssertEqual(state.path.count, 0)
  
  // When: 검색 결과 클릭
  let action = SearchFeature.Action.searchResultTapped(article)
  let effect = SearchFeature().reduce(into: &state, action: action)
  
  // Then: 상세 화면이 스택에 추가됨
  XCTAssertEqual(state.path.count, 1)
  XCTAssertEqual(state.path[0], .detail)
}

 두 번째로, 네비게이션 로직이 View와 강하게 결합되어 있어 UI 없이 독립적인 테스팅이 거의 불가능하다. 하지만 StackState 기반 네비게이션 구조는 독립적인 테스팅이 가능하다. 예를들어, 검색 결과 화면에서 검색 결과를 클릭했을 때 path에 detail이 추가되는가? 를 검증할 수 있다. 

 

StackState를 사용하는 가장 큰 이유는 네비게이션을 예측 가능한 상태로 만들기 위함이다. 상태로 관리되는 네비게이션은 추적할 수 있고, 테스트할 수 있고, 디버깅할 수 있다. 

 

StateState의 전체 구조를 알아보자.

Reducer

@Reducer
public struct HomeFeature {
  @ObservableState
  public struct State {
    var path: StackState<Path.State> = .init()  // 스택 컨테이너
    var searchText: String = ""
  }
  
  public enum Action {
    case path(StackActionOf<Path>)  // 스택 액션
    case searchResultTapped(Article)
  }
  
  public var body: some ReducerOf<Self> {
    Reduce { state, action in
      switch action {
      case let .searchResultTapped(article):
        state.path.append(.detail(.init(article: article)))
        return .none
        
      case .path:
        return .none
      }
    }
    .forEach(\.path, action: \.path)  // 스택 리듀서 연결
  }
  
  // 스택에 들어갈 수 있는 화면/패스들 정의
  @Reducer(state: .equatable, action: .equatable)
  public enum Path {
    case detail(DetailFeature)
    case edit(EditFeature)
    case settings(SettingsFeature)
  }
}

StackState는 크게 세 가지 요소로 구성된다. 먼저 State에 StackState 타입의 path 프로퍼티를 선언한다. 이 path가 실제 네비게이션 스택을 나타내는 배열 역할을 한다. 다음으로, Action에 StackActionOf 타입의 path 액션을 정의한다. 이는 스택 내부의 각 화면에서 발생하는 모든 액션을 처리하기 위한 것이다. 마지막으로, @Reducer 매크로로 Path enum을 정의하여 스택에 들어갈 수 있는 화면 타입들을 명시한다. 

 

View

public struct HomeView {
  @Bindable public var store: StoreOf<HomeFeature>
  
  public init(store: StoreOf<HomeFeature>) {
    self.store = store
  }
}

extension HomeView: View {
  public var body: some View {
    NavigationStack(path: $store.scope(state: \.path, action: \.path)) {
      VStack {
       ... Home View UI
      }
    } destination: { store in
      switch store.case {
        case let .detail(store):
          DetailView(store: store)
        case let .edit(store):
          EditView(store: store)
        case let .settings(store):
          SettingsView(store: store)
      }
    }
  }
}

View에서는 NavigationStack을 사용하고, path 파라미터에 store의 path를 바인딩한다. 위의 예제 코드에서 NavigationStack(path: $store.scope(state: \.path, action: \.path))처럼 작성하면 TCA의 path 상태와 SwiftUI의 NavigationStack이 양방향으로 연결 된다. NavigationStack의 desitination 클로저에 Path enum의 각 case별로 switch문을 작성해 해당하는 화면을 반환한다.

 

+ 최상위 뷰에서만 NavigationStack을 감싸주면 된다.

 

자주 사용하는 메소드

// 추가 (push)
state.path.append(.detail(.init()))

// 마지막 제거 (pop)
state.path.removeLast()

// n개 제거
state.path.removeLast(2)

// 전체 제거 (pop to root)
state.path.removeAll()

// 특정 인덱스 제거
state.path.remove(at: 1)

// 범위 제거
state.path.removeSubrange(1...3)

배열과 유사한 구조를 가지고 있기 때문에, 배열을 관리하거나 제어하는 메소드를 사용하면 된다.

 

dismiss?

StackState 기반 네비게이션에서는 @Dependency(\.dismiss)를 사용하지 않는다. 이유는 아래와 같다.

 

1. dismiss는 SwiftUI의 환경 값을 통해 동작하는 명령형 API이다. 이는 View 레이어에서 직접 네비게이션을 제어한다. TCA의 StackState는 상태가 변경되면 화면이 자동으로 변하는 선언형 방식이다. 이 두 패러다임 사이에서 충돌이 일어날 수 있다.

 

2. dismiss를 호출하면 SwiftUI가 화면을 닫기는 하지만 이것이 state.path에 반영되지 않을 수 있다. 특히 비동기로 동작하는 Effect와 함께 사용할 때 타이밍 이슈가 생길 수 있다. 

 

3. 부모 Feature가 네비게이션을 제어하는 구조와 맞지 않다. dismiss를 자식에서 직접 호출하면 부모는 화면이 닫혔다는 사실조차 모를 수 있다. 

'iOS > TCA' 카테고리의 다른 글

[TCA] 03. TCA의 바인딩 방식을 알아보자.  (0) 2026.01.13
[TCA] 02. Store를 알아보자.  (1) 2026.01.13
[TCA] 1. The Composable Architecture ?  (0) 2025.10.17
'iOS/TCA' 카테고리의 다른 글
  • [TCA] 03. TCA의 바인딩 방식을 알아보자.
  • [TCA] 02. Store를 알아보자.
  • [TCA] 1. The Composable Architecture ?
여성일
여성일
  • 여성일
    성일노트
    여성일
  • 전체
    오늘
    어제
    • 분류 전체보기 N
      • 탭탭 - TapTap
        • 리팩토링
        • 트러블슈팅
        • 개발일지
      • 애플 디벨로퍼 아카데미
        • 챌린지 회고
        • 하루의 날씨
      • Swift Student Challenge 202..
      • AI를 잘쓰는 개발자가 될래요
      • 우리 같이 협업하자
      • ToyProject - 사카마카 (살까말까 고민 ..
      • ToyProject - Book2OnNon (모바..
      • ToyProject - 바꿔조 (환율 계산기)
      • iOS N
        • iOS
        • Vapor
        • Design Pattern
        • CoreData
        • Tuist
        • RxSwift
        • ReactorKit
        • TCA N
      • Swift
        • Swift 기본기
        • UIkit
        • SwiftUI
      • 원티드 프리온보딩 챌린지 iOS 과정
  • 블로그 메뉴

    • 링크

    • 공지사항

    • 인기 글

    • 태그

    • 최근 댓글

    • 최근 글

    • hELLO· Designed By정상우.v4.10.6
    여성일
    [TCA] 04. TCA의 네비게이션 방식에 대해 알아보자.
    상단으로

    티스토리툴바