์ถœ์ฒ˜ ๋ฐ ์ฐธ๊ณ  ์‚ฌ์ดํŠธ 

https://axiomatic-fuschia-666.notion.site/Chapter-3-TCA-2-c56b24efb2154dad9ed8e54139247024

 

Chapter 3. TCA์˜ ๊ธฐ๋ณธ๊ฐœ๋…(2)

์•ž์„  ์žฅ์—์„œ ์šฐ๋ฆฌ๋Š” ์•ฑ์˜ ์ƒํƒœ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” State์™€ ์ด๋ฅผ ๋ณ€๊ฒฝํ•  ์ˆ˜๋‹จ์ธ Action, ๊ทธ Action์˜ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๊ณ  ์ƒํƒœ์˜ ๋ณ€๊ฒฝ์„ ์ฒ˜๋ฆฌํ•˜๋Š” Reducer์„ ์•Œ์•„๋ณด๋ฉฐ, TCA์—์„œ์˜ ๋ฐ์ดํ„ฐํ๋ฆ„์— ๋Œ€ํ•ด์„œ ์‚ดํŽด๋ณด์•˜์Šต๋‹ˆ

axiomatic-fuschia-666.notion.site

 

Effect๋ž€?

Action์ด ๋ฐ˜ํ™˜ํ•˜๋Š” ํƒ€์ž…์„ ๋œปํ•˜๋ฉฐ Action์„ ๊ฑฐ์นœ ๋ชจ๋“  ๊ฒฐ๊ณผ๋ฌผ์„ ์˜๋ฏธํ•œ๋‹ค. ๋น„๋™๊ธฐ ์ž‘์—…์ด๋‚˜ ์™ธ๋ถ€ ์ž‘์šฉ์— ์˜ํ•ด ๋ฐœ์ƒํ•˜๋Š” Side Effect๋Š” ์–ด๋–ค ์ฒ˜๋ฆฌ ์ดํ›„ ์˜ˆ์ƒ์น˜ ๋ชปํ•˜๊ฒŒ ์–ป์€ ๊ฒฐ๊ณผ๋ฌผ์„ ์˜๋ฏธํ•œ๋‹ค. Store๋Š” Side Effect๋ฅผ ์•ฑ์˜ ๋กœ์ง์— ํ†ตํ•ฉํ•˜๋Š” ์—ญํ™œ์„ ํ•œ๋‹ค. 

public struct Reduce<State, Action>: Reducer {
  @usableFromInline
  let reduce: (inout State, Action) -> Effect<Action>
  // ...
}

public typealias ReducerOf<R: Reducer> = Reducer<R.State, R.Action>
  • Action์€ State๋ฅผ ์ง์ ‘ ๋ณ€๊ฒฝํ•˜์ง€๋งŒ, Effect๋Š” ๋น„๋™๊ธฐ์ ์ธ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๊ณ  ๊ฒฐ๊ณผ๋ฅผ Action์œผ๋กœ ๋ฐ˜ํ™˜ ํ•œ๋’ค State์— ๋ฐ˜์˜ํ•จ
  • ํŠน์ • Action ์‹คํ–‰ > ๊ฒฐ๊ณผ์— ๋”ฐ๋ผ ์ƒˆ๋กœ์šด Action ์ƒ์„ฑ(์ง์ ‘ ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹˜) > State ์—…๋ฐ์ดํŠธ
  • ex ) ๋„คํŠธ์›Œํฌ ํ˜ธ์ถœ, ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ, ์™ธ๋ถ€ ์„œ๋น„์Šค์™€ ๊ต๋ฅ˜ ๋“ฑ์˜ ๋‹ค์–‘ํ•œ ๋น„๋™๊ธฐ ์ž‘์—…

