범위
이번 정리에서 볼 흐름
ParentFeature
-> Scope
-> ChildFeature
지금까지는 화면 하나에 Feature 하나를 붙였다
이번에는 Feature 안에 다른 Feature를 넣어볼 것이다
예제는 계속 카운터를 사용한다
다만 카운터 자체를 자식 Feature로 두고 부모 Feature가 그 카운터를 화면에 붙인다
이번 글에서 볼 것은 두 가지
Reducer 안의 Scope
View 안의 store.scope
자식 Feature
먼저 기존 카운터와 거의 같은 Feature를 만든다
@Reducer
struct CounterScopeFeature {
@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는 혼자서도 완성된 카운터다
가지고 있는 것
count State
증가 Action
감소 Action
count를 바꾸는 Reducer
부모 Feature
부모 Feature는 자식 Feature의 State를 가진다
@Reducer
struct CounterDashboardFeature {
@ObservableState
struct State: Equatable {
var counter = CounterScopeFeature.State()
}
enum Action {
case counter(CounterScopeFeature.Action)
case resetButtonTapped
}
}
여기서 counter는 자식 Feature의 State다
부모 State 안에 자식 State가 들어간다
CounterDashboardFeature.State
-> counter: CounterScopeFeature.State
-> count
Action도 같은 구조다
case counter(CounterScopeFeature.Action)
자식에서 발생한 Action은 부모 입장에서 .counter(...)로 감싸진다
Child Action
-> CounterScopeFeature.Action.incrementButtonTapped
Parent Action
-> CounterDashboardFeature.Action.counter(.incrementButtonTapped)
Reducer에서 Scope
부모 Reducer는 Scope로 자식 Reducer를 연결한다
var body: some ReducerOf<Self> {
Scope(state: \.counter, action: \.counter) {
CounterScopeFeature()
}
Reduce { state, action in
switch action {
case .counter:
break
case .resetButtonTapped:
state.counter.count = 0
}
return .none
}
}
Scope가 하는 일
부모 State 중 counter만 자식 Reducer에 넘김
부모 Action 중 counter Action만 자식 Reducer에 넘김
즉 이 코드
Scope(state: \.counter, action: \.counter) {
CounterScopeFeature()
}
풀어서 보면 이런 의미다
CounterDashboardFeature.State.counter
-> CounterScopeFeature.State
CounterDashboardFeature.Action.counter
-> CounterScopeFeature.Action
그래서 부모 Reducer의 .counter case에서는 직접 처리할 일이 없다
case .counter:
break
자식 카운터 Action은 CounterScopeFeature가 처리한다
부모는 부모가 가진 Action만 처리하면 된다
case .resetButtonTapped:
state.counter.count = 0
여기서는 부모가 자식 State를 초기화한다
이런 식으로 부모는 자식 Feature를 조합하면서 필요한 상위 동작만 추가할 수 있다
View에서 store.scope
Reducer에서 Scope를 연결했으면 View에서도 자식 View에 자식 Store를 넘겨야 한다
부모 View는 부모 Store를 가진다
struct CounterScopeView: View {
let store: StoreOf<CounterDashboardFeature>
}
자식 View는 자식 Store를 가진다
struct CounterScopeChildView: View {
let store: StoreOf<CounterScopeFeature>
}
부모 Store를 자식 Store로 좁힐 때 store.scope를 쓴다
CounterScopeChildView(
store: store.scope(state: \.counter, action: \.counter)
)
이 코드가 하는 일
부모 StoreOf<CounterDashboardFeature>
-> 자식 StoreOf<CounterScopeFeature>
자식 View 입장에서는 부모가 있다는 사실을 몰라도 된다
자식 View는 그냥 자기 Store만 사용한다
Button {
store.send(.incrementButtonTapped)
} label: {
Image(systemName: "plus")
}
하지만 실제 흐름은 부모 Store를 거친다
ChildView
-> store.send(.incrementButtonTapped)
-> ParentAction.counter(.incrementButtonTapped)
-> Scope
-> CounterScopeFeature
-> state.counter.count += 1
Reducer Scope와 View scope
이 둘은 이름이 비슷하지만 위치가 다르다
Reducer의 Scope
-> 부모 Reducer가 자식 Reducer를 포함하는 구조
View의 store.scope
-> 부모 Store에서 자식 View용 Store를 만드는 구조
둘 다 필요하다
Reducer 쪽 Scope만 있으면 자식 Reducer는 연결되지만 자식 View에 넘길 Store가 없다
View 쪽 store.scope만 있으면 자식 View는 Store를 받을 수 있지만 부모 Reducer가 자식 Action을 처리할 연결이 없다
현재 기준
Scope를 쓰면 Feature를 화면 단위로 나눌 수 있다
중요한 기준은 파일을 작게 쪼개는 것이 아니다
상태와 액션을 가진 기능 단위를 독립된 Feature로 둘 수 있는지가 기준이다
카운터 예제 기준
CounterScopeFeature
-> count를 가진 독립 카운터
CounterDashboardFeature
-> 카운터를 화면에 배치하고 초기화 동작을 추가하는 부모
정리
현재 이해한 Scope
ChildFeature = 독립적으로 State, Action, Reducer를 가진 작은 기능
ParentFeature = ChildFeature.State와 ChildFeature.Action을 포함하는 상위 기능
Reducer Scope = 부모 Reducer 안에서 자식 Reducer를 실행하는 연결
store.scope = 부모 Store를 자식 Store로 좁혀 View에 넘기는 연결
Scope는 작은 Feature를 무조건 많이 만들기 위한 문법이 아니다
이미 독립된 State와 Action을 가진 기능을 상위 화면에 조합하기 위한 문법이다