[SwiftUI][TCA] TCA Case Studies - 01. Counter

별똥별·2025년 1월 21일

TCA

목록 보기
8/24
post-thumbnail

안녕하세요. 별똥별🌠 입니다.
지난시간까지 TCA tutorial에서 제공되는 Tutorial을 학습해 보았는데요.
이번 시간부터는 Composable Atchitecture Github 라이브러리에 포함되어 있는 examples중에 'Case Study' 프로젝트를 하나씩 공부해보는 시간을 가질 예정입니다.

먼저, 첫번째 시간인 만큼 가장 basic부터 차근차근 공부해나가면 되겠죠?
오늘 공부해볼 case는 바로 'Counter'입니다. 이전 튜토리얼에서도 다뤄본 주제인데요. 이번 시간에는 조금 더 상세하게 분석해보도록 합시다!

1. Counter Reducer 정의하기

먼저, Counter Reducer를 정의합니다. Reducer는 앱의 상태(State)와 액션(Action)을 기반으로 상태를 변화시키는 역할을 합니다.

@Reducer
struct Counter {
    @ObservableState
    struct State: Equatable {
        var count = 0
    }
    
    enum Action {
        case decrementButtonTapped
        case incrementButtonTapped
    }
    
    var body: some Reducer<State, Action> {
        Reduce { state, action in
            switch action {
            case .decrementButtonTapped:
                state.count -= 1
                return .none
            case .incrementButtonTapped:
                state.count += 1
                return .none
            }
        }
    }
}
  • State : 애플리케이션에서 관리할 데이터를 정의합니다. 여기서는 count라는 정수를 관리하며, Equatable을 채택하여 상태 비교 및 테스트가 가능하도록 설계했습니다.
  • Action : 상태를 변경할 수 있는 사용자 액션을 정의합니다. 버튼 탭 이벤트에 따라 increment와 decrement를 처리합니다.
  • body : Reduce를 사용해 State와 Action을 연결합니다. 액션에 따라 상태가 업데이트되며, 현재 예제에서는 부수 효과 없이 동작합니다.

2. Counter View 구현하기

Reducer가 준비되었으니, 이를 사용할 View를 작성해보겠습니다. View는 사용자의 입력(버튼 클릭)을 통해 Action을 Store에 전달하고, State를 구독하여 UI를 업데이트합니다.

struct CounterView: View {
    let store: StoreOf<Counter>
    
    var body: some View {
        HStack {
            Button {
                store.send(.decrementButtonTapped)
            } label: {
                Image(systemName: "minus")
            }
            
            Text("\(store.count)")
                .monospaced()
            
            Button {
                store.send(.incrementButtonTapped)
            } label: {
                Image(systemName: "plus")
            }
        }
    }
}
  • Store : Counter Reducer의 상태와 액션을 관리하는 Store를 주입받습니다.
  • store.send : 사용자가 버튼을 클릭하면 Action을 Reducer로 전달하여 상태를 업데이트합니다.
  • store.count : 상태를 읽어 UI에 표시합니다. 여기서는 count 값을 표시합니다.

3. Counter Demo View로 연결하기

최종적으로 CounterView를 앱에 통합하는 데모 뷰를 만들어봅니다. 이 뷰는 상태와 리듀서를 Store로 초기화하고, CounterView를 렌더링합니다.

struct CounterDemoView: View {
    let store: StoreOf<Counter>
    
    var body: some View {
        Form {
            Section {
                AboutView(readMe: readMe)
            }
            
            Section {
                CounterView(store: store)
                    .frame(maxWidth: .infinity)
            }
        }
        .buttonStyle(.borderless)
        .navigationTitle("Counter Demo")
    }
}
  • Store 초기화 : Store(initialState: Counter.State())로 초기 상태를 설정하고, Reducer를 연결합니다.
  • UI 구성 : CounterView를 재사용 가능하도록 별도로 구성한 뒤, Form 안에 추가합니다.
  • Navigation Title : 간단한 제목을 설정하여 뷰를 명확하게 표시합니다.

4. Preview로 확인하기

SwiftUI의 #Preview를 활용해 앱의 UI와 동작을 빠르게 확인할 수 있습니다.

#Preview {
    NavigationStack {
        CounterDemoView(
            store: Store(initialState: Counter.State()) {
                Counter()
            }
        )
    }
}

이미지

최종 결과

위 코드를 통해 TCA의 기본 구조를 활용하여 간단한 Counter 애플리케이션을 구현했습니다. TCA의 핵심은 다음과 같습니다.

State : 앱의 상태를 단일 소스로 관리합니다.
Action : 상태 변경을 유발하는 모든 이벤트를 정의합니다.
Reducer : 상태와 액션을 기반으로 상태를 변화시키는 핵심 로직입니다.
Store : 상태와 액션을 뷰에 전달하여 상태를 구독하고 액션을 전달합니다.

위와 같은 구조를 통해 상태 관리가 쉬워지고, 코드의 재사용성과 테스트 가능성이 높아집니다. TCA를 처음 사용하는 분들께 좋은 시작점이 되기를 바랍니다! 😊

profile
밍밍

0개의 댓글