Jinsujin / translate-documents

iOS μ—°κ΄€ λ¬Έμ„œ λ²ˆμ—­
0 stars 0 forks source link

The Composable Architecture(TCA) #8

Open Jinsujin opened 1 year ago

Jinsujin commented 1 year ago

What is the Composable Architecture?

이 λΌμ΄λΈŒλŸ¬λ¦¬λŠ” λ‹€μ–‘ν•œ λͺ©μ κ³Ό λ³΅μž‘μ„±μ„ 가진 μ–΄ν”Œλ¦¬μΌ€μ΄μ…˜μ„ κ΅¬μΆ•ν• λ•Œ μ‚¬μš©ν•  수 μžˆλŠ” λͺ‡κ°€μ§€ core tool 을 μ œκ³΅ν•œλ‹€. μ–΄ν”Œλ¦¬μΌ€μ΄μ…˜μ„ κ΅¬μΆ•ν• λ•Œ μΌμƒμ μœΌλ‘œ λ°œμƒν•˜λŠ” λ§Žμ€ 문제λ₯Ό ν•΄κ²°ν•˜κΈ° μœ„ν•΄ λ‹€μŒκ³Ό 같은 섀득λ ₯μžˆλŠ” 사둀λ₯Ό μ œκ³΅ν•œλ‹€:

Basic Usage

Composable Architecture λ₯Ό μ‚¬μš©ν•΄ κΈ°λŠ₯을 λΉŒλ“œν•˜λ €λ©΄, λ‹Ήμ‹ μ˜ domain 을 λͺ¨λΈλ§ ν•˜λŠ” λͺ‡κ°€μ§€ νƒ€μž…κ³Ό 값을 μ •μ˜ν•˜λΌ:

Jinsujin commented 1 year ago

Simple Example

+, - λ²„νŠΌμœΌλ‘œ 숫자λ₯Ό 증가&κ°μ†Œ μ‹œν‚€λŠ” μ•±

1. κΈ°λŠ₯ μ •μ˜

2. κ΅¬ν˜„

이 κΈ°λŠ₯을 κ΅¬ν˜„ν•˜λ €λ©΄ ReducerProtocol λ₯Ό μ€€μˆ˜ν•˜μ—¬ κΈ°λŠ₯의 도메인과 λ™μž‘μ„ μ •μ˜ν•  μƒˆλ‘œμš΄ type 을 생성해야 ν•œλ‹€:

import ComposableArchitecture

struct Feature: ReducerProtocol {
}

μ—¬κΈ°μ„œ ν˜„μž¬ μΉ΄μš΄νŠΈμ— λŒ€ν•œ integer 와, 화면에 ν‘œμ‹œν•  alert 의 title 을 λ‚˜νƒ€λ‚΄λŠ” optional string 둜 κ΅¬μ„±λœ κΈ°λŠ₯의 state type 을 μ •μ˜ν•΄μ•Ό ν•œλ‹€. (nil 은 alert 을 ν‘œμ‹œν•˜μ§€ μ•ŠμŒμ„ λ‚˜νƒ€λ‚΄κΈ° λ•Œλ¬Έ)

struct Feature: ReducerProtocol {
  struct State: Equatable {
    var count = 0
    var numberFactAlert: String?
  }
}

λ˜ν•œ κΈ°λŠ₯의 action 에 λŒ€ν•œ type 을 μ •μ˜ν•΄μ•Ό ν•œλ‹€. μ—¬κΈ°μ—λŠ” κ°μ†Œ λ²„νŠΌ, 증가 λ²„νŠΌ, fact λ²„νŠΌμ„ νƒ­ν–ˆμ„λ•Œμ™€ 같은 λͺ…λ°±ν•œ action 이 μžˆλ‹€.

κ·ΈλŸ¬λ‚˜ μ—¬κΈ°μ—λŠ” μ•½κ°„ λͺ…ν™•ν•˜μ§€ μ•Šμ€ μ•‘μ…˜λ“€λ„ μžˆλ‹€. alert 을 dismiss ν•˜κ±°λ‚˜ fact API 둜 λΆ€ν„° 응닡을 λ°›μ„λ•Œ λ°œμƒν•˜λŠ” μž‘μ—…μ²˜λŸΌ:

struct Feature: ReducerProtocol {
  struct State: Equatable { … }
  enum Action: Equatable {
    case factAlertDismissed
    case decrementButtonTapped
    case incrementButtonTapped
    case numberFactButtonTapped
    **case numberFactResponse(TaskResult<String>)**
  }
}

κ·ΈλŸ°λ‹€μŒ reduce method λ₯Ό κ΅¬ν˜„ν•œλ‹€. μ΄λŠ” κΈ°λŠ₯(feature)에 λŒ€ν•œ μ‹€μ œ logic 및 λ™μž‘μ„ μ²˜λ¦¬ν•˜λŠ” 역할을 ν•œλ‹€.

