iOS/TCA

[SwiftUI] ์ƒํƒœ๊ด€๋ฆฌ(Action Pullbacks)

year.number 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๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์—ˆ๋‹ค.

 

 

 

 

 

 

๋Œ“๊ธ€์ˆ˜0