Effect는 Action에 의해 트리거된 Reducer의 리턴 값입니다. 이는 Side-Effect로 인한 예상치 못한 상태 변경을 방지하기 위해 사용됩니다.
컴퓨터 과학에서 Side-Effect이란 함수가 외부 상태를 직접 변경하거나 예측할 수 없는 작업을 말합니다. 예를 들어, 네트워크 통신과 같은 비동기 작업을 진행한다고 가정하겠습니다. escaping-closure를 통해 통신의 결과를 state에 반영한다고 했을 때, 우리는 이를 예측할 수 없습니다.
TCA에서는 이를 해결하기 위해, Action을 통해 외부에서의 어떠한 처리가 발생하면 이를 Effect로 반환하고 Effect는 또 다른 Action을 발행합니다.
이를 통해, 비동기 작업/Side-Effect 분리/ 에러 헨들링 등이 예측 가능하며 작업의 순서를 보장할 수 있게 됩니다.
public struct Effect<Action>: Sendable {
@usableFromInline
enum Operation: Sendable {
case none
case publisher(AnyPublisher<Action, Never>)
case run(TaskPriority? = nil, @Sendable (_ send: Send<Action>) async -> Void)
}
@usableFromInline
let operation: Operation
@usableFromInline
init(operation: Operation) {
self.operation = operation
}
}
Effect는 operation 인자를 통해 결정됩니다.
enum Action {
case fetchDataResponse(Result<String, Error>)
}
// publisher
case .fetchData:
return .publisher(
apiClient.fetchData()
.map { .fetchDataResponse($0) } // 새롭게 발행된 액션
.eraseToAnyPublisher()
)
enum Action {
case dataLoaded(Data)
case dataLoadFailed(Data)
}
// run
case .loadData:
return .run { send in
do {
let data = try await fetchData()
await send(.dataLoaded(data)) // 새롭게 발행된 액션
} catch {
await send(.dataLoadFailed(error)) // 새롭게 발행된 액션
}
}
우리는 이전에, store의 개념에 대해서 학습하였습니다. 하지만, 실제 View와 Store를 바인딩 시켜주는 과정에서 WithViewStore로 store를 감싸주는 것을 확인할 수 있습니다.
struct ContentView: View {
let store: StoreOf<SampleFeature>
var body: some View {
// WithViewStore
WithViewStore(self.store, observe: { $0 }) { viewStore in
HStack {
Button {
viewStore.send(.decrementButtonTapped)
} label: {
Text("sub")
}
Text("\\(viewStore.currentCnt)")
Button {
viewStore.send(.incrementButtonTapped)
} label: {
Text("add")
}
}
.padding()
}
}
}
앱의 규모가 커질수록 한 가지의 스토어에서 모든 것을 관리하기 힘들어집니다. TCA에서는 MultiStore라는 개념을 도입했습니다. 하위 View가 상위 View의 일부 상태를 소유하는 별도의 Store를 연결하는 것이죠. 이때, 상위 View에 의한 하위 View의 불필요한 렌더링을 막기 위해, 중복 방지 기능을 탑지한 ViewStore의 필요성이 대두되었습니다.
//ViewStore 구현부
public final class ViewStore<ViewState, ViewAction>: ObservableObject {
public init<State>(
_ store: Store<State, ViewAction>,
// 원본 state를 ViewState로 반환
observe toViewState: @escaping (_ state: State) -> ViewState,
// 만들어진 ViewState의 스트림에서 중복되는 값을 제거
removeDuplicates isDuplicate: @escaping (_ lhs: ViewState, _ rhs: ViewState) -> Bool
) {
self._send = { store.send($0, originatingFrom: nil) }
self._state = CurrentValueRelay(toViewState(store.state.value))
self._isInvalidated = store._isInvalidated
self.viewCancellable = store.state
.map(toViewState)
.removeDuplicates(by: isDuplicate)
.sink { [weak objectWillChange = self.objectWillChange, weak _state = self._state] in
guard let objectWillChange = objectWillChange, let _state = _state else { return }
objectWillChange.send()
_state.value = $0
}
}
결론적으로, 불필요한 렌더링을 필터링해 앱의 효율성을 높일 수 있습니다.