반응형

TCA Study - State · Action · Reducer · Store — TCA의 핵심 구조 완전 정복

 

The Composable Architecture(TCA)의 핵심은 단순합니다.
어떤 기능이라도 State(상태), Action(이벤트), Reducer(상태 전이), Store(실행 환경)
이 네 가지 구성 요소만으로 정의합니다.


1. TCA는 왜 4요소(State·Action·Reducer·Store)를 사용하는가?

MVVM, MVC, VIPER, Clean Architecture 등 다양한 아키텍처가 존재해도

대규모 SwiftUI 앱에서는 상태 변경의 일관성이 핵심 문제로 등장합니다.

TCA는 이를 해결하기 위해 다음 전략을 사용합니다.

  • 상태(State): 이 기능이 소유하는 모든 데이터
  • 액션(Action): 무슨 일이 발생했는지 명시
  • 리듀서(Reducer): 상태가 어떻게 변화하는지를 정의
  • 스토어(Store): View와 Reducer를 연결해 실행

이 네 가지를 명확히 구분함으로써,

“어디에서 상태가 바뀌는가?”라는 SwiftUI 개발의 근본 문제를 해결합니다.


2. State — 기능의 모든 상태를 단일 구조로 보관

State는 기능(feature)의 데이터를 모두 담고 있는 구조체입니다.

TCA에서는 최신 문법으로 @ObservableState를 사용합니다.

2.1 State 정의 예제

import ComposableArchitecture

@Reducer
struct CounterFeature {
  @ObservableState
  struct State: Equatable {
    var count = 0
    var isLoading = false
  }
}

State 설계 원칙

  1. 필요한 데이터는 모두 State에 넣는다
  2. 계산된 값은 State에 넣지 않고 computed property로 처리
  3. UI 상태(로딩, 에러 플래그 등)도 State에 포함
  4. View가 값을 직접 보관하지 않도록 한다

SwiftUI에서는 View 내부에 상태가 흩어지기 쉽지만,

TCA에서는 “상태는 반드시 Feature의 State에 존재”해야 합니다.


3. Action — 어떤 이벤트가 발생했는지 선언

Action은 단순하지만 매우 중요한 역할을 합니다.

Action이 명확하게 정의되면 “앱에서 발생하는 모든 이벤트를 추적”할 수 있습니다.

3.1 기본 Action 구조

enum Action {
  case decrementTapped
  case incrementTapped
  case loadButtonTapped
  case responseReceived(Int)
}

Action 설계 원칙

  • 모든 상태 변경은 Action을 통해 발생한다
  • View에서 직접 State를 수정할 수 없다
  • 비동기 응답도 Action으로 전달된다
  • 이벤트는 명확한 이름을 사용한다

예:

load, submit, update, toggle, dismiss 


4. Reducer — 상태 전이 + Effect 정의

Reducer는 TCA의 핵심이며,

State와 Action을 입력받아 새로운 상태를 반환하는 “순수 함수”입니다.

4.1 Reducer 기본 구조

var body: some Reducer<State, Action> {
  Reduce { state, action in
    switch action {
    case .incrementTapped:
      state.count += 1
      return .none

    case .decrementTapped:
      state.count -= 1
      return .none

    case .loadButtonTapped:
      state.isLoading = true
      return .run { send in
        try await Task.sleep(for: .seconds(1))
        await send(.responseReceived(Int.random(in: 0...100)))
      }

    case let .responseReceived(value):
      state.isLoading = false
      state.count = value
      return .none
    }
  }
}

Reducer 설계 원칙

  1. Reducer만이 State를 변경할 수 있다
  2. View에서는 절대 상태를 직접 변경하지 않는다
  3. 비동기 로직은 Reducer 내부에서 .run Effect로 처리한다
  4. Reducer는 테스트 가능해야 한다

이 구조 덕분에 TCA는 매우 예측 가능한 상태 모델을 갖습니다.


5. Effect — 비동기 사이드 이펙트를 안전하게 처리

SwiftUI+MVVM에서는 네트워크 요청을 ViewModel 내부에서 실행하는 경우가 많습니다.

하지만 이렇게 하면 테스트하기 어렵고, 로직이 뷰와 얽혀버립니다.

TCA에서는 모든 비동기 작업이 Effect로 분리됩니다.

5.1 .run Effect 예제

return .run { [count = state.count] send in
  let result = try await api.fetch(count)
  await send(.responseReceived(result))
}

장점:

  • 테스트에서 api.fetch를 쉽게 모킹 가능
  • Reducer에 의해 트리거되므로 로직이 한곳에 모임
  • cancellation 제어가 쉬움

6. Store — View와 Reducer를 연결하는 실행 엔진

Store는 TCA의 런타임입니다.

역할

  1. State를 보관
  2. Action을 Reducer에 전달
  3. Reducer가 반환한 Effect를 실행
  4. 업데이트된 State를 SwiftUI에 Publish

View에서 Store 사용하는 법 (최신 문법)

struct CounterView: View {
  let store: StoreOf<CounterFeature>

  var body: some View {
    VStack {
      Text("Count: \(store.count)")

      Button("+") {
        store.send(.incrementTapped)
      }

      Button("-") {
        store.send(.decrementTapped)
      }
    }
  }
}

Store는 SwiftUI의 상태 관리 시스템과 다르며,

TCA의 모든 기능이 결합되는 중심 역할을 합니다.


7. TCA Feature 전체 코드 요약 (최신 스타일)

TCA Feature는 대부분 다음과 같은 구조를 갖습니다:

@Reducer
struct MyFeature {
  @ObservableState
  struct State: Equatable { ... }

  enum Action {
    case userTapped
    case dataLoaded(Result<String, Error>)
  }

  @Dependency(\.apiClient) var apiClient

  var body: some Reducer<State, Action> {
    Reduce { state, action in
      switch action {
      case .userTapped:
        return .run { send in
          let result = await apiClient.fetch()
          await send(.dataLoaded(result))
        }

      case let .dataLoaded(.success(text)):
        state.text = text
        return .none

      case .dataLoaded(.failure):
        state.errorMessage = "로드 실패"
        return .none
      }
    }
  }
}

8. TCA 구조의 장점 요약

예측 가능한 상태 전이

Reducer가 상태를 변경하므로 디버깅이 쉽습니다.

UI로부터 로직을 완전히 분리

View는 Action만 보내고 State만 읽습니다.

높은 테스트 가능성

TestStore로 모든 흐름을 테스트할 수 있습니다.

대규모 프로젝트에 강함

TCA는 Feature 단위 조합이 자연스럽습니다.

반응형
Posted by 까칠코더
,