J
[TCA] Effect? DependencyValues? (์์ ํฌํจ) ๋ณธ๋ฌธ
๐ง ์์ํ๊ธฐ ์ ์…
์ด๋ฒ ๊ธ์์๋ ์กฐ๊ธ ๋ ์ฌํ์ ์ผ๋ก Effect์ DependencyValues, ๊ทธ๋ฆฌ๊ณ ๋คํธ์ํฌ ํต์ ์ ์งํํ ๊ฒฝ์ฐ๋ฅผ ์์๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
TCA์ Effect์ DependencyValues ์ค๋ช
TCA์์๋ ๋น๋๊ธฐ ์์
(๋คํธ์ํฌ ์์ฒญ, ํ์ด๋จธ, ๋ฐ์ดํฐ ์ ์ฅ ๋ฑ)์ Reducer ๋ด๋ถ์์ ์ง์ ์คํํ ์ ์๊ณ ,
Effect๋ฅผ ์ฌ์ฉํ์ฌ ์ฒ๋ฆฌ ํด์ผ ํฉ๋๋ค.
๋ํ, ์ต์ TCA์์๋ Environment ๋์
DependencyValues๋ฅผ ์ฌ์ฉํ์ฌ ์์กด์ฑ์ ์ฃผ์
ํ๊ณ ๊ด๋ฆฌํฉ๋๋ค.
Effect๋? (๋น๋๊ธฐ ์์ ์ ์ฒ๋ฆฌํ๋ ๋ฐฉ์)
Effect์ ์ญํ
- ๋คํธ์ํฌ ์์ฒญ(API ํธ์ถ)
- ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฝ๊ธฐ/์ฐ๊ธฐ
- ํ์ด๋จธ ์คํ
- ํ์ผ ์ ์ฅ/์ฝ๊ธฐ
- ์ ๋๋ฆฌํฑ์ค(Analytics) ๋ก๊ทธ ์ ์ก
โ ์๋ชป๋ ๋ฐฉ์: Effect ์์ด ๋น๋๊ธฐ ์์ ์ ์ง์ Reducer์์ ์คํ
case .fetchRandomNumber:
let randomValue = Int.random(in: 1...100) // ๋๊ธฐ ์์
state.count = randomValue // ์ํ ๋ณ๊ฒฝ
return .none
- Reducer๋ ์์ ํจ์์ฌ์ผ ํ๋ฏ๋ก ๋น๋๊ธฐ ์์ ์ ์ง์ ์คํํ๋ฉด ์ ๋จ.
- Reducer ๋ด๋ถ์์ ๋คํธ์ํฌ ์์ฒญ์ ์ํํ๋ฉด ์ํ ๋ณ๊ฒฝ์ด ์์ธก ๋ถ๊ฐ๋ฅํด์ง๊ณ , ํ ์คํธ๊ฐ ์ด๋ ค์์ง.
- ๋ฐ๋ผ์ ๋น๋๊ธฐ ์์ ์ Effect์์ ๋ถ๋ฆฌํ์ฌ ์คํ ํด์ผ ํจ.
โ ์ฌ๋ฐ๋ฅธ ๋ฐฉ์: Effect๋ฅผ ์ฌ์ฉํ์ฌ ๋คํธ์ํฌ ์์ฒญ ์คํ
case .fetchRandomNumber:
return .run { send in
let randomValue = Int.random(in: 1...100) // ๋๋ค ์ซ์ ์์ฑ
await send(.randomNumberResponse(randomValue)) // ๊ฒฐ๊ณผ๋ฅผ Reducer์ ์ ๋ฌ
}
- Effect๋ ๋น๋๊ธฐ ์์ ์ ์คํํ ํ, ๊ฒฐ๊ณผ๋ฅผ Action์ผ๋ก Reducer์ ์ ๋ฌ ํจ.
- await send(.randomNumberResponse(value)) ์ ํตํด ์ํ ๋ณ๊ฒฝ์ Reducer์์ ์ฒ๋ฆฌํ๋๋ก ํจ.
DependencyValues๋? (ํ๊ฒฝ ์์กด์ฑ ์ฃผ์ )
์ต์ TCA์์ Environment๊ฐ ์ฌ๋ผ์ง๊ณ DependencyValues๋ก ๋ณ๊ฒฝ๋จ
๊ธฐ์กด TCA์์๋ Environment๋ฅผ ์ฌ์ฉํ์ฌ ๋คํธ์ํฌ ํด๋ผ์ด์ธํธ, ํ์ด๋จธ, ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ฑ์ ์ธ๋ถ ์์กด์ฑ์ ์ฃผ์ ํ์ง๋ง,
์ต์ ๋ฒ์ ์์๋ DependencyValues๋ฅผ ์ฌ์ฉํ์ฌ ์์กด์ฑ์ ๊ด๋ฆฌํฉ๋๋ค.
DependencyValues๋ฅผ ํ์ฉํ ๋คํธ์ํฌ ์์ฒญ ์์
(1) Action ์ ์
enum Action: Equatable {
case increment // ์ซ์ ์ฆ๊ฐ
case decrement // ์ซ์ ๊ฐ์
case fetchRandomNumber // ๋๋ค ์ซ์ ์์ฒญ
case randomNumberResponse(Int) // API์์ ๋ฐ์ ๋๋ค ์ซ์๋ฅผ ์ํ์ ๋ฐ์
}
- fetchRandomNumber: ๋๋ค ์ซ์๋ฅผ ๊ฐ์ ธ์ค๋ ์์ฒญ์ ์์
- randomNumberResponse(Int): ์๋ต์ ๋ฐ์ ํ ์ํ๋ฅผ ๋ณ๊ฒฝ
(2) DependencyValues์ ๋คํธ์ํฌ ํด๋ผ์ด์ธํธ ๋ฑ๋ก
struct RandomCounterClient {
var fetchRandomNumber: () async throws -> Int
}
extension RandomCounterClient {
// API ํต์
static let live = RandomCounterClient(fetchRandomNumber: {
let url = URL(string: "<http://www.randomnumberapi.com/api/v1.0/random?min=1&max=1000&count=1>")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode([Int].self, from: data).first ?? 0
})
}
private enum RandomCounterClientKey: DependencyKey {
static let liveValue = RandomCounterClient.live
}
- RandomNumberClient๋ฅผ ์ ์ํ์ฌ ๋คํธ์ํฌ ์์ฒญ์ ์ฒ๋ฆฌํ๋ fetch() ๋ฉ์๋๋ฅผ ํฌํจ
- DependencyKey๋ฅผ ์ฌ์ฉํ์ฌ liveValue์ ๊ธฐ๋ณธ ๋์์ ์ ์
- ์ดํ, DependencyValues์ randomNumberClient๋ฅผ ๋ฑ๋กํด์ผ ํจ
(3) DependencyValues ํ์ฅ (์์กด์ฑ ์ฃผ์ )
extension DependencyValues {
var randomNumberClient: RandomNumberClient {
get { self[RandomNumberClientKey.self] }
set { self[RandomNumberClientKey.self] = newValue }
}
}
- DependencyValues์ randomNumberClient๋ฅผ ์ถ๊ฐ
- ์ดํ @Dependency(\.randomNumberClient)๋ก ์์กด์ฑ์ ์ฃผ์ ํ์ฌ ์ฌ์ฉ ๊ฐ๋ฅ
(4) Reducer์์ Effect ์คํ
struct CounterFeature: Reducer {
@Dependency(\.randomCounterClient) var client // Dependency ์ฃผ์
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .increment:
state.count += 1
return .none
case .decrement:
state.count -= 1
return .none
case .fetchRandomNumber:
return .run { send in
do {
let number = try await client.fetchRandomNumber()
await send(.randomNumberResponse(number)) // ์๋ต์ Reducer๋ก ์ ๋ฌ
} catch {
print("๋๋ค ๋๋ฒ ๊ฐ์ ธ์ค๊ธฐ ์คํจ: \\(error)")
}
}
case .randomNumberResponse(let number):
state.count = number
return .none
}
}
}
}
- @Dependency(\.randomNumberClient)๋ฅผ ์ฌ์ฉํ์ฌ randomNumberClient.fetch()๋ฅผ ํธ์ถ
- ๋คํธ์ํฌ ์์ฒญ์ด ๋๋๋ฉด .randomNumberResponse(value) ์ก์ ์ ๋ฐ์์์ผ ์ํ๋ฅผ ๋ณ๊ฒฝ
๐ฅ ์ ์ฒด ์ฝ๋
import SwiftUI
import ComposableArchitecture
// MARK: - RandomCounterClient ์ ์
struct RandomCounterClient {
var fetchRandomNumber: () async throws -> Int
}
// MARK: - ์ค์ ๊ตฌํ
extension RandomCounterClient {
static let live = RandomCounterClient(fetchRandomNumber: {
let url = URL(string: "<http://www.randomnumberapi.com/api/v1.0/random?min=1&max=1000&count=1>")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode([Int].self, from: data).first ?? 0
})
}
// MARK: - DependencyKey ๋ฑ๋ก
private enum RandomCounterClientKey: DependencyKey {
static let liveValue = RandomCounterClient.live
}
// MARK: - ependencyValues ํ์ฅ (์์กด์ฑ ์ฃผ์
)
extension DependencyValues {
var randomCounterClient: RandomCounterClient {
get { self[RandomCounterClientKey.self] }
set { self[RandomCounterClientKey.self] = newValue }
}
}
import SwiftUI
import ComposableArchitecture
// MARK: - Feature ์ ์ (Reducer)
struct RandomCounterFeature: Reducer {
struct State: Equatable {
var count = 0 // ํ์ฌ ์นด์ดํธ ๊ฐ(count)์ ์ ์ฅ
}
enum Action: Equatable {
case increment // ์ซ์ ์ฆ๊ฐ
case decrement // ์ซ์ ๊ฐ์
case fetchRandomNumber // ๋๋ค ์ซ์ ์์ฒญ
case randomNumberResponse(Int) // API์์ ๋ฐ์ ๋๋ค ์ซ์๋ฅผ ์ํ์ ๋ฐ์
}
@Dependency(\\.randomCounterClient) var client
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .increment:
state.count += 1
return .none
case .decrement:
state.count -= 1
return .none
case .fetchRandomNumber:
return .run { send in
do {
let number = try await client.fetchRandomNumber()
await send(.randomNumberResponse(number))
} catch {
print("๋๋ค ๋๋ฒ ๊ฐ์ ธ์ค๊ธฐ ์คํจ: \\(error)")
}
}
case .randomNumberResponse(let number):
state.count = number
return .none
}
}
}
}
import SwiftUI
import ComposableArchitecture
// MARK: - SwiftUI View
struct RandomCounterView: View {
let store: StoreOf<RandomCounterFeature>
var body: some View {
// WithViewStore๋ฅผ ์ฌ์ฉํ์ฌ store๋ฅผ ๊ด์ฐฐํ๊ณ viewStore.count๋ฅผ UI์ ๋ฐ์
WithViewStore(self.store, observe: { $0 }) { viewStore in
VStack(spacing: 20) {
Text("์นด์ดํธ: \\(viewStore.count)")
.font(.largeTitle)
HStack(spacing: 40) {
Button(action: { viewStore.send(.decrement) }) {
Text("–")
.font(.largeTitle)
.frame(width: 60, height: 60)
.background(Color.red.opacity(0.7))
.foregroundColor(.white)
.clipShape(Circle())
}
Button(action: { viewStore.send(.increment) }) {
Text("+")
.font(.largeTitle)
.frame(width: 60, height: 60)
.background(Color.green.opacity(0.7))
.foregroundColor(.white)
.clipShape(Circle())
}
}
Button(action: { viewStore.send(.fetchRandomNumber) }) {
Text("๋๋ค ์ซ์ ๊ฐ์ ธ์ค๊ธฐ")
.font(.title2)
.padding()
.background(Color.blue.opacity(0.7))
.foregroundColor(.white)
.clipShape(RoundedRectangle(cornerRadius: 10))
}
}
.padding()
}
}
}
import SwiftUI
import ComposableArchitecture
@main
struct TCAExampleApp: App {
var body: some Scene {
WindowGroup {
RandomCounterView(
store: Store(
initialState: RandomCounterFeature.State(), // ์ด๊ธฐ ์ํ
reducer: { RandomCounterFeature() } // Reducer ์ฃผ์
)
)
}
}
}
๐ ๊ฒฐ๋ก
- Effect๋ Reducer์์ ์คํํ ์ ์๋ ๋น๋๊ธฐ ์์
์ ์ฒ๋ฆฌํ๋ ์ญํ ์ ํ๋ค.
- .run {}์ ์ฌ์ฉํ์ฌ ๋น๋๊ธฐ ์์ ์คํ ํ send(action)์ผ๋ก Reducer์ ์๋ต์ ๋ณด๋.
- .fireAndForget {}์ ์ฌ์ฉํ๋ฉด ๊ฒฐ๊ณผ๋ฅผ Reducer์ ์ ๋ฌํ์ง ์๊ณ ๋จ์ํ ์คํ๋ง ํจ.
- DependencyValues๋ ์ต์ TCA์์ Environment๋ฅผ ๋์ฒดํ๋ ์์กด์ฑ ๊ด๋ฆฌ ์์คํ
์ด๋ค.
- @Dependency(\.randomNumberClient)๋ฅผ ์ฌ์ฉํ์ฌ ๋คํธ์ํฌ ํด๋ผ์ด์ธํธ ๋ฑ ์ธ๋ถ ์์กด์ฑ์ ์ฃผ์ ํ ์ ์์.
- DependencyKey์ DependencyValues๋ฅผ ํ์ฉํ๋ฉด ํ ์คํธ์ฉ Mock ๋ฐ์ดํฐ๋ ์ฝ๊ฒ ์ค์ ๊ฐ๋ฅ.
โก TCA์์ ๋น๋๊ธฐ ์์ ์ ๊ด๋ฆฌํ๋ ค๋ฉด Effect๋ฅผ ์ฌ์ฉํด์ผ ํ๊ณ , ์ธ๋ถ ์์กด์ฑ์ DependencyValues๋ฅผ ํตํด ๊ด๋ฆฌํด์ผ ํ๋ค!
'iOS > TCA' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[TCA] Environment vs. DependencyValues (0) | 2024.07.09 |
---|---|
[TCA] Client ํ๊ณ ๊ฐ๊ธฐ (0) | 2024.07.05 |
[TCA] ์ต์ ๋ฒ์ TCA์ ๋์ ๊ณผ์ (์์ ํฌํจ) (0) | 2024.07.05 |
[TCA] Redux ํจํด ์ํฅ? ๋จ๋ฐฉํฅ? (0) | 2024.06.30 |