Effect๊ฐ€ ํ•˜๋Š” ์ผ

  • ๋น„๋™๊ธฐ ์ž‘์—…
    • ๋„คํŠธ์›Œํฌ ์š”์ฒญ, ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ, ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ ๋“ฑ
  • Side Effect ๋ถ„๋ฆฌ
    • State ๋ณ€ํ™”๋ฅผ ์ผ์œผํ‚ค๋Š” ๋ถ€๋ถ„๊ณผ Side Effect ๋‹ค๋ฃจ๋Š” ๋ถ€๋ถ„์„ ๋ช…ํ™•ํ•˜๊ฒŒ ๋ถ„๋ฆฌํ•  ์ˆ˜ ์žˆ์Œ
    • ์ฝ”๋“œ ๊ฐ€๋…์„ฑ ํ–ฅ์ƒ
    • ํ…Œ์ŠคํŠธ์™€ ๋””๋ฒ„๊น… ์šฉ์ด
  • ์ทจ์†Œ ๋ฐ ์—๋Ÿฌ ํ•ธ๋“ค๋ง
    • ๋น„๋™๊ธฐ ์ž‘์—… ์„ฑ๊ณต, ์‹คํŒจ, ์ค‘๋‹จ ๊ด€๋ฆฌ
    • ex) ๋„คํŠธ์›Œํฌ ์š”์ฒญ ์ค‘ ๋ฐœ์ƒํ•œ ์˜ค๋ฅ˜ ๋Œ€์‘
  • ์ˆœ์„œ ๋ณด์žฅ
    • TCA์—์„œ์˜ Effect๋Š” ์ˆœ์ฐจ์ ์œผ๋กœ ์‹คํ–‰
    • ์ˆœ์„œ๋ฅผ ๋ณด์žฅํ•จ
    • ์˜ˆ์ธก ๊ฐ€๋Šฅํ•œ ๊ฒฐ๊ณผ๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ์Œ

Side Effect 

  • ์•ฑ์˜ ์ฃผ์š” ๋กœ์ง๊ณผ ๋ณ„๊ฐœ๋กœ ๋ฐœ์ƒํ•˜๋Š” ์ž‘์—…
  • ์™ธ๋ถ€ ์„œ๋น„์Šค์™€์˜ ์ƒํ˜ธ์ž‘์šฉ ๋˜๋Š” ๋น„๋™๊ธฐ ์ž‘์—… ์ฒ˜๋ฆฌ
  • ์‚ฌ์ด๋“œ ์ดํŽ™ํŠธ๋Š” ์ฝ”๋“œ ๋ณต์žก์„ฑ์ด ์ฆ๊ฐ€์‹œํ‚ค๊ณ  ํ…Œ์ŠคํŠธ ํ•˜๊ธฐ ์–ด๋ ต๊ฒŒ ํ•จ
  • ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ์ ์ ˆํ•œ Action์„ ์ƒ์„ฑํ•˜์—ฌ State๋ฅผ ์—…๋ฐ์ดํŠธ ํ•˜์—ฌ ์‚ฌ์šฉ์ž์—๊ฒŒ ํ”ผ๋“œ๋ฐฑ ์ œ๊ณต
            case .getProductDetail:
                return .run { [id = state.productId] send in
                    let (data, _) = try await URLSession.shared.data(from: URL(string: "https://api/v1/products/\(id)")!)
                    do {
                        let product = try JSONDecoder().decode(Product.self, from: data)
                        print(product)
                        await send(.productResponse(product))
                    } catch {
                        await send(.alertShow)
                    }
                }

ใ„ด ์—๋Ÿฌ ๋ฐœ์ƒ์‹œ ์–ผ๋Ÿฟ์„ ๋ณด์—ฌ์ฃผ๋Š” Action์„ ์ƒ์„ฑํ•˜์—ฌ State ์—…๋ฐ์ดํŠธ

 

      case .onTask:
        return .run { send in
          let status = await withUnsafeContinuation { continuation in
            SFSpeechRecognizer.requestAuthorization { status in
              continuation.resume(with: .success(status))
            }
          }
          for await _ in self.clock.timer(interval: .seconds(1)) {
            await send(.timerTicked)
          }
        }
  • View๊ฐ€ ํ™”๋ฉด์— ๋‚˜ํƒ€๋‚˜๋ฉด ๊ถŒํ•œ์„ ์š”์ฒญํ•˜๋Š” ์ฝ”๋“œ
  • Apple API์— ์ ‘๊ทผ -> ์‚ฌ์šฉ์ž์˜ ๊ฒฐ์ • -> ์‹œ์Šคํ…œ์— ํ”ผ๋“œ๋ฐฑ -> ์‚ฌ์ด๋“œ ์ดํŽ™ํŠธ : View๊ฐ€ ์‹œ์ž‘ํ•˜์ž ๋งˆ์ž Side Effect๊ฐ€ ๋ฐœ์ƒ
  • .run ๋ฉ”์„œ๋“œ๋Š” ํ•ด๋‹น ํด๋กœ์ €์— Action์„ ์‹œ์Šคํ…œ์œผ๋กœ ๋‹ค์‹œ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๋Š” .send๋กœ ๋น„๋™๊ธฐ ์ž‘์—…์„ ์ˆ˜ํ–‰
