Notice
Recent Posts
Recent Comments
Link
«   2025/03   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31
Archives
Today
Total
관리 메뉴

J

[SwiftUI] TCA 공식문서로 시작하기_Reducer 본문

SwiftUI

[SwiftUI] TCA 공식문서로 시작하기_Reducer

yujaehui 2024. 8. 20. 00:36

Reducer

어플리케이션의 현재 상태를 주어진 액션에 따라 다음 상태로 발전시키는 방법을 설명하고, 나중에 스토어에서 실행해야 할 Effect가 있다면 이를 반환하는 프로토콜입니다.


선언

protocol Reducer<State, Action>

개요

이 프로토콜을 준수하면 특정 기능의 도메인, 로직, 동작을 표현할 수 있습니다.

여기서 도메인은 State와 Action을 통해 정의되며, 이들은 해당 구조체 내부에 중첩 타입으로 선언될 수 있습니다.

struct Feature: Reducer {
  struct State {
    var count = 0
  }
  enum Action {
    case decrementButtonTapped
    case incrementButtonTapped
  }

  // ...
}

리듀서 구현

이제 기능의 로직을 정의해 보겠습니다.

리듀서는 특정 액션이 시스템에 전달될 때, 현재 상태를 변경하는 역할을 합니다.

이를 위해 reduce(into:action:) 메서드를 구현합니다.

struct Feature: Reducer {
  // ...

  func reduce(into state: inout State, action: Action) -> Effect<Action> {
    switch action {
    case .decrementButtonTapped:
      state.count -= 1
      return .none

    case .incrementButtonTapped:
      state.count += 1
      return .none
    }
  }
}

Effect 적용

reduce 메서드는 두 가지 역할을 수행합니다.

  1. 특정 액션에 대해 현재 상태를 변경합니다.
  2. 비동기적으로 실행될 Effect를 반환하여, 추가적인 액션을 시스템에 다시 전달할 수 있도록 합니다.

위의 예제에서는 Effect가 필요 없기 때문에 .none을 반환했습니다.

그러나 타이머 기능을 추가하여 일정 시간마다 count를 증가시키는 기능을 만들고 싶다면, Effect를 활용해야 합니다.

struct Feature: Reducer {
  struct State {
    var count = 0
  }

  enum Action {
    case decrementButtonTapped
    case incrementButtonTapped
    case startTimerButtonTapped
    case stopTimerButtonTapped
    case timerTick
  }

  enum CancelID { case timer }

  func reduce(into state: inout State, action: Action) -> Effect<Action> {
    switch action {
    case .decrementButtonTapped:
      state.count -= 1
      return .none

    case .incrementButtonTapped:
      state.count += 1
      return .none

    case .startTimerButtonTapped:
      return .run { send in
        while true {
          try await Task.sleep(for: .seconds(1))
          await send(.timerTick)
        }
      }
      .cancellable(CancelID.timer)

    case .stopTimerButtonTapped:
      return .cancel(CancelID.timer)

    case .timerTick:
      state.count += 1
      return .none
    }
  }
}
⚠️ 주의
위 예제에서는 Task.sleep을 사용하여 타이머를 구현했지만, 이 방법은 오차가 누적될 수 있습니다.
보다 정확한 타이머가 필요하다면, 클럭(clock) 의존성을 주입하여 구현하는 것이 좋습니다.

Reducer 정의 방법

Reducer를 정의하는 방법은 두 가지가 있습니다.

  1. reduce(into:action:) 메서드를 구현하는 방법
    • 직접적으로 State를 변경하고, Effect를 반환합니다.
  2. body 프로퍼티를 사용하여 여러 개의 Reducer를 조합하는 방법
    • Reduce를 사용하여 개별 리듀서를 결합할 수 있습니다.
var body: some Reducer<State, Action> {
  Reduce { state, action in
    // 추가 로직 구현
  }
  Activity()
  Profile()
  Settings()
}

또는, 추가 로직을 별도의 메서드로 분리하여 Reduce에서 호출할 수도 있습니다.

var body: some Reducer<State, Action> {
  Reduce(self.core)
  Activity()
  Profile()
  Settings()
}

func core(state: inout State, action: Action) -> Effect<Action> {
  // 추가 로직 구현
}
⚠️ 주의
reduce(into:action:)와 body를 동시에 구현할 수 있지만, 이 경우 reduce(into:action:)이 우선적으로 호출됩니다.
만약 여러 개의 리듀서를 조합하면서 추가 로직을 포함해야 한다면, 추가 로직을 body 내부에 배치하는 것이 좋습니다.

 


Reducer 변형(Custom Operator)

사용자 정의 연산자로 기존 리듀서를 변형할 때는 body 프로퍼티를 직접 호출하지 말고,

반드시 reduce(into:action:)을 호출해야 합니다.

예를 들어, 모든 액션을 로깅하는 커스텀 연산자를 만든다면:

extension Reducer {
  func logActions() -> some Reducer<State, Action> {
    Reduce { state, action in
      print("Received action: \\(action)")
      return self.reduce(into: &state, action: action)
    }
  }
}

참고: https://pointfreeco.github.io/swift-composable-architecture/1.2.0/documentation/composablearchitecture/reducer