TCA

TCA - ReducerProtocol

GREEN.1229 2023. 1. 31. 09:09

안녕하세요. 그린입니다🍏

이번 포스팅에서는 TCA의 ReducerProtocol을 간단히 학습해보려합니다🙌

 

TCA가 날이 갈수록 업데이트도 빨라지고 더 발전하고 있어요!

현재 버전이 0.50.1까지 나왔으며 그전에 대격변이 0.41.0에서 일어났습니다.

바로 ReducerProtocol의 등장이죠🕺🏻

 

기존에 TCA에서 Core를 구성할때는 State, Action, Environment들을 별도 struct, enum으로 정의하고 이를 Reducer타입의 상수로 만들어 정의하는 형태였어요.

그런데 이와 반대로 Protocol Programming의 장점도 살리면서 더 적합하게 ReducerProtocol의 사용으로 전부 변화되었으며 장점도 많아졌어요🙏🏻

 

만약 이전 TCA 구성이 대략적으로 어떻게 되어있었는지 궁금하신분들은 아래 사용 예시 포스팅을 먼저 보고 비교해보시는것도 좋습니다!

https://green1229.tistory.com/156

 

Composable Architecture로 랜덤 통신 구현하기

안녕하세요. 그린입니다🟢 이번 포스팅에서는 Composable Architecture으로 랜덤한 통신을 구현해보겠습니다🧑🏻‍💻 뷰는 SwiftUI를 통해 간단히 구현하였습니다. 우선 간략한 기능을 설명드리겠습

green1229.tistory.com

 

그럼 본격적으로 ReducerProtocol에 대해 알아보겠습니다🥊

 

ReducerProtocol?

우선 Pointfree 공식문서에 따르면 앱에서 액션이 주어질때 현재 상태에서 다음 상태로의 전환되는 방법을 설명하고 후에 Store에서 실행해야하는 Effect를 설명하는 프로토콜입니다.

protocol ReducerProtocol<State, Action>

즉 선언은 당연히 State, Action을 갖는 프로토콜입니다.

그럼 이제는 이 프로토콜을 채택하는 구조체를 하나 만들면서 이 안에 늘 먹던것처럼 State, Action, Env 그리고 reducer 메서드까지 구현해주는것이죠.

공식문서 기반으로 예시를 한번 보겠습니다!

struct Feature: ReducerProtocol {
  struct State: Equatable {
    var count = 0
    var numberFactAlert: String?
  }
  
  enum Action: Equatable {
    case factAlertDismissed
    case decrementButtonTapped
    case incrementButtonTapped
    case numberFactButtonTapped
    case numberFactResponse(TaskResult<String>)
  }

 func reduce(into state: inout State, action: Action) -> Effect<Action, Never> {
    switch action {
      case .factAlertDismissed:
        state.numberFactAlert = nil
        return .none

      case .decrementButtonTapped:
        state.count -= 1
        return .none

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

      case .numberFactButtonTapped:
        return .task { [count = state.count] in 
          await .numberFactResponse(
            TaskResult { 
              String(
                decoding: try await URLSession.shared
                  .data(from: URL(string: "http://numbersapi.com/\(number)/trivia")!).0,
                using: UTF8.self
              )
            }
          )
        }

      case let .numberFactResponse(.success(fact)):
        state.numberFactAlert = fact
        return .none

      case .numberFactResponse(.failure):
        state.numberFactAlert = "Could not load a number fact :("
        return .none
      } 
    }
  }
}

이렇게 프로토콜을 채택하고 State, Action을 정의 해주고 있죠.

그 다음 reducer 상수를 구현하듯 reduce 메서드를 구현해줘요.

State는 변화를 입혀야되니 inout 타입이고 반환 타입은 당연히 Effect 타입입니다.

실제 이제 사용에 있어 .run, .task라던지 SwiftConcurrency에 맞춰 async await을 적용하면서 변경된 부분들은 양이 꽤 되기에 이후 포스팅에서 다뤄보겠습니다🥲

 

 자 그럼 이제 View단 body에서는 어떻게 다뤄줘야할지 보겠습니다.

struct FeatureView: View {
  let store: StoreOf<Feature>

  var body: some View {
    WithViewStore(self.store, observe: { $0 }) { viewStore in
      VStack {
        HStack {
          Button("−") { viewStore.send(.decrementButtonTapped) }
          Text("\(viewStore.count)")
          Button("+") { viewStore.send(.incrementButtonTapped) }
        }

        Button("Number fact") { viewStore.send(.numberFactButtonTapped) }
      }
      .alert(
        item: viewStore.binding(
          get: { $0.numberFactAlert.map(FactAlert.init(title:)) },
          send: .factAlertDismissed
        ),
        content: { Alert(title: Text($0.title)) }
      )
    }
  }
}