.task { await viewStore.send(.onTask).finish() }
  • View๊ฐ€ ํ™”๋ฉด์— ๋‚˜ํƒ€๋‚˜๋ฉด ๋น„๋™๊ธฐ ์ž‘์—… ์‹คํ–‰, View๊ฐ€ ์‚ฌ๋ผ์ง€๋ฉด ๋น„๋™๊ธฐ ์ž‘์—… ์ž๋™ ์ทจ์†Œ
  • .task()์™€ .send(_:Action) ๋ฉ”์„œ๋“œ๋กœ Effect์˜ ์ž‘์—…์„ View์˜ Lifecycle๊ณผ ์—ฐ๊ฒฐ
  • Action ์ „๋‹ฌ ํ›„ ๋ชจ๋“  Effect๊ฐ€ ์™„๋ฃŒ๋˜๋Š” .finish๋ฅผ ๊ธฐ๋‹ค๋ฆผ
func withUnsafeContinuation<T>(_ fn: (UnsafeContinuation<T, Never>) -> Void) async -> T
  • ์ „๋‹ฌ๋œ ํด๋กœ์ ธ๋Š” ๋™๊ธฐ์ ์œผ๋กœ ์‹คํ–‰๋˜๋ฉฐ ํด๋กœ์ €๊ฐ€ ๋ฐ˜ํ™˜๋˜๋ฉด ์ž‘์—…์ด ์ผ์‹œ ์ค‘๋‹จ๋จ
  • ์ž‘์—…์„ ์ฆ‰์‹œ ์žฌ๊ฐœํ•˜๊ฑฐ๋‚˜ ๋‚˜์ค‘์— ์™„๋ฃŒ๋ฅผ ์œ„ํ•ด continuation์—์„œ ๋น ์ ธ๋‚˜์˜ฌ ์ˆ˜ ์žˆ์Œ
  • resume ๋ฉ”์„œ๋“œ๋Š” ํ•œ ๋ฒˆ๋งŒ ํ˜ธ์ถœํ•ด์•ผ ํ•˜๋ฉฐ ์ด ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜์ง€ ์•Š์œผ๋ฉด ์ž‘์—…์ด ๋ฌด๊ธฐํ•œ ์ผ์‹œ ์ค‘๋‹จ๋œ ์ƒํƒœ๋กœ ์œ ์ง€๋จ
  • reusme์„ ๋‘๋ฒˆ ํ˜ธ์ถœํ•˜๊ฑฐ๋‚˜ ํ˜ธ์ถœํ•˜๋Š” ๊ฒƒ์„ ์žŠ์–ด ๋ฒ„๋ฆฌ์ง€ ๋ง์•„์•ผ ํ•จ

 

continuation: UnsafeContinuation<SFSpeechRecognizerAuthorizationStatus, Never>
  • requestAuthorization์˜ ์ฝœ๋ฐฑ์„ confinuation์„ ์ด์šฉํ•ด ๋น„๋™๊ธฐ ํ•จ์ˆ˜๋กœ ์—ฐ๊ฒฐ
  • ์‚ฌ์šฉ์ž๊ฐ€ ๊ถŒํ•œ์„ ์„ค์ •ํ•˜๋ฉด ์ผ์‹œ ์ค‘๋‹จ ์ข…๋ฃŒ

 

Effect์˜ ์ข…๋ฅ˜

