์ถ์ฒ ๋ฐ ์ฐธ๊ณ ์ฌ์ดํธ
https://axiomatic-fuschia-666.notion.site/Chapter-3-TCA-2-c56b24efb2154dad9ed8e54139247024
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๋ฅผ ๋์์ ์คํ
- ์์๊ฐ ๋ณด์ฅ๋จ
'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 |