안녕하세요.
이번에는 대표적인 튜토리얼로 많이 사용되는 카운터를 TCA로 만들어볼게요.
TCA 0.54.0 기준으로 작성했습니다.
전체 코드는 여기를 봐주세요!
초기값 0부터 시작해서 간단하게 +/- 버튼이 있고 버튼을 누를 때마다 값이 증가하고 감소하는 앱을 만들어볼게요.
일반적으로 TCA의 State, Action, Reducer는 ReducerProtocol을 준수하는 구조체 안에 정의를 합니다.
ReducerProtocol을 채택한 Counter 구조체를 만들어줄게요.
그다음은 ReducerProtocol을 채택한 구조체(=Counter) 안에 State, Action, Reducer를 정의할 차례입니다.
일단 State 먼저!!
State는 UI를 그리거나 내부 비즈니스 로직을 수행할 때 필요한 데이터들을 정의한 곳이라고 할 수 있습니다.
이번 예시에선 어떤 값을 화면에 보여줄지에 대한 데이터를 가지고 있으면 되기 때문에 count라는 Int 형 변수를 State 구조체 안에 정의를 해줄게요.
한 가지 명심해야 할 점은 State는 Equatable 프로토콜을 준수해야 합니다.
Action은 사용자 interaction 또는 내부 비즈니스 로직에서 생기는 모든 이벤트들을 의미하고 정의합니다.
이번 예시에선 + 버튼 눌림, - 버튼 눌림이 Action에 해당하기 때문에 Action이란 enum에 정의해 줄게요.
이것도 마찬가지로 Aciton은 Equatable 프로토콜을 준수해야 합니다.
Reducer 차례!
Reducer는 body 라는 ReducerBuilder 변수를 정의하는 방법과 reduce(into:action:) 함수를 정의하는 방법 중에 하나를 선택하면 되는데 이번에는 reduce 함수를 정의하는 방향으로 구현해 볼게요.
[참고] body vs reduce
Reducer 계층이 있다고 했을 때, 보통 body 변수를 정의하는 방법은 자식 Reducer가 포함된 부모 Reducer에서 사용하는 방법이고 reduce(into:action:) 함수를 정의하는 방법은 최하위 자식 Reducer에서 사용하는 방법입니다.
좀 더 구체적인 내용은 여기를 참고해 주세요!
reduce 함수는 Action 이벤트가 발생할 때마다 실행되는 함수라고 볼 수 있습니다. 그래서 switch 구문을 사용해서 Action 마다 다른 동작을 하도록 구현해 주면 됩니다.
(Action에 따라 State 값을 바꿀 수 있게 inout으로 선언된 것 보이시죠?ㅎㅎ)
그러면 return .none이 뭘까 하는 의문이 들 텐데요.
none을 반환한 것은 특정 Action이 왔을 때 추가 동작(Action) 없이 Reducer를 종료시키겠다는 의미입니다.
(지금 예시에선 +/- 버튼이 눌리고 State 변경 후 추가 작업할 것이 없으니 종료한 것이라고 볼 수 있습니다.)
[참고] Reducer를 종료하는 것이 아닌 다른 Action을 수행시키는 경우는 어떻게 해야 하나요?
.none을 반환하는 것이 아닌 run을 반환해 주면 됩니다.
(이건 다음 글에서 자세하게 설명할게요..ㅎㅎ..)
이것으로 TCA의 State, Action, Reducer를 모두 만들어줬습니다.
마지막으로 Store를 만들어볼게요.
Store는 쉽게 말해서 뷰의 State와 Reducer 객체를 저장하고 있는 저장소입니다.
그런데 TCA에선 ViewStore라는 개념이 하나 추가됩니다.
ViewStore는 Store를 관찰함과 동시에 Reducer한테 Action을 전달하는 역할도 합니다.
즉, SwiftUI의 View와 Store를 사이에 위치한다고 보면 될 것 같아요.
(제가 이해한대로 그림을 그려봤는데, 혹시 틀린 부분 있으면 알려주세요!)
코드로 살펴볼게요.
SwiftUI에서 Store를 만들 땐 StoreOf를 사용하고, ViewStore를 만들 땐 WithViewStore를 사용합니다.
WithViewStore의 closure는 ViewBuilder이기 때문에 closure 안에 View를 구현하면 되고, closure의 argument인 ViewStore는 내부 코드를 살펴보면 ObservedObject인 것을 알 수 있습니다.
그리고 viewStore.send를 통해서 ViewStore에서 Reducer로 Action을 전달해 줍니다.
[참고] ViewStore의 State 내부 값에 접근하는데, viewStore.state.count가 아니라 viewStore.count가 가능한 이유가 뭔가요?
dynamicMemberLookup 덕분입니다.
dynamicMemberLookup & KeyPath를 사용하면 dot(.) operator로 멤버에 접근할 수 있게 됩니다.
dynamicMemberLookup에 대해 설명해 놓은 글이 있으니 참고해 주세요.
- https://phillip5094.tistory.com/135
이걸로 View 구현은 끝났고, 이제 마지막으로 View한테 Store 객체를 주입해 줘야겠죠?
아래처럼 State 객체와 Reducer 객체를 생성해서 Store 초기값을 넘겨주면 됩니다.
잘 동작하네요ㅎㅎ
지금까지 TCA 아주 기본 예제를 살펴봤어요.
ReactorKit을 사용해 보신 분들이라면 ReactorKit이랑 매우 유사하다고 느끼실 것 같아요. (실제로도 그렇구요!)
개념 자체는 동일하기 때문에 이해하는데 크게 어려움은 없을 것 같습니다.
앞으로 TCA 관련 예시 코드는 여기에 올려놓을테니, 전체 코드 보고 싶으신 분들은 참고해주세요!
# 참고
- https://github.com/pointfreeco/swift-composable-architecture
이번 글은 여기서 마무리.
'SwiftUI' 카테고리의 다른 글
TCA(3) : _printChanges (0) | 2023.06.24 |
---|---|
TCA(2) : TaskResult, run (0) | 2023.06.20 |
TCA(0) : The Composable Architecture 개요 (0) | 2023.06.07 |
SwiftUI View Lifecycle에 대해 알아보자. (0) | 2023.05.28 |
overlay + matchedGeometryEffect로 Hero Anmiation 비슷하게 만들기 (0) | 2023.05.24 |