-
[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๋ฅผ ๋ง๋ค ์ ์์๋ค.
'iOS > TCA' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[TCA] TCA ์๋ ๋ฐฉ์ (0) 2023.08.31 [SwiftUI] ์ํ๊ด๋ฆฌ(Higher-Order Reducers) (0) 2023.08.02 [SwiftUI] ์ํ๊ด๋ฆฌ(State Pullbacks) (0) 2023.07.07 [SwiftUI] ์ํ๊ด๋ฆฌ(redux) (0) 2023.07.06 [SwiftUI] ์ํ ๊ด๋ฆฌ (0) 2023.07.05