public struct Effect<Action> {
  @usableFromInline
  enum Operation {
    case none
    case publisher(AnyPublisher<Action, Never>)
    case run(TaskPriority? = nil, @Sendable (_ send: Send<Action>) async -> Void)
  }
  // ...
}

Effect์—๋Š” none, publisher, run ํƒ€์ž…์ด ์กด์žฌ

  @inlinable
  public init(_ reduce: @escaping (_ state: inout State, _ action: Action) -> Effect<Action>) {
    self.init(internal: reduce)
  }

Reducer์„ ๋”ฐ๋ฅด๋Š” Reduce์˜ reduce๋กœ ์ „๋‹ฌ๋œ ํด๋กœ์ €๋Š” Effect๋ฅผ ๋ฐ˜ํ™˜

.none

Effect๋ฅผ ๋ฐ˜ํ™˜ํ•ด์•ผ ํ•˜๋‚˜ ์•„๋ฌด๋Ÿฐ effect๋„ ๋ฐ˜ํ™˜ํ•˜๊ณ  ์‹ถ์ง€ ์•Š์„ ๋•Œ ์‚ฌ์šฉ๋จ

case .likeButtonTapped:
    state.isLike.toggle()
    return .none

 

.send

  public static func send(_ action: Action) -> Self {
    Self(operation: .publisher(Just(action).eraseToAnyPublisher()))
  }
  • Effect์˜ ์ต์Šคํ…์…˜์— ๊ตฌํ˜„๋œ send๋Š” Action์„ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋ฐ›๋Š” ๋ฉ”์„œ๋“œ
  • ์•ก์…˜ ์ดํ›„ ์ถ”๊ฐ€์ ์ธ '๋™๊ธฐ' ์•ก์…˜์ด ํ•„์š”ํ•  ๋•Œ ์‚ฌ์šฉ๋จ
  • ์•ก์…˜ ์ „๋‹ฌ ๋ฐ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ๋™์‹œ์— ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅ
  • ๋‹จ๋ฐฉํ–ฅ ๋ฐ์ดํ„ฐ ํ๋ฆ„์„ ํ—ค์น  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ๋กœ์ง ๊ณต์œ  ๋ชฉ์ ์œผ๋กœ ์‚ฌ์šฉ๋˜์„œ๋Š” ์•ˆ๋จ
  • ex ์ž์‹ ์ปดํฌ๋„ŒํŠธ์—์„œ ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ๋กœ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ

.run

  • ๋น„๋™๊ธฐ ์ž‘์—… ์ˆ˜ํ–‰
static func run(
    priority: TaskPriority? = nil,
    operation: @escaping (Send<ProductDetailFeature.Action>) async throws -> Void,
    catch handler: ((Error, Send<ProductDetailFeature.Action>) async -> Void)? = nil,
    fileID: StaticString = #fileID,
    line: UInt = #line
) -> Effect<ProductDetailFeature.Action>
  • operation ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋น„๋™๊ธฐ ํด๋กœ์ €๋ฅผ ์ „๋‹ฌ๋ฐ›์•„ ์‹คํ–‰ํ•จ
                return .run { [id = state.productId] send in
                    let (data, _) = try await URLSession.shared.data(from: URL(string: "")!)
                    do {
                        let product = try JSONDecoder().decode(Product.self, from: data)
                        print(product)
                        await send(.productResponse(product))
                    } catch {
                        // TODO: error handling
                    }
                }
  • ํด๋กœ์ € ๋‚ด๋ถ€์—์„œ send๋กœ ์•ก์…˜์„ ์ „๋‹ฌ ํ•  ์ˆ˜ ์žˆ์Œ

.cancellable, .cancel

  • effect๋ฅผ ์ทจ์†Œํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋งŒ๋“œ๋Š” ๋ฉ”์„œ๋“œ
