범위
이번 정리에서 볼 흐름
View
-> StoreOf
-> Action
-> Reducer
-> State
-> StoreOf
-> View
Reducer는 Action을 받아 State를 바꾸는 규칙이다
StoreOf는 그 Reducer를 실제 View에서 사용할 수 있게 잡아주는 타입이다
public typealias StoreOf<R: Reducer> = Store<R.State, R.Action>
Store는 실제 저장소 객체이고 StoreOf<CounterFeature>는 CounterFeature에 맞춘 Store 타입 별칭으로 보면 된다
이번 글에서 볼 것은 세 가지
View가 StoreOf를 가진다
View는 StoreOf에서 State를 읽는다
View는 StoreOf에 Action을 보낸다
이번 예제는 기존 카운터 Feature를 그대로 사용한다
Feature 코드
카운터 Feature
@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
}
}
}
여기까지는 Feature 쪽 코드
아직 View와 연결되지 않았다
State, Action, Reducer는 Feature 안에 있지만 View가 이 값을 쓰려면 StoreOf가 필요하다
StoreOf
View는 Feature를 직접 들고 있지 않는다
View는 StoreOf를 가진다
struct CounterView: View {
let store: StoreOf<CounterFeature>
}
StoreOf<CounterFeature>는 이 Feature에 맞춰진 Store 타입이라는 뜻으로 보면 된다
풀어서 생각하면 이 정도
CounterFeature.State를 가지고 있고
CounterFeature.Action을 받을 수 있는 StoreOf
ViewModel을 쓰던 코드와 비교하면 위치가 보인다
@State var viewModel: CounterViewModel
TCA에서는 View가 ViewModel 대신 StoreOf를 가진다
let store: StoreOf<CounterFeature>
여기서 프로퍼티 이름은 store지만 타입은 StoreOf<CounterFeature>다
실제로 View에서 계속 쓰는 것은 이 store 프로퍼티다
State 읽기
View는 StoreOf를 통해 State를 읽는다
Text("\(store.count)")
count는 CounterFeature.State 안에 있는 값이다
struct State: Equatable {
var count = 0
}
View 입장에서는 store.count처럼 바로 읽는다
State가 바뀌면 StoreOf를 통해 View가 다시 그려진다
Action 보내기
View는 상태를 직접 바꾸지 않는다
버튼을 누르면 StoreOf에 Action을 보낸다
Button {
store.send(.incrementButtonTapped)
} label: {
Image(systemName: "plus")
}
흐름은 이렇게 이어진다
Button tap
-> store.send(.incrementButtonTapped)
-> Reducer
-> state.count += 1
-> StoreOf가 가진 State 변경
-> View 다시 그림
감소 버튼도 같은 구조다
Button {
store.send(.decrementButtonTapped)
} label: {
Image(systemName: "minus")
}
View는 count를 직접 증가시키거나 감소시키지 않는다
View는 Action만 보낸다
Store 생성
StoreOf 프로퍼티에 넘길 Store는 Feature의 초기 State와 Reducer로 만든다
CounterView(
store: Store(initialState: CounterFeature.State()) {
CounterFeature()
}
)
여기서 필요한 것
initialState: 화면이 처음 가질 State
CounterFeature(): Action을 처리할 Reducer
View는 만들어진 Store를 StoreOf<CounterFeature>로 받아서 State를 읽고 Action을 보낸다
StoreOf가 없으면 View는 Feature의 State와 Action에 접근할 수 없다
현재 기준
Feature 안쪽
State
Action
Reducer
Effect
Dependency
View 연결
StoreOf
store.send
State 값 읽기
StoreOf는 Reducer의 내부 규칙이라기보다 만들어진 Feature를 View에서 실제로 사용하게 해주는 연결부다
정리
StoreOf
Store = Feature의 State를 보관하고 Action을 Reducer로 보내는 실제 객체
StoreOf<Feature> = 특정 Feature에 맞춰진 Store 타입
View의 역할은 단순하게
State 읽기
Action 보내기
ViewModel을 TCA로 바꿀 때 가장 먼저 바뀌는 모양
ViewModel을 들던 View
-> StoreOf<Feature>를 드는 View
카운터 예제 기준으로는 이렇게 정리한다
CounterView
-> StoreOf<CounterFeature>
-> store.count 읽기
-> store.send(.incrementButtonTapped)