ν˜„μž¬ state λ₯Ό next state 둜 λ°”κΎΈλŠ” 방법과 μ–΄λ–€ effect λ₯Ό μ‹€ν–‰ν•΄μ•Ό ν•˜λŠ”μ§€ μ„€λͺ…ν•œλ‹€. 일뢀 action은 effectλ₯Ό μ‹€ν–‰ν•  ν•„μš”κ°€ μ—†μœΌλ©°, 이λ₯Ό ν‘œν˜„ν•˜κΈ° μœ„ν•΄ .none 을 λ°˜ν™˜ν•œλ‹€:

struct Feature: ReducerProtocol {
  struct State: Equatable { … }
  enum Action: Equatable { … }

  func reduce(into state: inout State, action: Action) -> EffectTask<Action> {
    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/\(count)/trivia")!).0,
                as: 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
    }
  }
}

그리고 λ‚˜μ„œ λ§ˆμ§€λ§‰μœΌλ‘œ feature λ₯Ό 화면에 ν‘œμ‹œν•˜λŠ” view λ₯Ό μ •μ˜ν•œλ‹€.

state에 λŒ€ν•œ λͺ¨λ“  변경을 κ΄€μ°°(observe) ν•˜κ³  λ‹€μ‹œ λ Œλ”λ§ν•  수 μžˆλ„λ‘ StoreOf<Feature> 을 μœ μ§€ν•œλ‹€. 그리고 μš°λ¦¬λŠ” state κ°€ λ³€κ²½λ˜λ„λ‘ λͺ¨λ“  μ‚¬μš©μž action 을 store 둜 λ³΄λ‚Όμˆ˜ μžˆλ‹€.

.alertΒ view modifier 에 ν•„μš”ν•œIdentifiable 둜 λ§Œλ“€κΈ° μœ„ν•΄ fact alert μ£Όμœ„μ— struct wrapper λ₯Ό λ„μž…ν•΄μ•Ό ν•œλ‹€:

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 }
}

λ˜ν•œ UIKit controller λ₯Ό ꡬ동(driven)ν•˜λŠ” 것도 κ°„λ‹¨ν•˜λ‹€.

UI λ₯Ό μ—…λ°μ΄νŠΈν•˜κ³  alert 을 ν‘œμ‹œν•˜κΈ° μœ„ν•΄ viewDidLoad μ—μ„œ store λ₯Ό ꡬ독(subscribe) ν•œλ‹€. ν•΄λ‹Ή μ½”λ“œλŠ”SwiftUI 버전보닀 더 κΈΈλ‹€:

class FeatureViewController: UIViewController {
  let viewStore: ViewStoreOf<Feature>
  var cancellables: Set<AnyCancellable> = []

  init(store: StoreOf<Feature>) {
    self.viewStore = ViewStore(store)
    super.init(nibName: nil, bundle: nil)
  }

  required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  override func viewDidLoad() {
    super.viewDidLoad()

    let countLabel = UILabel()
    let incrementButton = UIButton()
    let decrementButton = UIButton()
    let factButton = UIButton()

    // Omitted: Add subviews and set up constraints...

    self.viewStore.publisher
      .map { "\($0.count)" }
      .assign(to: \.text, on: countLabel)
      .store(in: &self.cancellables)

    self.viewStore.publisher.numberFactAlert
      .sink { [weak self] numberFactAlert in
        let alertController = UIAlertController(
          title: numberFactAlert, message: nil, preferredStyle: .alert
        )
        alertController.addAction(
          UIAlertAction(
            title: "Ok",
            style: .default,
            handler: { _ in self?.viewStore.send(.factAlertDismissed) }
          )
        )
        self?.present(alertController, animated: true, completion: nil)
      }
      .store(in: &self.cancellables)
  }

  @objc private func incrementButtonTapped() {
    self.viewStore.send(.incrementButtonTapped)
  }
  @objc private func decrementButtonTapped() {
    self.viewStore.send(.decrementButtonTapped)
  }
  @objc private func factButtonTapped() {
    self.viewStore.send(.numberFactButtonTapped)
  }
}

μ•±μ˜ μ§„μž…μ (entry point)μ—μ„œ 이 viewλ₯Ό 화면에 ν‘œμ‹œν•  μ€€λΉ„κ°€ 되면, μš°λ¦¬λŠ” storeλ₯Ό ꡬ성(construct) ν•  수 μžˆλ‹€. μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ˜ initial state 와, μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ— power λ₯Ό 곡급할 reducer λ₯Ό 지정해 μˆ˜ν–‰ν•  수 μžˆλ‹€:

import ComposableArchitecture

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

이걸둜 ν™”λ©΄μ—μ„œ μ‹€ν–‰ν•˜κΈ° μΆ©λΆ„ν•˜λ‹€.

순수 SwiftUI λ°©μ‹μœΌλ‘œ 이 μž‘μ—…μ„ μˆ˜ν–‰ν•˜λŠ” κ²½μš°λ³΄λ‹€ λͺ‡λ‹¨κ³„κ°€ 더 ν•„μš”ν•˜μ§€λ§Œ, λͺ‡κ°€μ§€ 이점이 μžˆλ‹€.