본문 바로가기
Swift/TCA

[TCA] Effect.swift 공식문서 음미하기

by 마라민초닭발로제 2024. 8. 24.

 

1. TCA에서 호출하는 Reducer Action은 모두 Main Thread니까 Thread Safe? (아님)

TCA에서 호출하는 Reducer Action은 모두 MainThread에서 실행됩니다.  이유는 Send에 나와있습니다. 우리가 Reducer의 Action을 호출하는 경우는 두 가지 입니다. 

 - view에 저장된 Store의 send 매서드를 통해 을 통해 호출 (만약 MainThraead가 아닌 다른 곳에서 호출할 시 런타임 에러 발생)

 - reducer 내부에서 send를 통해 호출 

 

Store에 저장된 send와, reducer의 send의 경우 다음과 같은 코드를 볼 수 있습니다. @MainActor 때문에 send Struct을 Effect.run에서 호출 할 때 마다 MainThread에서 호출됩니다. 

@MainActor
public struct Send<Action>: Sendable {
  let send: @MainActor @Sendable (Action) -> Void

  public init(send: @escaping @MainActor @Sendable (Action) -> Void) {
    self.send = send
  }

  /// Sends an action back into the system from an effect.
  ///
  /// - Parameter action: An action.
  public func callAsFunction(_ action: Action) {
    guard !Task.isCancelled else { return }
    self.send(action)
  }

  /// Sends an action back into the system from an effect with animation.
  ///
  /// - Parameters:
  ///   - action: An action.
  ///   - animation: An animation.
  public func callAsFunction(_ action: Action, animation: Animation?) {
    callAsFunction(action, transaction: Transaction(animation: animation))
  }

  /// Sends an action back into the system from an effect with transaction.
  ///
  /// - Parameters:
  ///   - action: An action.
  ///   - transaction: A transaction.
  public func callAsFunction(_ action: Action, transaction: Transaction) {
    guard !Task.isCancelled else { return }
    withTransaction(transaction) {
      self(action)
    }
  }
}

 

 

2. Effect.merge를 통해서 병렬적으로 실행

 

실제 TCA코드는 merge Effect가 다음과 같이 기술되어 있습니다. 

핵심 로직은, merge를 통한 effect들은 reduce를 통해서 한개의 Combine Publisher로 합쳐집니다. 그리고 이를 Combine merge를 통해서 병렬적으로 처리하거나,  withTaskGroup(of: Void.self) { group in  ... } 을 통해서 병렬적으로 처리합니다. 둘을 나눈 이유는 Effect.run에서 TaskPriority를 받을 수 있는데, 이를 처리하기 위함입니다. 

@inlinable
  public static func merge(_ effects: Self...) -> Self {
    Self.merge(effects)
  }

  /// Merges a sequence of effects together into a single effect, which runs the effects at the same
  /// time.
  ///
  /// - Parameter effects: A sequence of effects.
  /// - Returns: A new effect
  @inlinable
  public static func merge(_ effects: some Sequence<Self>) -> Self {
    effects.reduce(.none) { $0.merge(with: $1) }
  }

  /// Merges this effect and another into a single effect that runs both at the same time.
  ///
  /// - Parameter other: Another effect.
  /// - Returns: An effect that runs this effect and the other at the same time.
  @inlinable
  public func merge(with other: Self) -> Self {
    switch (self.operation, other.operation) {
    case (_, .none):
      return self
    case (.none, _):
      return other
    case (.publisher, .publisher), (.run, .publisher), (.publisher, .run):
      return Self(
        operation: .publisher(
          Publishers.Merge(
            _EffectPublisher(self),
            _EffectPublisher(other)
          )
          .eraseToAnyPublisher()
        )
      )
    case let (.run(lhsPriority, lhsOperation), .run(rhsPriority, rhsOperation)):
      return Self(
        operation: .run { send in
          await withTaskGroup(of: Void.self) { group in
            group.addTask(priority: lhsPriority) {
              await lhsOperation(send)
            }
            group.addTask(priority: rhsPriority) {
              await rhsOperation(send)
            }
          }
        }
      )
    }
  }

 

 

 

3.  Effect.concatenate를 통해서 직렬 코드 실행

또한 concatenate도 Combine Publihser를 통해서 해결하거나, Task Prioritry로 직렬로 코드가 수행됩니다. 

/// Concatenates a variadic list of effects together into a single effect, which runs the effects
  /// one after the other.
  ///
  /// - Parameter effects: A variadic list of effects.
  /// - Returns: A new effect
  @inlinable
  public static func concatenate(_ effects: Self...) -> Self {
    Self.concatenate(effects)
  }

  /// Concatenates a collection of effects together into a single effect, which runs the effects one
  /// after the other.
  ///
  /// - Parameter effects: A collection of effects.
  /// - Returns: A new effect
  @inlinable
  public static func concatenate(_ effects: some Collection<Self>) -> Self {
    effects.reduce(.none) { $0.concatenate(with: $1) }
  }

  /// Concatenates this effect and another into a single effect that first runs this effect, and
  /// after it completes or is cancelled, runs the other.
  ///
  /// - Parameter other: Another effect.
  /// - Returns: An effect that runs this effect, and after it completes or is cancelled, runs the
  ///   other.
  @inlinable
  @_disfavoredOverload
  public func concatenate(with other: Self) -> Self {
    switch (self.operation, other.operation) {
    case (_, .none):
      return self
    case (.none, _):
      return other
    case (.publisher, .publisher), (.run, .publisher), (.publisher, .run):
      return Self(
        operation: .publisher(
          Publishers.Concatenate(
            prefix: _EffectPublisher(self),
            suffix: _EffectPublisher(other)
          )
          .eraseToAnyPublisher()
        )
      )
    case let (.run(lhsPriority, lhsOperation), .run(rhsPriority, rhsOperation)):
      return Self(
        operation: .run { send in
          if let lhsPriority {
            await Task(priority: lhsPriority) { await lhsOperation(send) }.cancellableValue
          } else {
            await lhsOperation(send)
          }
          if let rhsPriority {
            await Task(priority: rhsPriority) { await rhsOperation(send) }.cancellableValue
          } else {
            await rhsOperation(send)
          }
        }
      )
    }
  }