ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [SwiftUI] ์ƒํƒœ๊ด€๋ฆฌ(Action Pullbacks)
    iOS/TCA 2023. 7. 18. 00:28

     

    PointFree ๊ฐ•์˜ Composable Architecture
    Composable State Management: Action Pullbacks ์ •๋ฆฌ

     

     

     

    ๐Ÿ“š ์ด๋ฒˆ ๊ฐ•์˜ ๋ชฉํ‘œ

    AppAction์„ ๊ฐœ๋ณ„ Action์œผ๋กœ ๋ถ„๋ฆฌํ•ด๋ณด์ž

     

     

     

    ๐Ÿ“š ์ฝ”๋“œ

     

     

    - ๊ธฐ์กด ์ฝ”๋“œ์˜ ๋ฌธ์ œ์ 

     

    func counterReducer(state: inout Int, action: AppAction) -> Void {
      switch action {
      case .counter(.decrTapped):
        state -= 1
    
      case .counter(.incrTapped):
        state += 1
    
      default:
        break
      }
    }

     

    counterReducer ํ•จ์ˆ˜๋ฅผ ๋ณด๋ฉด reducer๊ฐ€ ์ •์ˆ˜์—์„œ๋งŒ ์ž‘๋™ํ•œ๋‹ค.

     

    ํ•˜์ง€๋งŒ ์ „์ฒด AppAction์„ ๋ฐ›๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์—

    CounterAction ์—ด๊ฑฐํ˜•์— ์ƒˆ๋กœ์šด ์•ก์…˜์„ ์ถ”๊ฐ€ํ•˜๋ฉด ์ปดํŒŒ์ผ๋Ÿฌ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š๊ณ 

    reducer์—์„œ ํ•ด๋‹น ์•ก์…˜์ด ์ž๋™์œผ๋กœ ๋ฌด์‹œ(default)๋˜๋Š” ๋ฌธ์ œ์ ์ด ๋ฐœ์ƒํ•œ๋‹ค. 

     

     

     

     

     

    func counterReducer(state: inout Int, action: CounterAction) -> Void {
      switch action {
      case .decrTapped:
        state -= 1
    
      case .incrTapped:
        state += 1
      }
    }

     

    ๋”ฐ๋ผ์„œ ์ฝ”๋“œ๋ฅผ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ˆ˜์ •ํ•˜๋ฉด ๋”์ด์ƒ default ๊ฐ’์„ ์„ค์ •ํ•˜์ง€ ์•Š์•„๋„ ๋œ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๋ชจ๋“ˆ๋กœ ์ถ”์ถœ์ด ๊ฐ€๋Šฅํ•ด์ง„๋‹ค.

     

     

     

     

     

    let appReducer = combine(
      pullback(counterReducer, value: \.count),
      primeModalReducer,
      pullback(favoritePrimesReducer, value: \.favoritePrimesState)
    )

     

    ๐Ÿ›‘ Type of expression is ambiguous without more context

     

    ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜๋Š”๋ฐ, ๊ทธ ์ด์œ ๋Š” counterReducer๊ฐ€ state์— ๊ด€ํ•ด์„œ๋Š” ๋‹ค๋ฅธ reducer์™€ ๋™์ผํ•œ ์–ธ์–ด๋ฅผ ์‚ฌ์šฉํ•˜์ง€๋งŒ, action์— ๊ด€ํ•ด์„œ๋Š” ๋™์ผํ•œ ์–ธ์–ด๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

     

    counterReducer๋Š” counterAction๋งŒ ์ดํ•ดํ•  ์ˆ˜ ์žˆ๊ณ , AppAction์˜ ์ „์ฒด ์•ก์…˜์€ ์ดํ•ดํ•  ์ˆ˜ ์—†์œผ๋ฏ€๋กœ, ๋” ์ด์ƒ reducer๋ฅผ ํ•จ๊ป˜ ๊ฒฐํ•ฉํ•  ์ˆ˜ ์—†๋‹ค. ๋”ฐ๋ผ์„œ localAction๋งŒ ์ดํ•ดํ•˜๋Š” reducer๋ฅผ Global Action์„ ์ดํ•ดํ•˜๋Š” reducer๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ์ž‘์—…์ด ํ•„์š”ํ•˜๋‹ค.

     

     

     

    Local ์ƒํƒœ์—์„œ ์ž‘๋™ํ•˜๋Š” reducer๋ฅผ Global ์ƒํƒœ์—์„œ ์ž‘๋™ํ•˜๋Š” reducer๋กœ ๋ณ€ํ™˜ํ•˜๊ณ ์ž ํ–ˆ์„ ๋•Œ, Global ์ƒํƒœ์—์„œ Local ์ƒํƒœ๋กœ ๋‚ด๋ ค๊ฐ€๋Š” ํ•ต์‹ฌ ๊ฒฝ๋กœ๋ฅผ ๋”ฐ๋ผ์„œ pullbackํ•˜๋Š” ๊ฒƒ์ด ํ•ด๊ฒฐ์ฑ…์ž„์„ ์•Œ ์ˆ˜ ์žˆ์—ˆ๋‹ค. action์—์„œ๋Š” ์–ด๋–ค ์‹์œผ๋กœ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์„๊นŒ?

     

    ์•ก์…˜์€ enumํƒ€์ž…์œผ๋กœ ์ •์˜๋˜์–ด ์žˆ๊ณ , ์ด๋Ÿฌํ•œ ์—ด๊ฑฐํ˜•์—๋Š” ํ‚ค ๊ฒฝ๋กœ๋ผ๋Š” ๊ฐœ๋…์ด ์—†๋‹ค. ํ•˜์ง€๋งŒ key path์˜ ๋ณธ์งˆ์„ ๊ตฌํ˜„ํ•ด๋ณด๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

     

    struct _KeyPath<Root, Value> {
      let get: (Root) -> Value
      let set: (inout Root, Value) -> Void
    }

     

    key path์˜ ํ•ต์‹ฌ์€ root์—์„œ ๊ฐ’์„ getํ•˜๋Š” ์ˆ˜๋‹จ์„ ์ œ๊ณตํ•˜๊ณ , root ๋‚ด๋ถ€์˜ ๊ฐ’์„ setํ•ด์„œ ๋ณ€๊ฒฝ๋œ ์ƒˆ๋กœ์šด root๋ฅผ ์ œ๊ณตํ•˜๋Š” ๊ฒƒ์ด๋‹ค.

    ์ด ๋‘ ๊ฐ€์ง€ ์—ฐ์‚ฐ์€ ๊ธฐ๋ณธ์ ์œผ๋กœ ๊ตฌ์กฐ์ฒด์—์„œ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๋Š” ์ผ๋ฐ˜์ ์ธ ์—ฐ์‚ฐ์ด๋‹ค.

     

    ์—ด๊ฑฐํ˜•์—๋„ ๋น„์Šทํ•œ ์—ฐ์‚ฐ์ด ์กด์žฌํ•œ๋‹ค.

     

    AppAction.counter(CounterAction.incrTapped)

     

    ์ด ๊ฒƒ์€ setter ์—ฐ์‚ฐ๊ณผ ๋น„์Šทํ•˜๊ฒŒ ์ž‘๋™ํ•˜๋ฉฐ, ๊ฐ’์„ ๊ฐ€์ ธ์™€์„œ AppAction ์—ด๊ฑฐํ˜• ์•ˆ์— ์‚ฝ์ž…ํ•˜๋Š” ๊ฒƒ์ด๋‹ค.

    ๋‹ค์Œ์€ getter์™€ ๋น„์Šทํ•œ ์—ฐ์‚ฐ์œผ๋กœ ์—ด๊ฑฐํ˜• ๊ฐ’์„ ๊ฐ€์ ธ์™€์„œ ํŠน์ • ์ผ€์ด์Šค์—์„œ ๊ฐ’์„ ์ถ”์ถœํ•  ์ˆ˜ ์žˆ๋‹ค.

     

    let action = AppAction.favoritePrimes(.deleteFavoritePrimes([1]))
    let favoritePrimesAction: FavoritePrimesAction?
    switch action {
    case let .favoritePrimes(action):
      favoritePrimesAction = action
    default:
      favoritePrimesAction = nil
    }

     

     

     

    ๋งŒ์•ฝ Swift์—์„œ ์—ด๊ฑฐํ˜• key path๋ฅผ ์ง€์›ํ–ˆ๋‹ค๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์—ˆ์„ ๊ฒƒ์ด๋‹ค.

     

    let action = AppAction.favoritePrimes(.deleteFavoritePrimes([1]))
    let favoritePrimesAction: FavoritePrimesAction?
    switch action {
    case let .favoritePrimes(action):
      favoritePrimesAction = action
    default:
      favoritePrimesAction = nil
    }

     

     

     

     


    ์—ด๊ฑฐํ˜• ํ”„๋กœํผํ‹ฐ

     

    ๊ตฌ์กฐ์ฒด(struct)๋Š” . ๊ตฌ๋ฌธ์„ ํ†ตํ•ด ๋งค์šฐ ๊ฐ„๋‹จํ•œ ๋ฐ์ดํ„ฐ ์•ก์„ธ์Šค๊ฐ€ ๊ฐ€๋Šฅํ•˜๋‹ค. ์—ด๊ฑฐํ˜•(enum)์€ ์ด๋Ÿฌํ•œ ์–ดํฌ๋˜์Šค๊ฐ€ ์—†๋‹ค.

    ๋˜ํ•œ ๊ตฌ์กฐ์ฒด์˜ ๋ชจ๋“  ํ”„๋กœํผํ‹ฐ๋Š” ์ปดํŒŒ์ผ๋Ÿฌ์—์„œ ์ƒ์„ฑ๋œ key path๋ฅผ ๊ฐ€์ง€๋ฉฐ, ์ด๋Š” getter/setter์—ญํ• ์„ ์ˆ˜ํ–‰ํ•œ๋‹ค. ํ•˜์ง€๋งŒ ์—ด๊ฑฐํ˜•์€ ์—†๋‹ค.

     

    ์ด๋Ÿฌํ•œ ๋ฌธ์ œ๋ฅผ ์—ด๊ฑฐํ˜• ํ”„๋กœํผํ‹ฐ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ•ด๊ฒฐํ•˜๊ณ ์ž ํ•œ๋‹ค.

     

     

    enum AppAction {
        case counter(CounterAction)
        case primeModal(PrimeModalAction)
        case favoritePrimes(FavoritePrimesAction)
        
        // enum properties
        // ํ”„๋กœํผํ‹ฐ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ AppAction ์—ด๊ฑฐํ˜•์— ์žˆ๋Š” ๋ชจ๋“  ์ผ€์ด์Šค์˜ ๊ด€๋ จ ๋ฐ์ดํ„ฐ์— ๋Œ€ํ•œ ์ธ์Šคํ„ด์Šค ์•ก์„ธ์Šค ๊ฐ€๋Šฅ
        var counter: CounterAction? {
            get {
                guard case let .counter(value) = self else { return nil }
                return value
            }
            set {
                guard case .counter = self, let newValue = newValue else { return }
                self = .counter(newValue)
              }
        }
        var primeModal: PrimeModalAction? {
            get {
                guard case let .primeModal(value) = self else { return nil }
                return value
            }
            set {
                guard case .primeModal = self, let newValue = newValue else { return }
                self = .primeModal(newValue)
            }
        }
        var favoritePrimes: FavoritePrimesAction? {
            get {
                guard case let .favoritePrimes(value) = self else { return nil }
                return value
            }
            set {
                guard case .favoritePrimes = self, let newValue = newValue else { return }
                self = .favoritePrimes(newValue)
            }
        }
    }

     

    ์—ด๊ฑฐํ˜• ํ”„๋กœํผํ‹ฐ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด AppAction ์—ด๊ฑฐํ˜•์— ์žˆ๋Š” ๋ชจ๋“  case์˜ ๊ด€๋ จ ๋ฐ์ดํ„ฐ์— ๋Œ€ํ•œ ์ธ์Šคํ„ด์Šค ์•ก์„ธ์Šค๋ฅผ ํ•  ์ˆ˜ ์žˆ๋‹ค.

    ์ด๋Ÿฌํ•œ ์—ด๊ฑฐํ˜• ์†์„ฑ์€ ์œ ์šฉํ•˜์ง€๋งŒ ์œ ์ง€/๊ด€๋ฆฌํ•˜๊ธฐ ๋ฒˆ๊ฑฐ๋กญ๊ธฐ ๋•Œ๋ฌธ์— SwiftSyntax ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ†ตํ•ด ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.

     

     

     

    ์ด๋ ‡๊ฒŒ ๊ตฌํ˜„ํ•˜๋ฉด ๊ตฌ์กฐ์ฒด์ฒ˜๋Ÿผ ์‰ฝ๊ฒŒ ์ž‘๋™ํ•œ๋‹ค.

    ์˜ˆ๋ฅผ ๋“ค๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์—ด๊ฑฐํ˜• ๊ฐ’์— ์‰ฝ๊ฒŒ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋‹ค.

     

    let someAction = AppAction.counter(.incrementTapped)
    someAction.counter  // Optional(incrememtTapped)
    someAction.favoritePrimes   // nil
    
    // ๊ฐ ์—ด๊ฑฐํ˜• ์ผ€์ด์Šค์— ๋Œ€ํ•œ key path๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ๋‹ค
    \AppAction.counter  // WritableKeyPath<AppAction, CounterAction?>

     

     

     

     

     


    Pulling back reducers along actions

     

    Local ์•ก์…˜์—์„œ ์ž‘๋™ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ๊ณ  ์žˆ๋Š” reducer๋ฅผ Global ์•ก์…˜์—์„œ ์ž‘๋™ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ๊ณ  ์žˆ๋Š” reducer๋กœ pullbackํ•˜๊ธฐ๋ฅผ ์›ํ•œ๋‹ค.๊ธฐ๋ณธ์ ์ธ ํ•จ์ˆ˜์˜ ํ‹€์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

     

    func pullback<Value, GlobalAction, LocalAction>(
      _ reducer: @escaping (inout Value, LocalAction) -> Void,
      ???
    ) -> (inout Value, GlobalAction) -> Void {
    
      ???
    }

     

     

     

    ์šฐ๋ฆฌ๊ฐ€ ์›ํ•˜๋Š” ๊ฒƒ์€ optional local ์•ก์…˜์— ๋Œ€ํ•œ key path์ด๋‹ค.

     

    func pullback<Value, GlobalAction, LocalAction>(
      _ reducer: @escaping (inout Value, LocalAction) -> Void,
      action: WritableKeyPath<GlobalAction, LocalAction?>
    ) -> (inout Value, GlobalAction) -> Void {
    
      ???
    }

     

     

     

     

    Global ์•ก์…˜์ด ๋“ค์–ด์˜ฌ ๋•Œ key path๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ Local ์•ก์…˜์„ ์ถ”์ถœํ•˜๋ ค๊ณ  ์‹œ๋„ํ•œ๋‹ค. ์„ฑ๊ณตํ•˜๋ฉด reducer๋กœ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ๊ณ , ์‹คํŒจํ•˜๋ฉด ์•„๋ฌด ์ž‘์—…๋„ ํ•˜์ง€ ์•Š๊ณ  ํ•ด๋‹น ์•ก์…˜์„ ์กฐ์šฉํžˆ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.

    func pullback<GlobalValue, LocalValue, GlobalAction, LocalAction>(
      _ reducer: @escaping (inout LocalValue, LocalAction) -> Void,
      value: WritableKeyPath<GlobalValue, LocalValue>,
      action: WritableKeyPath<GlobalAction, LocalAction?>
    ) -> (inout GlobalValue, GlobalAction) -> Void {
    
      return { globalValue, globalAction in
        guard let localAction = globalAction[keyPath: action] else { return }
        reducer(&globalValue[keyPath: value], localAction)
      }
    }

     

     

     

     

    ์ด์ œ AppAction์„ ๊ฐœ๋ณ„ CounterAction์œผ๋กœ ํˆฌ์˜ํ•˜๋Š” Key path๋ฅผ ํ†ตํ•ด์„œ counter reducer๋ฅผ pullbackํ•  ์ˆ˜ ์žˆ๋‹ค

     

    let _appReducer = combine(
      pullback(counterReducer, value: \.count, action: \.counter),
      primeModalReducer,
      pullback(favoritePrimesReducer, value: \.favoritePrimesState, action: \.self)
    )
    let appReducer = pullback(_appReducer, value: \.self, action: \.self)

     

     

     

     

     

    pullback(favoritePrimesReducer, value: \.favoritePrimesState, action: \.self)

     

    favoritePrimesReducer, primeModalReducer์˜ ๊ฒฝ์šฐ ID key path์— ๋”ฐ๋ผ action์„ ๋‹ค์‹œ ๊ฐ€์ ธ์˜ค๊ณ  ์žˆ๋Š”๋ฐ,

    ์ด๋Š” ์–ด์ ผํžˆ ์ „์ฒด AppAction ์—์„œ ์ž‘๋™ํ•˜๊ณ  ์žˆ๋‹ค๋Š” ์˜๋ฏธ์ด๋‹ค.

     

     

     

     

    func favoritePrimesReducer(state: inout FavoritePrimesState, action: FavoritePrimesAction) -> Void {
      switch action {
      case let .deleteFavoritePrimes(indexSet):
        for index in indexSet {
          state.activityFeed.append(.init(timestamp: Date(), type: .removedFavoritePrime(state.favoritePrimes[index])))
          state.favoritePrimes.remove(at: index)
        }
      }
    }
    
    
    
    func primeModalReducer(state: inout AppState, action: PrimeModalAction) -> Void {
      switch action {
      case .removeFavoritePrimeTapped:
        state.favoritePrimes.removeAll(where: { $0 == state.count })
        state.activityFeed.append(.init(timestamp: Date(), type: .removedFavoritePrime(state.count)))
    
      case .saveFavoritePrimeTapped:
        state.favoritePrimes.append(state.count)
        state.activityFeed.append(.init(timestamp: Date(), type: .addedFavoritePrime(state.count)))
      }
    }

     

    ๋จผ์ € reducer ํ•จ์ˆ˜๋ฅผ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ˆ˜์ •ํ•œ๋‹ค.

     

     

     

     

    let _appReducer = combine(
      pullback(counterReducer, value: \.count, action: \.counter),
      pullback(primeModalReducer, value: \.self, action: \.primeModal),
      pullback(favoritePrimesReducer, value: \.favoritePrimesState, action: \.favoritePrimes)
    )

     

    ๋‹ค์Œ์œผ๋กœ pullback์„ ์—…๋ฐ์ดํŠธํ•ด์ค€๋‹ค.

     

     

     

     

     

    ๐Ÿ›‘ Generic parameter ‘Action’ could not be inferred

     

    ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜๋Š”๋ฐ, ๊ทธ ์ด์œ ๋Š” reducer๋ฅผ AppAction์—์„œ ์™„์ „ํžˆ ๋ถ„๋ฆฌํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— ๋”์ด์ƒ ์ถ”๋ก ํ•  ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ์ปดํŒŒ์ผ๋Ÿฌ๋Š” ์–ด๋–ค ์ข…๋ฅ˜์˜ Global Action์œผ๋กœ ๋˜๋Œ์•„๊ฐ€๋Š”์ง€ ์•Œ ์ˆ˜ ์—†๋‹ค.

     

    let _appReducer: (inout AppState, AppAction) -> Void = combine(
      pullback(counterReducer, value: \.count, action: \.counter),
      pullback(primeModalReducer, value: \.self, action: \.primeModal),
      pullback(favoritePrimesReducer, value: \.favoritePrimesState, action: \.favoritePrimes)
    )

     

    ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ•ด์„œ ํ•ด๊ฒฐํ•˜์ž.

     

     

     

     

     

     

     


    ๋!

     

    ์ „์—ญ state์™€ ์ „์—ญ action์— ๋Œ€ํ•ด ์ž‘๋™ํ•˜๋˜ ๊ฑฐ๋Œ€ํ•œ reducer๋ฅผ ์ž‘์€ ์ƒํƒœ์—์„œ๋งŒ ์ž‘๋™ํ•˜๋Š” 3๊ฐœ์˜ reducer๋กœ ๋ฆฌํŒฉํ† ๋งํ–ˆ๋‹ค. ๋‹ค์Œ์œผ๋กœ ์ด๋Ÿฌํ•œ ์ž‘์€ reducer๋“ค์„ ๊ฐ€์ ธ์™€ ๊ฒฐํ•ฉํ•˜์—ฌ ํฐ AppReducer๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์—ˆ๋‹ค.

     

     

     

     

     

     

Designed by Tistory.