J
[SwiftUI] SwiftUI + TCA: Effect? DependencyValues? (์์ ํฌํจ) ๋ณธ๋ฌธ
[SwiftUI] SwiftUI + TCA: Effect? DependencyValues? (์์ ํฌํจ)
yujaehui 2024. 7. 8. 19:54๐ง ์์ํ๊ธฐ ์ ์…
์ด์ ๊ธ์์ ๋ฒํผ์ ๋๋ฌ ์นด์ดํธ๋ฅผ ์ฆ๊ฐ ๋๋ ๊ฐ์์ํฌ ์ ์๋ ์์ ๋ฅผ ํตํด ์ต์ ๋ฒ์ TCA์ ๋ํด ๊ฐ๋จํ ์ค๋ช ํ๋๋ฐ์.
์ด๋ฒ ๊ธ์์๋ ์กฐ๊ธ ๋ ์ฌํ์ ์ผ๋ก Effect์ DependencyValues, ๊ทธ๋ฆฌ๊ณ ๋คํธ์ํฌ ํต์ ์ ์งํํ ๊ฒฝ์ฐ๋ฅผ ์์๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
๐ TCA์ Effect์ DependencyValues ์ค๋ช
TCA์์๋ ๋น๋๊ธฐ ์์ (๋คํธ์ํฌ ์์ฒญ, ํ์ด๋จธ, ๋ฐ์ดํฐ ์ ์ฅ ๋ฑ)์ Reducer ๋ด๋ถ์์ ์ง์ ์คํํ ์ ์๊ณ , Effect๋ฅผ ์ฌ์ฉํ์ฌ ์ฒ๋ฆฌ ํด์ผ ํฉ๋๋ค.
๋ํ, ์ต์ TCA์์๋ Environment ๋์ DependencyValues๋ฅผ ์ฌ์ฉํ์ฌ ์์กด์ฑ์ ์ฃผ์ (Dependency Injection, DI) ๋ฐ ๊ด๋ฆฌํฉ๋๋ค.
๐ 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๋ฅผ ํตํด ๊ด๋ฆฌํด์ผ ํ๋ค!
๐ง ๋ง์น๋ฉฐ…
์ด๋ฒ ๊ธ์์๋ API ํต์ ์ ํฌํจํ ์์ ๋ฅผ ๋ค๋ฃจ๋ฉด์ TCA์ Effect์ DependencyValues๋ฅผ ์ค๋ช ํ๋๋ฐ์.
์ฌ์ค ๋ค๋ฅธ ๋ธ๋ก๊ทธ ๊ธ์์๋ Effect์ ๊ด๋ จํ์ฌ Environment๊ฐ ๋ง์ด ๋ฑ์ฅํด์ ์ ๋ํ ์ฒ์ ํ์ต์ ์งํํ ๋ ์ ๋ฅผ ๋ง์ด ๋จน์๋๋ฐ์.
๋ค์ ๊ธ์์ Environment์ DependencyValuse์ ์ฐจ์ด์ ๋ํด ์ค๋ช ํ ์์ ์ด๋ ๋ด์ฃผ์๋ฉด ์ข์ ๊ฒ ๊ฐ์์!
'SwiftUI' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[SwiftUI] TCA ๊ณต์๋ฌธ์๋ก ์์ํ๊ธฐ_Getting started (0) | 2024.08.20 |
---|---|
[SwiftUI] SwiftUI + TCA: Environment vs. DependencyValues (0) | 2024.07.09 |
[SwiftUI] SwiftUI + TCA: Client ํ๊ณ ๊ฐ๊ธฐโฆ (0) | 2024.07.05 |
[SwiftUI] SwiftUI + TCA: ์ต์ ๋ฒ์ TCA์ ๋์ ๊ณผ์ (์์ ํฌํจ) (0) | 2024.07.05 |
[SwiftUI] SwiftUI + TCA : Redux ํจํด ์ํฅ? ๋จ๋ฐฉํฅ? (0) | 2024.06.30 |