안녕하세요.
이번에는 TCA의 Scope에 대해서 알아볼게요.
(TCA 0.54.0 기준으로 작성했으며, 전체 코드는 여기를 봐주세요!)
# Scope?
Scope를 사용해서 부모 Reducer에 자식 Reducer를 포함(선언? 정의?)시킬 수 있습니다.
(Scope로 A(부모) Reducer와 B(자식) Reducer를 연결해 준다고 이해하면 될 것 같아요.)
Reducer에서의 부모-자식 관계는 여러 가지가 될 수 있는데요. 큰 Reducer를 작은 Reducer로 쪼개는 것도 부모-자식 관계가 될 수 있고, 뷰 계층에서 부모 뷰의 Reducer와 자식 뷰의 Reducer도 부모-자식 관계가 될 수 있어요.
# TCA에서의 부모-자식 관계 특징
부모-자식 관계를 설정했을 때의 가장 큰 특징은 2가지라고 생각해요.
- 부모가 자식의 State를 포함
- 자식 이벤트(=Action)를 부모한테 전달
부모가 자식의 State를 포함하게 되면, 부모는 현재 자식 State 값이 뭔지 알 수 있고 부모가 원하는 타이밍에 자식 State를 초기화시킬 수 있게 됩니다.
자식 이벤트를 부모한테 전달한다는 의미에 대해 추가로 설명을 드리자면,
Scope를 사용하지 않았을 때, 자식 View에서 이벤트가 발생한 경우 그 이벤트는 자식 Reducer로 곧바로 전달됩니다.
하지만, Scope를 사용해서 부모-자식 관계를 설정한 경우엔 자식 View에서 발생한 이벤트는 자식 Reducer로 이벤트가 전달되고, 전달된 이벤트는 부모 Reducer에게 다시 전달됩니다.
(최종적으로 최상위 부모 Reducer까지 이벤트가 전달됩니다.)
이 메커니즘을 이용해서 부모 <-> 자식 간 이벤트 전달이 가능하게 됩니다.
(이 부분은 다른 글에서 한번 더 다룰게요.^^)
# Scope 사용법
Scope를 사용해서 부모 Reducer와 자식 Reducer 간의 관계를 정의하고 사용하는 방법은 크게 5단계로 진행됩니다.
1) 부모 State에 자식 State를 포함.
2) 부모 Action에 자식 Action을 포함.
3) body 프로퍼티 안에 Scope를 사용해서 자식 Reducer 정의.
4) (필요하면) body 프로퍼티 안에 부모의 비즈니스 로직을 구현.
5) scope 함수를 사용해서 자식 Reducer를 자식 View에 전달.
위에 설명한 5단계를 통해서 Scope로 자식 Reducer를 정의하고 사용하는 예시를 만들어볼게요.
# 예시
TCA(1) : ReducerProtocol, StoreOf, WithViewStore에서 다뤄봤던 Counter View와 Reducer를 활용해서 Counter가 2개 있고 Counter를 초기화시킬 수 있는 버튼이 있는 TwoCounter라는 앱을 만들어 볼까 해요.
부모-자식 간의 특징을 눈으로 직접 확인하기 위해서 Counter1만 TwoCounter의 자식으로 넣어주도록 구현해 봤어요.
(Counter2는 그냥 쌩판 남남인 것으로...)
위에서 설명한 Scope 사용법 5단계를 토대로 구현했는데요. 관련해서 좀 더 설명을 드릴게요.
1️⃣ : 자식 State를 부모 State에 포함
부모가 자식 State 객체를 가지고 있다는 말입니다.
자식 State 안의 프로퍼티가 public 이라면 부모에서 자식 State 내부의 디테일한 정보도 볼 수 있습니다.
그게 아니더라도, 부모가 자식 State 객체를 치환 또는 초기화해서 재할당 시켜 줄 수 있습니다.
2️⃣ : 부모 Action에 자식 Action을 포함
자식 Action이 부모 Action으로 전달되기 때문에, 어떤 Action이 전달되는지 부모 Action에도 정의를 해주는 것입니다.
3️⃣ : body 프로퍼티 안에 Scope를 사용해서 자식 Reducer 정의
Scope를 사용해서 부모와 자식 Reducer를 연결(?) 시켜줍니다.
[주의] 부모 Reducer에서 reduce(into:action:) 함수를 구현해선 안됩니다.
reduce(into:action:) 함수가 body 프로퍼티보다 우선순위가 높아요.
부모 Reducer 안에서 body 프로퍼티와 reduce(into:action:) 함수를 모두 구현하면, Store에서는 항상 reduce(into:action:) 함수만 호출하기 때문에 다른 Reducer를 조합하는 상위 Reducer(부모)에선 body 프로퍼티만 구현되어 있어야 합니다.
[참고] 왜 action은 역슬래시(\)가 아닌 일반 슬래시(/)?
state 타입은 WritableKeyPath이고, action 타입은 CasePath이기 때문입니다.
CasePath에 대해 정리한 글이 있으니 참고해 주세요.
4️⃣ : 부모가 Action을 받았을 때의 비즈니스 로직 구현
부모가 Action을 받았을 때의 내부 로직은 Reduce를 사용해서 구현합니다.
[주의] Scope는 항상 Reduce 보다 위에 있어야 합니다.
Reduce가 Scope보다 위에 있으면, 자식 이벤트를 부모가 먼저 받게 됩니다.
(반대로 Scope가 위에 있으면 자식이 먼저 이벤트를 받습니다.)
부모가 자식보다 이벤트를 먼저 받아서 이것저것 처리한 후에 자식한테 이벤트를 전달하지 않고 종료시켜 버릴 수가 있기 때문에, TCA에선 Scope가 Reduce보다 위에 있는 것을 권장하고 있어요.
(실행할 때 런타임 경고를 보여주고, 테스트 코드에선 실패한다고 합니다.)
[참고] 부모가 자식 이벤트 받아서 처리할 것이 없으면 Reduce는 없어도 돼요!
만약 부모 Reducer가 자식 Reducer를 조합하기만 하고 별도의 비즈니스 로직이 없다면, body 프로퍼티 안에 Reduce는 없어도 상관없어요!
5️⃣ : scope 함수를 사용해서 자식 Reducer를 자식 View에 전달
부모 View에서 scope 함수를 사용해서 자식 State, 자식 Action, 자식 Reducer를 자식 View의 store 인자로 넘겨줍니다.
[참고] ViewStore(self.store).send(.xxx)가 뭐임?
WithViewStore 클로저를 사용하지 않고 Action을 보내고 싶을 때, ViewStore(self.store).send(.xxx)를 사용합니다.
viewStore를 사용하는 가장 큰 이유는 데이터가 변할 때 Observing 하기 위함인데요. (UI에 반영시키기 위해)
예시에선 자식 View가 자식 State를 Observing 하고 있기 때문에, 부모 View에선 viewStore를 사용할 필요가 없습니다.
(WithViewStore를 사용하면, 반강제적으로 코드 indent가 늘어나는 속상함이 있어요.)
이렇게 만든 예시로 위에서 설명한 부모-자식 관계 특징들을 직접 살펴볼게요.
Counter1 버튼을 누르면 Counter Reducer로 이벤트가 전달되었다가 부모한테로 전달되는 반면
독립적으로 구현한 Counter2 버튼을 누르면 Counter Reducer까지만 이벤트가 전달되고 끝납니다.
이것을 토대로, 자식 이벤트는 자식 Reducer를 거쳐 부모에게까지 전달된다는 것을 알 수 있습니다.
또한, Reset 버튼을 눌렀을 때 부모가 자식 State(=Counter1의 State)를 초기화시켜 줄 수도 있습니다.
이것으로 Scope를 사용해서 부모 Reducer 안에 자식 Reducer를 포함시키는 방법에 대해 알아보았는데요.
주저리주저리 말은 많았지만... Scope를 사용해서 부모-자식 Reducer를 선언하는 것은 복잡한 것 같으면서도 어렵지 않은 내용이니, 직접 몇 번 구현해 보고 이것저것 수정해 보면서 눈으로 확인하면 이해가 좀 더 쉬울 거예요ㅎㅎ
# 요약
- Scope를 사용하면 부모-자식 관계를 설정할 수 있다.
# 참고
이번 글은 여기서 마무리.
'SwiftUI' 카테고리의 다른 글
TCA(6) : WebView (0) | 2023.07.28 |
---|---|
TCA(5) : 부모-자식 간 이벤트 전달 (0) | 2023.07.20 |
navigationBar가 숨김 처리된 상태에서 제스처로 화면 뒤로가기 (0) | 2023.06.28 |
TCA(3) : _printChanges (0) | 2023.06.24 |
TCA(2) : TaskResult, run (0) | 2023.06.20 |