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 설계 원칙
- 필요한 데이터는 모두 State에 넣는다
- 계산된 값은 State에 넣지 않고 computed property로 처리
- UI 상태(로딩, 에러 플래그 등)도 State에 포함
- 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 설계 원칙
- Reducer만이 State를 변경할 수 있다
- View에서는 절대 상태를 직접 변경하지 않는다
- 비동기 로직은 Reducer 내부에서 .run Effect로 처리한다
- 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의 런타임입니다.
역할
- State를 보관
- Action을 Reducer에 전달
- Reducer가 반환한 Effect를 실행
- 업데이트된 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 단위 조합이 자연스럽습니다.
'Dev Study > Swift' 카테고리의 다른 글
| TCA Study - Swift 개발자가 TCA를 알아야 하는 이유 (0) | 2025.12.16 |
|---|---|
| Swift에서 정렬: sort vs sorted (1) | 2025.11.11 |
| Swift에서 옵셔널 기본값: 중첩 if let vs a ?? b (0) | 2025.11.11 |
| Swift에서 배열 초기화: 반복 append vs Array(repeating:count:) (0) | 2025.11.11 |
| Swift에서 문자열 비교 (0) | 2025.11.11 |
| Swift에서 ARC 최적화: weak vs unowned (0) | 2025.11.11 |
| Swift에서 @inlinable / @inline(__always) (0) | 2025.11.11 |
| Swift에서 구조체(Value Type) 기반 설계 (0) | 2025.11.11 |


