ABOUT ME

-

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

     

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

     

     

     

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

    ์ด์ „์— ์ž‘์„ฑํ–ˆ๋˜ reducer ์ฝ”๋“œ๊ฐ€ ๋ฌด๊ฑฐ์›Œ๋ณด์ž„ -> ํ•˜๋‚˜์˜ ํฐ reducer๋ฅผ ํ•œ ๊ฐ€์ง€ ํŠน์ • ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๋Š” ์—ฌ๋Ÿฌ ๊ฐœ์˜ reducer๋กœ ์ชผ๊ฐ  ๋‹ค์Œ ์„œ๋กœ ์—ฐ๊ฒฐํ•ด์„œ master reducer๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž

     

     

     

     

     

     

    ๐Ÿ“š ์ฝ”๋“œ

     

    - ๊ธฐ์กด์— ์—ฌ๋Ÿฌ ๊ฐ€์ง€ ์—ญํ• ์„ ํ•˜๋Š” ํ•จ์ˆ˜๊ฐ€ ์ •์˜๋˜์–ด ์žˆ๋Š” reducer ์ฝ”๋“œ

     

    func appReducer(value: inout AppState, action: AppAction) -> Void {
      switch action {
      case .counter(.decrTapped):
        state.count -= 1
    
      case .counter(.incrTapped):
        state.count += 1
    
      case .primeModal(.saveFavoritePrimeTapped):
        state.favoritePrimes.append(state.count)
        state.activityFeed.append(.init(timestamp: Date(), type: .addedFavoritePrime(state.count)))
    
      case .primeModal(.removeFavoritePrimeTapped):
        state.favoritePrimes.removeAll(where: { $0 == state.count })
        state.activityFeed.append(.init(timestamp: Date(), type: .removedFavoritePrime(state.count)))
    
      case let .favoritePrimes(.deleteFavoritePrimes(indexSet)):
        for index in indexSet {
          let prime = state.favoritePrimes[index]
          state.favoritePrimes.remove(at: index)
          state.activityFeed.append(.init(timestamp: Date(), type: .removedFavoritePrime(prime)))
        }
      }
    }

     

     

     

     

     

     

     

    - ๋‘ ๊ฐœ์˜ reducer๊ฐ€ ์žˆ์„ ๋•Œ ํ•˜๋‚˜์˜ reducer๋กœ ๊ฒฐํ•ฉํ•˜๋Š” ๋ฐฉ๋ฒ•

    : ์ฒซ ๋ฒˆ์งธ reducer๋ฅผ ์‹คํ–‰ํ•œ ๋’ค ๋‘ ๋ฒˆ์งธ reducer ์‹คํ–‰ํ•œ๋‹ค!

     

    func combine<Value, Action>(
      _ first: @escaping (inout Value, Action) -> Void,
      _ second: @escaping (inout Value, Action) -> Void
    ) -> (inout Value, Action) -> Void {
    
      return { value, action in
        first(&value, action)
        second(&value, action)
      }
    }

     

     

     

     

     

     

     

    - ๊ธฐ์กด ์ฝ”๋“œ๋ฅผ ์—ฌ๋Ÿฌ ๊ฐœ์˜ reducer๋กœ ๋ถ„ํ•ดํ•ด๋ณด์ž

     

    func counterReducer(value: inout AppState, action: AppAction) -> Void {
      switch action {
      case .counter(.decrTapped):
        state.count -= 1
    
      case .counter(.incrTapped):
        state.count += 1
    
      default:
        break
      }
    }
    
    func primeModalReducer(state: inout AppState, action: AppAction) -> Void {
      switch action {
      case .primeModal(.addFavoritePrime):
        state.favoritePrimes.append(state.count)
        state.activityFeed.append(.init(timestamp: Date(), type: .addedFavoritePrime(state.count)))
    
      case .primeModal(.removeFavoritePrime):
        state.favoritePrimes.removeAll(where: { $0 == state.count })
        state.activityFeed.append(.init(timestamp: Date(), type: .removedFavoritePrime(state.count)))
    
      default:
        break
      }
    }
    
    func favoritePrimesReducer(state: inout AppState, action: AppAction) -> Void {
      switch action {
      case let .favoritePrimes(.removeFavoritePrimes(indexSet)):
        for index in indexSet {
          state.activityFeed.append(.init(timestamp: Date(), type: .removedFavoritePrime(state.favoritePrimes[index])))
          state.favoritePrimes.remove(at: index)
        }
    
      default:
        break
      }
    }

     

     

     

     

     

     

     

     

     

    let appReducer = combine(combine(counterReducer, primeModalReducer), favoritePrimesReducer)

     

    app reducer๋ฅผ ์•„๊นŒ ๋งŒ๋“  combine ํ•จ์ˆ˜๋ฅผ ์ด์šฉํ•˜์—ฌ ์ƒˆ๋กญ๊ฒŒ ์ƒ์„ฑํ•œ๋‹ค

    ํ˜„์žฌ combine ํ•จ์ˆ˜๋Š” ์—ฌ๋Ÿฌ ๊ฐœ๋ฅผ ์‹คํ–‰ํ• ์ˆ˜๋ก ๊ณ„์†ํ•ด์„œ ์ค‘์ฒฉํ•ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋ณต์žกํ•ด์ง„๋‹ค.

    ๋”ฐ๋ผ์„œ ์—ฌ๋Ÿฌ ๊ฐœ์˜ ๊ฐœ๋ณ„ reducer๋ฅผ ๋ฐ›์„ ์ˆ˜ ์žˆ๋Š” ํ•จ์ˆ˜๋กœ ์ƒˆ๋กญ๊ฒŒ ์ •์˜ํ•œ๋‹ค

     

    func combine<Value, Action>(
      _ reducers: (inout Value, Action) -> Void...
    ) -> (inout Value, Action) -> Void {
    
      return { value, action in
        for reducer in reducers {
          reducer(&value, action)
        }
      }
    }

     

    ์œ„์™€ ๊ฐ™์ด ํ•จ์ˆ˜๋ฅผ ๋ณ€๊ฒฝํ•˜๋ฉด appReducer๋ฅผ ์ข€ ๋” ๊ฐ„๋‹จํ•˜๊ณ  ์ฝ๊ธฐ ์‰ฝ๊ฒŒ ์ •์˜ํ•  ์ˆ˜ ์žˆ๋‹ค.

     

    let appReducer = combine(
      counterReducer,
      primeModalReducer,
      favoritePrimesReducer
    )

     

     

     

     

     

     

     

    ๊ธฐ์กด counterReducer ํ•จ์ˆ˜๋ฅผ ์‚ดํŽด๋ณด๋ฉด ์‹ค์ œ๋กœ count์˜ ๊ฐ’๋งŒ ํ•„์š”ํ•จ์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ , ์ „์ฒด appState๋ฅผ ๋งค๊ฐœ๋กœ ๋ฐ›๋Š”๋‹ค.

    ๋”ฐ๋ผ์„œ state: inout Int๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ํ•„์š”ํ•œ ๊ฐ’๋งŒ AppState์—์„œ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋„๋ก ํ•œ๋‹ค.

     

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

     

    ํ•˜์ง€๋งŒ ์ด์™€ ๊ฐ™์ด ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ๋ณ€๊ฒฝํ•˜๋Š” ๊ฒฝ์šฐ reducer๊ฐ€ ๊ฐ™์€ state๋ฅผ ๊ฐ€๋ฆฌํ‚ค์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.

    counterReducer๋Š” integer๊ฐ’๋งŒ ํ•„์š”๋กœ ํ•˜์ง€๋งŒ, ๋‹ค๋ฅธ reducer๋“ค์€ ์ „์ฒด appState๊ฐ€ ํ•„์š”ํ•˜๋‹ค.

     

     

     

     

     โœจ ์ด๋Ÿฌํ•œ ์ƒํ™ฉ์—์„œ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด pullback!

     

     

    pullback์€ ์ž‘์€ ๋ฐ์ดํ„ฐ์—์„œ ํฐ ๋ฐ์ดํ„ฐ๋กœ ๋ณ€ํ™˜ํ•˜๋Š”๋ฐ ์œ ๋ฆฌํ•˜๋‹ค!

    ์˜ˆ๋ฅผ ๋“ค์–ด integer ๊ฐ’์ด ์ฃผ์–ด์กŒ์„ ๋•Œ ์‚ฌ์šฉ์ž ID ํ•„๋“œ ๊ฐ’์— ํˆฌ์˜ํ•˜์—ฌ ์ƒ์š”์ž ๋ชจ๋ธ์— ๋Œ€ํ•œ ์ˆ ์–ด๊ฐ€ ๋˜๋„๋ก ๋‹ค์‹œ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋‹ค.

     

    Local ์ƒํƒœ์˜ reducer๋ฅผ Global ์ƒํƒœ์˜ reducer๋กœ ๋ณ€ํ™˜ํ•  ์ˆ˜ ์žˆ๋Š” ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž.

     

     

    func pullback<LocalValue, GlobalValue, Action>(
      _ reducer: @escaping (inout LocalValue, Action) -> Void
    ) -> (inout GlobalValue, Action) -> Void {
    
    }

     

     

    ์š”๋Ÿฐ basic ํ•จ์ˆ˜์—๋‹ค๊ฐ€ LocalValue์™€ GlobalValue ์ œ๋„ค๋ฆญ์„ ์—ฐ๊ฒฐํ•˜์ž

     

    func pullback<LocalValue, GlobalValue, Action>(
      _ reducer: @escaping (inout LocalValue, Action) -> Void,
      get: @escaping (GlobalValue) -> LocalValue,
      set: @escaping (inout GlobalValue, LocalValue) -> Void
    ) -> (inout GlobalValue, Action) -> Void {
    
      return  { globalValue, action in
        var localValue = get(globalValue)
        reducer(&localValue, action)
        set(&globalValue, localValue)
      }
    }

     

     

    pullback(counterReducer, get: { $0.count }, set: { $0.count = $1 }),

     

    pullback ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๋‹ค.

     

     

     

     

     

    get, set ์ธ์ž๋ฅผ WritableKeyPath๋ฅผ ํ†ตํ•ด ์ข€ ๋” ๊ฐ„๋‹จํ•˜๊ฒŒ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.

     

    func pullback<LocalValue, GlobalValue, Action>(
      _ reducer: @escaping (inout LocalValue, Action) -> Void,
      value: WritableKeyPath<GlobalValue, LocalValue>
    ) -> (inout GlobalValue, Action) -> Void {
      return { globalValue, action in
        reducer(&globalValue[keyPath: value], action)
      }
    }

     

     

    key path๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.

     

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

     

    \.self๋Š” AppState์—์„œ AppState๋กœ ์ด๋™ํ•˜๋Š” ํ•ต์‹ฌ ๊ฒฝ๋กœ์ด๋‹ค.

     

     

     

     

     

     

     

    ์ด์ œ favoritePrimesReducer ํ•จ์ˆ˜๋ฅผ pullbackํ•ด๋ณด์ž!

     

    // ์›ํ•˜๋Š” ๋ฐ์ดํ„ฐ๋งŒ AppState์—์„œ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋„๋ก ๊ตฌ์กฐ์ฒด ์ƒˆ๋กœ ์„ ์–ธ
    struct FavoritePrimesState {
      var favoritePrimes: [Int]
      var activityFeed: [AppState.Activity]
    }
    
    
    func favoritePrimesReducer(value: inout FavoritePrimesState, action: AppAction) -> Void {
      switch action {
      case let .favoritePrimes(.removeFavoritePrimes(indexSet)):
        for index in indexSet {
          state.activityFeed.append(.init(timestamp: Date(), type: .removedFavoritePrime(state.favoritePrimes[index])))
          state.favoritePrimes.remove(at: index)
        }
    
      default:
        break
      }

     

     

    ๋ฐœ์ƒํ•˜๋Š” ์—๋Ÿฌ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด AppState์— ์žˆ๋Š” favoritePrimesReducer๋ฅผ ์ˆ˜์ •ํ•ด์ค€๋‹ค.

    (ํ•„์š”ํ•œ ์ƒํƒœ ๊ฐ’์ด ๋‹จ์ผ AppState๊ฐ€ ์•„๋‹ˆ๋ผ ๋‘ ๊ฐœ์˜ AppState์—์„œ ๋‚˜์˜ค๊ธฐ ๋•Œ๋ฌธ)

     

    extension AppState {
      var favoritePrimesState: FavoritePrimesState {
        get {
          return FavoritePrimesState(
            favoritePrimes: self.favoritePrimes,
            activityFeed: self.activityFeed
          )
        }
        set {
          self.activityFeed = newValue.activityFeed
          self.favoritePrimes = newValue.favoritePrimes
        }
      }
    }

     

     

     

     

     

     

     

    appReducer๊ฐ’์„ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋ณ€๊ฒฝํ•ด์ค€๋‹ค.

     

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

     

     

     

     

     

     


    ์ž‘์—…์— ํ•„์š”ํ•œ ์ตœ์†Œํ•œ์˜ state์—์„œ๋งŒ ์ž‘๋™ํ•˜๋„๋ก ์ „์ฒด reducer๋ฅผ ์„ธ๋ถ€ํ™”ํ•˜๊ณ , ์ „์ฒด reducer์— ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋„๋ก ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ•ด๋ดค๋‹ค.

     

     

     

     

     

Designed by Tistory.