범위
이번 정리에서 볼 흐름
enum case
-> CaseKeyPath
-> \.caseName 문법
-> Reducer 조합
TCA에서는 Feature를 조합할 때 enum case를 자주 가리킨다
부모 Action 안에 자식 Action을 넣을 때가 있다
부모 State의 optional 값이나 enum case를 자식 Reducer로 좁혀 넘길 때도 있다
이번 글에서 볼 것은 세 가지
@CasePathable
\.caseName
TestStore receive
case path가 필요한 곳
TCA의 Action은 보통 enum이다
enum Action {
case sheet(PresentationAction<Sheet>)
case fetchSettings
}
Reducer를 조합할 때는 부모가 자식 Reducer로 넘길 State와 Action 위치를 명확히 지정해야 한다
부모 State 중 어떤 값이 자식 State인가
부모 Action 중 어떤 case가 자식 Action을 담는가
대부분의 Feature State는 struct이고 그 안의 property는 key path로 가리킬 수 있다
\.sheet
하지만 Action은 enum case다
그래서 enum case를 가리키기 위한 도구가 필요하다
그 도구가 case path다
CaseKeyPath 문법
현재 기준에서는 enum case를 key path 문법으로 표현할 수 있다
Reduce { state, action in
// ...
}
.ifLet(\.child, action: \.child) {
ChildFeature()
}
이때 action: \.child의 \.child는 struct property key path가 아니다
enum case를 가리키는 case key path다
이 문법을 가능하게 해주는 macro가 @CasePathable이다
@Reducer가 자동으로 해주는 일
TCA Feature는 @Reducer를 붙인다
@Reducer
struct Feature {
enum Action {
case child(ChildFeature.Action)
}
}
TCA 문서 기준으로 @Reducer macro는 Feature의 Action enum에 @CasePathable을 자동으로 적용한다
그래서 대부분의 Feature Action에는 직접 @CasePathable을 붙이지 않아도 된다
@Reducer
-> Action enum에 @CasePathable 자동 적용
-> \.child 같은 case key path 문법 사용 가능
Feature의 State가 enum인 경우도 있다
이때는 State에도 @CasePathable과 @dynamicMemberLookup이 같이 적용된다
@dynamicMemberLookup은 dot-chaining으로 enum case 안의 값을 optional하게 꺼낼 수 있게 해준다
\.destination?.editForm
여기서 destination과 editForm은 struct property처럼 보이지만 실제로는 enum case다
@CasePathable이 enum case를 key path처럼 가리키게 해주고 @dynamicMemberLookup이 그 case path를 . 문법으로 이어서 쓸 수 있게 해준다
그래서 enum State를 navigation destination처럼 쓸 때 아래 같은 문법이 가능해진다
$store.scope(\.destination?.editForm, action: \.destination.editForm)
이번 글에서는 우선 Action과 별도 enum에 필요한 @CasePathable 흐름을 본다
직접 붙여야 하는 경우
@Reducer가 모든 enum에 @CasePathable을 붙여주는 것은 아니다
@Reducer가 자동으로 처리하는 것은 Feature의 Action enum이다
그리고 Feature의 State가 enum이라면 그 State도 자동 처리 대상이다
그 밖의 enum을 case key path 문법으로 쓰려면 직접 붙여야 한다
Feature.Action
-> @Reducer가 @CasePathable 자동 적용
Feature.State가 enum인 경우
-> @Reducer가 @CasePathable과 @dynamicMemberLookup 자동 적용
그 밖의 enum
-> case key path가 필요하면 직접 @CasePathable 적용
기준은 enum인지 아닌지가 아니다
Reducer나 View나 TestStore에서 \.detail처럼 case를 가리켜야 하는지가 기준이다
예시
상세 Feature에는 sheet 상태가 enum으로 있다
@ObservableState
@CasePathable
enum SheetState: Equatable {
case info
case detail(DetailFeature.State)
}
이 enum은 sheet가 어떤 내용을 보여줄지 나타낸다
info
-> 정보 sheet
detail
-> 다른 상세 Feature를 담는 sheet
SheetState는 @Reducer가 자동 처리하는 Feature의 최상위 State enum이 아니다
그리고 아래에서 \.detail case key path로 쓰인다
그래서 직접 @CasePathable을 붙인다
Sheet Action
sheet 내부에서 일어나는 Action도 별도 enum으로 둔다
enum Action {
case sheet(PresentationAction<Sheet>)
@CasePathable
enum Sheet {
case tapCloseButton
case detail(DetailFeature.Action)
}
}
여기서 Action 자체는 @Reducer가 처리한다
하지만 nested enum인 Sheet는 직접 @CasePathable을 붙인다
이 enum도 아래에서 \.detail case key path로 쓰이기 때문이다
.ifLet에서 사용
sheet를 열 수 있는 부모 State는 optional로 둔다
@ObservableState
struct State {
@Presents var sheet: SheetState?
}
sheet에서 올라오는 Action은 PresentationAction으로 받는다
enum Action {
case sheet(PresentationAction<Sheet>)
}
부모 Reducer는 sheet State와 sheet Action을 .ifLet으로 연결한다
var body: some ReducerOf<Self> {
Reduce { state, action in
// ...
}
.ifLet(\.$sheet, action: \.sheet) {
DetailSheetReducer()
}
}
여기서 두 key path는 의미가 다르다
\.$sheet
-> @Presents로 감싼 optional sheet State
\.sheet
-> Action.sheet case
action: \.sheet가 가능한 이유는 Action이 case path를 만들 수 있기 때문이다
Feature의 Action은 @Reducer가 @CasePathable로 만들어준다
.ifCaseLet에서 사용
sheet 안에는 다시 상세 Feature가 들어갈 수 있다
private struct DetailSheetReducer: Reducer {
typealias State = DetailFeature.SheetState
typealias Action = DetailFeature.Action.Sheet
var body: some ReducerOf<Self> {
EmptyReducer()
.ifCaseLet(\.detail, action: \.detail) {
DetailFeature()
}
}
}
여기서 \.detail은 두 곳에 쓰인다
state: \.detail
-> SheetState.detail case
action: \.detail
-> Action.Sheet.detail case
.ifCaseLet은 현재 enum State가 특정 case일 때만 자식 Reducer를 실행한다
그리고 해당 case 안의 State와 Action을 자식 Reducer에 전달한다
이 흐름이 가능하려면 SheetState와 Action.Sheet가 모두 case key path를 만들 수 있어야 한다
그래서 둘 다 @CasePathable이 필요하다
View에서의 연결
View에서도 같은 원리로 sheet를 Store scope로 연결한다
.sheet(item: $store.scope(state: \.sheet, action: \.sheet)) { sheetStore in
sheetContent(sheetStore)
}
앞의 \.sheet는 State의 property key path다
뒤의 \.sheet는 Action의 case key path다
둘 다 같은 \. 문법이라 헷갈릴 수 있지만 가리키는 대상이 다르다
TestStore에서의 사용
TCA 문서에서는 TestStore의 receive에서도 case key path 문법을 사용한다
await store.receive(\.timerTick) {
$0.count = 1
}
Effect가 Action을 되돌려 보냈을 때 어떤 Action case가 들어왔는지 \.timerTick으로 검증한다
nested Action도 원리는 같다
await store.receive(\.child)
이때도 child case를 case key path로 가리킨다
더 깊게 중첩된 Action도 case key path로 이어서 표현할 수 있다
await store.receive(\.child.presented.response.success)
이 문법도 중간에 있는 enum들이 @CasePathable이어야 가능하다
정리
@CasePathable은 CasePaths 라이브러리가 enum case를 \.caseName 문법으로 다룰 수 있게 해주는 macro다
Reducer 조합 API는 이 문법으로 State와 Action의 연결 지점을 표현한다
중요한 기준은 enum의 종류가 아니라 그 enum case를 \.detail처럼 가리켜야 하는지다
@Reducer가 자동 처리하지 않는 enum을 case key path로 넘겨야 한다면 직접 @CasePathable을 붙인다