컴바인을 사용하며 진행하던 프로젝트를 SwiftUI로 바꿔보면서 다른 아키텍처를 사용해보려고 했고, 선택한 아키텍처는 TCA입니다. 어떤 형태인지 살펴보고자 합니다.
https://github.com/pointfreeco/swift-composable-architecture
위 링크에서 기본적인 사용에 있어 도메인을 모델링하기 위해 정의해야 하는 몇 가지 타입과 값을 설명합니다. 아래처럼 번역해봤습니다.
MusicVideo
모델이 존재할 때 네트워크 요청을 통해 데이터를 가져오면 그 데이터 자체가 되는 속성이자 상태입니다.Button
의 액션을 떠올릴 수 있습니다.Effect
값을 반환하면서 완료되는 API 요청이 대표적입니다. 어떠한 액션이 있을 때 이에 반응해서 다음 상태로 넘어가는 과정을 나타냅니다.reducer
와 effect
를 실행할 수 있도록 합니다. 또한, 여기에서 상태 변화를 감지하고 UI를 업데이트합니다.한국어 번역 문서가 이미 존재하는데, Environment
한 가지가 추가적으로 설명되어 있습니다. 링크는 인용 아래에 있습니다.
환경(Environment): API 클라이언트나 애널리틱스 클라이언트와 같이 어플리케이션이 필요로 하는 의존성(Dependency)을 가지고 있는 타입입니다.
Reference
https://gist.github.com/pilgwon/ea05e2207ab68bdd1f49dff97b293b17
샘플 앱도 있으며 ToDos 앱을 참고하면서 글을 작성하기로 했습니다. 아래처럼 ReducerProtocol
을 따르는 구조체가 보입니다.
struct Todos: ReducerProtocol {
struct State: Equatable {
}
enum Action: Equatable {
}
}
ReducerProtocol
가 정의된 파일을 살펴보면 주어진 '주어진 액션으로 앱의 현재 상태가 다음 상태로 어떻게 진행되어야 하는지 설명하는 프로토콜'이라고 합니다. 동시에 스토어에 의해 EffectTask
가 이후 어떻게 실행되어야 하는지 설명하는 프로토콜이라고 하기도 합니다. 간단한 구현도 보여주고 있습니다.
struct Feature: ReducerProtocol {
struct State {
var count = 0
}
enum Action {
case decrementButtonTapped
case incrementButtonTapped
}
}
State
와 Action
의 정의가 필요한 것을 추측해볼 수 있습니다. 실제로 아래처럼 작성해보면 MusiocVideos
가 ReducerProtocol
을 따르지 않는다고 합니다.
struct MusicVideos: ReducerProtocol {
}
ReducerProtocol
을 살펴보면 아래처럼 State
, Action
가 있고, Body
라는 것도 있습니다.
public protocol ReducerProtocol<State,Action> {
/// A type that holds the current state of the reducer.
associatedtype State
/// A type that holds all possible actions that cause the ``State`` of the reducer to change
/// and/or kick off a side ``EffectTask`` that can communicate with the outside world.
associatedtype Action
associatedtype _Body
/// A type representing the body of this reducer.
///
/// When you create a custom reducer by implementing the ``body-swift.property-7foai``, Swift
/// infers this type from the value returned.
///
/// If you create a custom reducer by implementing the ``reduce(into:action:)-8yinq``, Swift
/// infers this type to be `Never`.
typealias Body = _Body
}
간단히 State
는 현재 상태를 갖고 있고, Action
은 가능한 경우의 수 만큼 액션을 갖는다고 합니다. Body
설명에 대한 이해는 아직 부족하지만 더 진행해보기로 했습니다.
이전에 구현하려면 MusicVideos
로 돌아와서 아래처럼 작성하면 ReducerProtocol
을 따르지 않는다는 메시지는 사라집니다.
struct MusicVideos: ReducerProtocol {
struct State {
}
enum Action {
}
var body: some ReducerProtocol<State, Action> {
}
}
body
속성의 반환이 없다는 메시지가 나옵니다. 샘플 앱인 ToDo 앱을 따라하면서 다시 아래처럼 작성합니다.
struct MusicVideos: ReducerProtocol {
struct State {
}
enum Action {
case showDetail
}
var body: some ReducerProtocol<State, Action> {
Reduce { state, action in
switch action {
case .showDetail:
return .none
}
}
}
}
Reduce
라는 것이 보입니다. 다음과 같이 구조체로 정의되어 있습니다. 한 가지 특징이 더 있다면 위 코드처럼 State
가 구현되어 있는 구조체에서 선언하면 Reduce
선언의 클로저 부분 state
, action
의 타입은 각각 MusicVideos.State
, MusicVideos.Action
입니다.
public struct Reduce<State, Action>: ReducerProtocol
다시 ToDos 샘플 앱을 살펴보려고 합니다. 코드의 일부를 보려고 합니다. 열거형인 Action
에 구현된 동작 각각을 Reduce
블록 아래에서 state
를 동작에 맞도록 변경해주고 있습니다. 즉 Reduce
를 통해서 Action
에 따라 State
를 관리합니다.
struct Todos: ReducerProtocol {
struct State: Equatable {
var todos: IdentifiedArrayOf<Todo.State> = []
}
enum Action: Equatable {
case addTodoButtonTapped
case delete(IndexSet)
}
var body: some ReducerProtocol<State, Action> {
Reduce { state, action in
switch action {
case .addTodoButtonTapped:
state.todos.insert(Todo.State(id: self.uuid()), at: 0)
return .none
case let .delete(indexSet):
let filteredTodos = state.filteredTodos
for index in indexSet {
state.todos.remove(id: filteredTodos[index].id)
}
return .none
}
}
}
}
요약하면 도메인 모델을 State
에 두고, 구현한 Action
을 Reduce
부분에서 각 동작에 따라 State
를 관리한다고 생각하면 어느 정도 틀을 이해할 수 있습니다.
마지막으로 샘플 앱에서 'Todo' 파일을 살펴보려고 합니다.
struct Todo: ReducerProtocol {
struct State: Equatable, Identifiable {
var description = ""
let id: UUID
var isComplete = false
}
enum Action: Equatable {
case checkBoxToggled
case textFieldChanged(String)
}
func reduce(into state: inout State, action: Action) -> EffectTask<Action> {
switch action {
case .checkBoxToggled:
state.isComplete.toggle()
return .none
case let .textFieldChanged(description):
state.description = description
return .none
}
}
}
Todos 파일과 다른 점은 변수로 정의해줬던 body
가 없다는 점입니다. ReducerProtocol
은 ReducerProtocol
을 따르는 객체에서 body
가 없는 경우 func reduce(into state: inout State, action: Action) -> EffectTask<Action>
메소드를 정의하면 ReducerProtocol
을 따르도록 구현되어 있습니다.