안녕하세요. 별똥별🌠 입니다.
지난시간에서는 TCA Case Studies의 첫 포문을 여는 Counter case를 공부했었는데요. 이번 시간에도 또다른 case를 통해 Composable Architecter를 공부해보도록 하겠습니다.

전 시간의 Counter는 1개의 Reducer를 통해 View를 구성했는데요. 이 글에서는 Composable Architecture (TCA)를 사용하여 두 개의 Counter를 관리하는 간단한 애플리케이션을 구현하는 방법을 소개합니다. Scope Reducer와 Store Scope를 활용하여 작은 기능을 조합해 큰 기능을 구현하는 방법을 설명합니다.
TwoCounters Reducer는 두 개의 Counter를 독립적으로 관리하며, 각 Counter의 상태와 액션을 하나의 큰 도메인으로 통합합니다.
@Reducer
struct TwoCounters {
@ObservableState
struct State: Equatable {
var counter1 = Counter.State()
var counter2 = Counter.State()
}
enum Action {
case counter1(Counter.Action)
case counter2(Counter.Action)
}
var body: some Reducer<State, Action> {
Scope(state: \.counter1, action: \.counter1) {
Counter()
}
Scope(state: \.counter2, action: \.counter2) {
Counter()
}
}
}
- State : 두 개의 Counter 상태를 포함한 큰 상태를 정의합니다. 각각의 Counter.State가 counter1과 counter2로 관리됩니다.
- Action : 각 Counter의 액션을 포함합니다. 이를 통해 각각의 Counter에서 발생한 액션을 구분하고 처리할 수 있습니다.
- Scope Reducer : Scope를 사용하여 counter1과 counter2의 상태와 액션을 각각 Counter Reducer에 연결합니다. 이를 통해 두 Counter를 독립적으로 관리할 수 있습니다.
다음으로, 두 개의 Counter를 화면에 렌더링하는 View를 구현합니다. Store의 scope 메서드를 사용하여 각 Counter의 상태와 액션을 해당 View에 연결합니다.
struct TwoCountersView: View {
let store: StoreOf<TwoCounters>
var body: some View {
Form {
Section {
AboutView(readMe: readMe)
}
HStack {
Text("Counter 1")
Spacer()
CounterView(store: store.scope(state: \.counter1, action: \.counter1))
}
HStack {
Text("Counter 2")
Spacer()
CounterView(store: store.scope(state: \.counter2, action: \.counter2))
}
}
.buttonStyle(.borderless)
.navigationTitle("Two Counters Demo")
}
}
- store.scope : store.scope를 사용하여 TwoCounters Store에서 각 Counter에 해당하는 상태와 액션을 분리합니다. 이를 통해 CounterView가 개별 상태를 구독하고 액션을 처리할 수 있습니다.
- HStack : 두 개의 Counter를 각각 표시하기 위해 HStack으로 구성했습니다. Text를 통해 구분하고 CounterView를 재사용하여 UI를 간결하게 유지합니다.
- Form : Counter들을 섹션별로 구분하여 깔끔한 레이아웃을 제공합니다.
#Preview를 사용해 빠르게 UI와 동작을 확인합니다. 초기 상태와 Reducer를 설정한 Store를 생성하고 View를 렌더링합니다.
#Preview {
NavigationStack {
TwoCountersView(
store: Store(initialState: TwoCounters.State()) {
TwoCounters()
}
)
}
}

이번 예제에서 TCA의 모듈화와 재사용성을 극대화한 점을 강조합니다.
- Reducer의 모듈화:
작은 기능(Counter)을 정의하고, 이를 조합하여 큰 기능(TwoCounters)을 구성합니다.
Scope를 사용해 상태와 액션을 연결함으로써, 독립적인 기능을 쉽게 관리할 수 있습니다.
- Store의 Scope 활용:
store.scope를 사용하여 상위 Store의 일부 상태와 액션을 하위 View로 전달합니다.
이를 통해 큰 Store를 효율적으로 관리하고, View가 필요한 상태만 구독하도록 만듭니다.
- View의 재사용:
CounterView를 별도로 정의하여 재사용 가능하게 설계했습니다.
이를 통해 코드 중복을 줄이고 유지보수성을 높였습니다.
TCA를 활용하면 상태와 액션을 명확히 분리하고, 작은 기능을 재사용 가능한 컴포넌트로 구성할 수 있습니다. 이번 예제는 작은 기능을 조합하여 더 큰 기능을 구현하는 방법을 보여주며, TCA의 강력한 모듈화와 스코프 기능을 잘 활용하는 사례가 될 것입니다.
이 글이 TCA를 학습하거나 활용하는 데 유용한 가이드가 되길 바랍니다! 😊