struct FactAlert: Identifiable {
  var title: String
  var id: String { self.title }
}

실제 View에 올릴때는 이제 store 상수에 StoreOf의 타입으로 생성하는데 해당 Core에서 정의하고 만든 Feature를 제네릭하게 받게 해줍니다.

그런다음 WithViewStore로 body 내부에서 감싸서 사용하는것은 기존과 거의 동일합니다!

 

마지막으로 이 View를 올려줄 main App에서는 이렇게 사용하게 되죠.

@main
struct MyApp: App {
  var body: some Scene {
    FeatureView(
      store: Store(
        initialState: Feature.State(),
        reducer: Feature()
      )
    )
  }
}

보면 state에서는 Feature의 State 인스턴스를 사용하고 reducer는 Feature 인스턴스를 생성해준것을 확인할 수 있죠.

 

그리고 안짚고 넘어간것이 있는데 기존 서버 통신 및 외부 사이드 이펙트를 처리할때는 Environment 구조체를 정의하고 이를 reducer 상수에서 제네릭하게 받아 사용했는데요.

이제는 다릅니다.

struct NumberFactClient {
  var fetch: (Int) async throws -> String
}

예를들어 이렇게 fetch 기능을 갖는 NumberFactClient 구조체가 있고 의존성을 주입하여 사용해야 된다고 가정해볼께요.

extension NumberFactClient: DependencyKey {
  static let liveValue = Self(
    fetch: { number in
      let (data, _) = try await URLSession.shared
        .data(from: .init(string: "http://numbersapi.com/\(number)")!)
      return String(decoding: data, as: UTF8.self)
    }
  )
}

extension DependencyValues {
  var numberFact: NumberFactClient {
    get { self[NumberFactClient.self] }
    set { self[NumberFactClient.self] = newValue }
  }
}

이제는 DependencyKey 프로토콜을 채택해 해당 유형을 의존성 관리 시스템에 등록하게 됩니다.

그런다음 DependencyValues를 확장해 numberFact라는 get set 프로퍼티를 구현해주죠.

앞으론 이걸 사용하게 될거에요.

struct Feature: ReducerProtocol {
  struct State { … }
  enum Action { … }
  @Dependency(\.numberFact) var numberFact
  …
}

그럼 이렇게 필요한 Core Feature에서 @Dependency 프로퍼티 래퍼 타입으로 numberFact를 가져옵니다.

@main
struct MyApp: App {
  var body: some Scene {
    FeatureView(
      store: Store(
        initialState: Feature.State(),
        reducer: Feature()
      )
    )
  }
}

그럼 이렇게 실제로 App단에서부터 의존성을 주입받아 부여하지 않아도 되는것이죠.

let store = TestStore(
  initialState: Feature.State(),
  reducer: Feature()
) {
  $0.numberFact.fetch = { "\($0) is a good number Brent" }
}

만약 테스트 환경을 구축한다면 TestStore를 만들때 live하지 않은 fetch 구현을 정의해주면 됩니다.

즉 ReducerProtocol을 사용하면서 장점으로 느껴졌던 큰 부분이 의존성이 편리해졌다는것 같아요⭐️

이전에는 App 상위부터 쭈욱 받아오며 의존성 주입을 해주니 까먹을때도 있고 추가되면 많은 뎁스의 코드 수정이 이뤄졌어야하는데 이제는 그런 작업이 불필요하죠👍

 

마무리

이렇게 아주아주아주 간단하게 ReducerProtocol의 리드미 정도로만 어떤게 나온건지 알아보긴만 했어요.

아직 깊이있고 실전 응용으로 하기에는 학습이 미생이라!

이제 최신버전을 기준으로 조금씩 블로깅 해나가겠습니다🙌

 

[참고 자료]

https://pointfreeco.github.io/swift-composable-architecture/0.41.0/documentation/composablearchitecture/reducerprotocol/

 

Documentation

 

pointfreeco.github.io

https://www.pointfree.co/blog/posts/81-announcing-the-reducer-protocol

 

Announcing the Reducer Protocol

Today we are releasing the biggest update to the Composable Architecture ever, completely reimagining how features are built with the library.

www.pointfree.co

https://github.com/pointfreeco/swift-composable-architecture/tree/0.50.1

 

GitHub - pointfreeco/swift-composable-architecture: A library for building applications in a consistent and understandable way,

A library for building applications in a consistent and understandable way, with composition, testing, and ergonomics in mind. - GitHub - pointfreeco/swift-composable-architecture: A library for bu...

github.com