func cancellable<ID>(
    id: ID,
    cancelInFlight: Bool = false
) -> Effect<ProductDetailFeature.Action> where ID : Hashable
  • effect๋ฅผ ์‹๋ณ„ํ•˜๊ธฐ ์œ„ํ•œ ๊ฐ’
  • cancelInFlight ๊ฐ’์„ false๋กœ ์„ค์ •ํ•˜๋ฉด ๊ฐ™์€ id๋ฅผ ์ง€์ •ํ•˜์—ฌ effect๋ฅผ ์ทจ์†Œํ•  ์ˆ˜ ์žˆ์Œ. ๊ธฐ๋ณธ ๊ฐ’์€ false
public func cancellable<ID: Hashable>(id: ID, cancelInFlight: Bool = false) -> Self {
  • ID๋กœ๋Š” hash ๊ฐ€๋Šฅํ•œ ์–ด๋–ค ๊ฐ’์œผ๋กœ๋“  ์ง€์ • ๊ฐ€๋Šฅ
    private enum CancelID {
        case timer
    }
  • enum์œผ๋กœ CancelID ์ง€์ •
            case .timerToggled:
                state.isTimerOn.toggle()
                if state.isTimerOn {
                    return .run { send in
                        for await _ in self.clock.timer(interval: .seconds(1)) {
                            await send(.timerTicked)
                        }
                    }
                    .cancellable(id: CancelID.timer)
                } else {
                    return .cancel(id: CancelID.timer)
                }
  • ํƒ€์ด๋จธ๊ฐ€ ์‹คํ–‰ ๋˜๋ฉด .run ์‹คํ–‰ -> 1์ดˆ ๋งˆ๋‹ค timerTicked ์•ก์…˜ ์‹คํ–‰ ๋จ -> ํƒ€์ด๋จธ ๋ฉˆ์ถค -> .cancel ํ˜ธ์ถœ -> CancelID timer ๋กœ ๋“ฑ๋ก๋œ ์ด๋ฒคํŠธ ์ทจ์†Œ(์ด๋•Œ ์ด๋ฒคํŠธ๋Š” state.isTimerOn ์ผ ๋•Œ ์‹คํ–‰๋˜๋Š” .run)
  • ์ •๋ฆฌ ) effect์— .cancellable๋กœ cancelID ๋“ฑ๋ก, ์ทจ์†Œ๊ฐ€ ํ•„์š”ํ•œ ํƒ€์ด๋ฐ์— .cancel๋กœ effect ์ทจ์†Œ

.merge

case .addCartButtonTappedAndAlertShow:
    return .merge(
        .send(.addCartButtonTapped),
        .send(.alertShow)
    )
}
  • ์—ฌ๋Ÿฌ Effect๋ฅผ ๋™์‹œ์— ์‹คํ–‰
  • ์ˆœ์„œ๊ฐ€ ๋ณด์žฅ๋˜์ง€ ์•Š์Œ
  • ๊ด€๋ จ ์—†๋Š” ๊ฐ๊ฐ์˜ Effect๋ฅผ ์‹คํ–‰์‹œํ‚ค๊ณ  ๊ทธ์— ๋”ฐ๋ฅธ ๊ฐ๊ฐ์˜ State๋ฅผ ๋ณ€ํ™” ์‹œํ‚ฌ ๋•Œ ์‚ฌ์šฉ

.concatenate

case .addCartButtonTappedAndAlertShow:
    return .concatenate(
        .send(.addCartButtonTapped),
        .send(.alertShow)
    )
}
  • ์—ฌ๋Ÿฌ Effect๋ฅผ ๋™์‹œ์— ์‹คํ–‰
  • ์ˆœ์„œ๊ฐ€ ๋ณด์žฅ๋จ

 

https://www.pexels.com/ko-kr/photo/256150/

'iOS ๐ŸŽ > Architecture' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€

[SwiftUI/TCA] Binding  (0) 2023.12.07
[SwiftUI/TCA] Scope  (1) 2023.11.30
[SwiftUI/TCA] Store and ViewStore  (0) 2023.11.29
[SwiftUI/TCA] Timer ๋งŒ๋“ค๊ธฐ  (1) 2023.11.24
[SwiftUI/TCA] TCA ๊ธฐ๋ณธ ๊ฐœ๋…์— ๋Œ€ํ•ด ์•Œ์•„๋ณด๊ธฐ  (0) 2023.11.21