범위

이번 정리에서 볼 흐름

View -> Action -> Reducer -> State -> View

첫 번째 글에서는 버튼을 누르면 숫자가 바뀌는 매우 카운터 예제를 구현했다

예제 코드

import ComposableArchitecture
import SwiftUI

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

    enum Action {
        case decrementButtonTapped
        case incrementButtonTapped
    }

    var body: some ReducerOf<Self> {
        Reduce { state, action in
            switch action {
            case .decrementButtonTapped:
                state.count -= 1

            case .incrementButtonTapped:
                state.count += 1
            }
            return .none
        }
    }
}

이 예제에서 볼 것.

  • State: 화면을 그리는 데 필요한 값
  • Action: 사용자의 입력이나 이벤트
  • Reducer: Action을 기준으로 State를 변경하는 로직

State

@ObservableState
struct State: Equatable {
    var count = 0
}

State는 현재 Feature가 가지고 있는 상태

카운터 예제에서 필요한 상태는 count 하나뿐이라서 State도 단순하게 유지한다

@ObservableState: Store의 State 변화를 View에서 관찰하기 위한 표시

Equatable은 화면 동작 자체에는 필수가 아니지만 상태 비교와 테스트 확장을 생각하면 붙여두는 편이 나은것 같다. (공식 문서 예제에서도 Equatable을 붙인 예제를 확인할 수 있었다)

Action

enum Action {
    case decrementButtonTapped
    case incrementButtonTapped
}

Action은 상태 변경의 원인이 되는 이벤트

카운터 예제에서는 버튼 탭만 Action으로 둔다

store.send(.incrementButtonTapped)

View는 count를 직접 증가시키지 않는다. View는 Action을 보낸다

Reducer

Reduce { state, action in
    switch action {
    case .decrementButtonTapped:
        state.count -= 1

    case .incrementButtonTapped:
        state.count += 1
    }
    return .none
}

Reducer는 Action을 받아서 State를 어떻게 바꿀지 결정한다

incrementButtonTapped가 들어오면 count를 1 증가시킨다

decrementButtonTapped가 들어오면 count를 1 감소시킨다

여기서 상태 변경 책임은 View가 아니라 Reducer에 있다

return .none은 추가로 실행할 Effect가 없다는 의미

View

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

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

            Button {
                store.send(.incrementButtonTapped)
            } label: {
                Image(systemName: "plus")
            }
        }
    }
}

View의 역할

  • Store에서 State 읽기
  • 사용자 입력을 Action으로 보내기

store.count는 현재 State의 count를 읽는 코드

store.send(.incrementButtonTapped)는 Store에 Action을 보내는 코드

흐름

Button tap
-> store.send(.incrementButtonTapped)
-> Reducer 실행
-> state.count += 1
-> Store의 State 변경
-> View 다시 그림

현재 이해한 TCA의 가장 작은 흐름.

  • View는 State를 읽는다
  • View는 Action을 보낸다
  • Reducer는 Action을 받아 State를 변경한다
  • State가 바뀌면 View가 다시 그려진다

정리

State = 현재 값
Action = 상태 변경의 계기
Reducer = Action에 따른 State 변경 규칙

참고