TCA๋ฅผ ๊ณต๋ถํ๋ค ๋ณด๋ SwiftUI ๋ฟ๋ง ์๋๋ผ UIKit์์๋ ์ฌ์ฉํ ์ ์๋ Dependency๋ฅผ ์ ๊ณตํ๊ธธ๋ ๋ฐ๋ก ๊ธ์ ์ ์ด์ผ ๊ฒ ๋๋ผ๊ตฌ์!
์์กด์ฑ์ ๊ด๋ฆฌํ๊ธฐ ์ํ Swinject, Niddle, Factory ๋ฑ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ์๋๋ฐ TCA์ Depdency๋ ํ๋ฅญํ๊ณ ์ฌ์ฉํ๊ธฐ ํธ๋ฆฌํ ์์กด์ฑ ๊ด๋ฆฌ๋ฅผ ์ ๊ณตํฉ๋๋ค.
๊ณต์ ๋ฌธ์์ ๊ณต์ ๋ฒ์ญ์ ์๋ ์ฌ์ดํธ์์ ํ์ธํ ์ ์์ด์
Chapter 5. Dependency | Built with Notion
5.1 TCA์ Dependency
axiomatic-fuschia-666.notion.site
๊ณต์ ๋ฌธ์๋ฅผ ๋ณด์๋ ค๋ฉด ์ด ์ฌ์ดํธ๋ก ์ด๋ํ์๋ฉด ๋ฉ๋๋ค.
Documentation
pointfreeco.github.io
TCA์์๋ DI(Dependency Injection)๋ฅผ ์ํ @Dependency ํ๋กํผํฐ ๋ํผ์ DependencyKey, DependencyValues๋ฅผ ์ ๊ณตํฉ๋๋ค. Environment์์ ์ฌ์ฉํ EnvironmentKey์ EnvironmentValues์ ๋์ผํ ํํ๋ฅผ ๊ฐ๊ณ ์๋ ๊ฑธ ์ ์ ์๋๋ฐ, Dependency ๋ํ ๊ฐ์ ๊ตฌํ ๋ฐฉ์์ผ๋ก dependency๋ฅผ ์ ์ญ์์ ์ฌ์ฉํ ์ ์๋๋ก ๋ง๋ค์ด ์ค๋๋ค.
์ฐ์ ์ ์ ์ญ์ผ๋ก ์ฌ์ฉํ ์์กด์ฑ์ ๋ง๋ค์ด ์ฃผ์ด์ผ ํด์!!
Dependency ์์ฑ
struct ProductClient {
typealias Products = [Product]
var fetch: @Sendable (_ offset: Int, _ limit: Int) async throws -> [Product]
}
๊ทธ ํ Environment์ ๋์ผํ๊ฒ DepdencyKey๋ฅผ ๋ฑ๋กํ๊ณ DependencyValues์ ๋ฑ๋กํด ์์กด์ฑ์ ์ ๊ทผํ๊ณ ์ฌ์ฉํ ์ ์๋๋ก ํด์ฃผ๋ฉด ๋ฉ๋๋ค.
ProductClient๋ remote ์๋ฒ๋ก ๋ถํฐ product ๋ชฉ๋ก์ ๊ฐ์ ธ์ต๋๋ค. ์ค์ api๋ฅผ requestํ๋ ์ฝ๋๋ DepdencyKey๋ฅผ ๋ฑ๋กํ ๋ ์์ฑํด ์ฃผ๋ฉด ๋ฉ๋๋ค.
DependencyKeh ๋ฑ๋ก
extension ProductClient: DependencyKey {
static let liveValue = Self { offset, limit in
let (data, _) = try await URLSession.shared.data(from: URL(string: "[api path ์๋ต]?offset=\(offset)&limit=\(limit)")!)
return try JSONDecoder().decode(Products.self, from: data)
}
static var previewValue = ProductClient { offset, limit in
let products = [
Product(id: 0, title: "๋งฅ๋ถ ์์ด 2022", price: 1000, description: "", images: [""], creationAt: "", updatedAt: "", category: Category(id: 0, name: "", image: "", creationAt: "", updatedAt: "")),
// ์๋ต..
]
return products
}
}
TCA์์๋ DependencyKey๋ฅผ ์ ๊ณตํด์ ์ ์ญ์ผ๋ก ์ฌ์ฉํ๊ธฐ ์ํ Depdency๋ฅผ ๊ด๋ฆฌํ ์ ์์ด์. ์ฌ๊ธฐ์ dependency overriding์ ์ ๊ณตํฉ๋๋ค.
static var liveValue: Value { get }
static var previewValue: Value { get }
static var testValue: Value { get }
TCA์ DependencyKey ๋ด๋ถ๋ฅผ ํ์ธํด ๋ณด๋ฉด liveValue, previewValue, testValue๋ฅผ ์ ๊ณตํ๊ณ ์์ด์. ๊ฐ๊ฐ ์ํฉ์ ๋ฐ๋ผ ์ ์ ํ dependency๋ฅผ ์ง์ ํ ํ ์ฃผ์ ํด ์ค ์ ์์ต๋๋ค.
์๋ฎฌ๋ ์ดํฐ๋ ์ค์ ๊ธฐ๊ธฐ์์๋ liveValue๋ฅผ ์ฌ์ฉํด dependency๋ฅผ ์ฃผ์ ํ๊ณ , preview์์๋ preview์์ UI๋ฅผ ํ์ธ ํ๊ธฐ ์ํ ์ฉ๋๋ก ์ฌ๋ฌ mock ๋ฐ์ดํฐ๋ ์ค์ api request ์ฝ๋ ๋ฑ์ ๋ฃ์ ์๋ ์์ด์
PointFree ๊ฐ์๋ฅผ ๋ณด๋ ์ค์ ๊ธฐ๊ธฐ์์ ๊ถํ ๋ฐ์์ค๋ ๊ฒ์ ๋ฌธ์ ๊ฐ ์์ง๋ง ํ๋ฆฌ๋ทฐ์์๋ ์ ์ ๋์ํ์ง ์์ ์ด๋ฅผ Dependency๋ฅผ ์ฌ์ฉํด์ ๊ฐ ์ํฉ์ ๋ง๊ฒ ๋์ํด ์ฃผ๊ณ ์์์ต๋๋ค.
์์กด์ฑ์ ์ฝํ๊ฒ ๋ง๋๋ ๊ฒ์ด ์ ์ค์ํ์ง๋ SwiftUI์ Preview๋ฅผ ์๊ฐํด๋ณด๋ฉด ์ฝ๊ฒ ์ดํดํ ์ ์์ต๋๋ค. SwiftUI๋ Canvas์ SwiftUI์ View๋ฅผ ๋น๋ํ๊ธฐ ์ ๋ฏธ๋ฆฌ ๋ณด์ฌ์ค๋๋ค. ์๋ฎฌ๋ ์ดํฐ์ ์ฑ์์ ์คํํ๋ ์ฝ๋๋ค๊ณผ ๋ฌ๋ฆฌ Preview๋ ํ์ฌ View์์ ์์ํฉ๋๋ค. View๊ฐ ์ธ๋ถ๋ก ๋ถํฐ ํ์๋ก ํ๋ ๊ฐ์ด ์๋ค๋ฉด Preview์์ initializer๋ dependency๋ฅผ ์ฃผ์ ํ๋ ๋ฐฉ์(for example: Environment)์ ์ฌ์ฉํด ์ฃผ๋ฉด ๋ฉ๋๋ค.
์ด๋ View๊ฐ ํ์๋ก ํ๋ ๊ฐ ์ค UserDefault์ ๊ฐ์ด ์๋ค๋ฉด ์ด๋ป๊ฒ ํด์ผ ํ ๊น์? Preview์์ ๊ธฐ๊ธฐ์ ์ ์ฅ๋ ๊ฐ์ด ์์๊น์? Preview ๋ฟ๋ง ์๋๋ผ Unit Test์ ํ์๋ก ํ๋ ๊ฐ์ ์ด๋ป๊ฒ ์ค์ ํด ์ฃผ์ด์ผ ํ ๊น์?
์ค์ ์๋ํ๋ ์ฑ ๋ฟ๋ง ์๋๋ผ Preview, Test ์ฝ๋ ๋ฑ์์ ์ด dependency๋ฅผ ์ฃผ์ ํด์ผ ํ๊ณ ์ด ์์กด์ฑ์ ์ฝํ๊ณ ์ ํํ๊ฒ ๋ง๋๋ ๊ฒ์ด ์ค์ํฉ๋๋ค.
DepdencyValues
extension DependencyValues {
var productClient: ProductClient {
get { self[ProductClient.self] }
set { self[ProductClient.self] = newValue }
}
}
@Depdency ํ๋กํผํฐ ๋ํผ๋ฅผ ์ฌ์ฉํด์ ์์กด์ฑ์ ์ ๊ทผํ๊ณ ์ฌ์ฉํ๊ธฐ ์ํด get, set์ ํตํด ์ฝ๋๋ฅผ ์์ฑํด ์ฃผ๋ฉด ๋ฉ๋๋ค. ์ด๋ก์ ๋ชจ๋ ์ค๋น๊ฐ ๋๋ฌ์ผ๋ ์ค์ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ์ ํ์ธํด ๋ด ์๋ค.
MainViewModel
final class MainViewModel: ObservableObject {
var offset = 0
var limit = 10
@Published var products: [Product] = []
@Dependency(\.productClient) var productClient
@MainActor
func fetchProduct() async {
self.products = try! await productClient.fetch(offset, limit)
}
}
MainViewModel์ productClient๋ผ๋ depdency๋ฅผ ๊ฐ์ง๊ณ ์๊ณ fetchProduct๊ฐ ์คํ๋๋ฉด ์ด depdency๋ก ๋ถํฐ ๋ฐ์ดํฐ๋ฅผ fetch ํด ์ต๋๋ค.
MainViewModel์ด ์์ฑ๋ ๋ ์ด dependency ๋ฅผ ์ฃผ์ ํด ์ค ์ ์์ด์
OnboardingView
struct OnboardingView: View {
@ObservedObject var viewModel: OnboardingViewModel
var body: some View {
ZStack {
Button {
viewModel.startButtonTapped()
} label: {
Text("์์ํ๊ธฐ")
}
.buttonStyle(.borderedProminent)
if viewModel.isShowMain {
AMainView(viewModel: withDependencies {
$0.productClient = .liveValue
} operation: {
MainViewModel()
})
}
}
}
}
AMainView๋ MainViewModel์ ํ์๋ก ํฉ๋๋ค. viewModel์ ์์ฑํ ๋ dependency์๋ productClient๋ฅผ ์ฃผ์ ํด ์ค์ผ ํฉ๋๋ค. ์ฝ๋๋ฅผ ์์ธํ ์ดํด ๋ณด๋ฉด
AMainView(viewModel: withDependencies {
$0.productClient = .liveValue
} operation: {
MainViewModel()
})
withDependecies๋ฅผ ์ฌ์ฉํด์ depdency๋ฅผ ์ง์ ํด ์ฃผ๊ณ ์์ด์.
static var liveValue: Value { get }
static var previewValue: Value { get }
static var testValue: Value { get }
์ด๋ liveValue๋ ๋ฐ๋ก ์์์ DepdencyKey๋ฅผ ๊ตฌํํด ์ค ๋ ์ ์ด ์ฃผ์๋ ๊ฐ์ ๋๋ค. liveValue๋ ๋ฌด์กฐ๊ฑด ์ ์ด ์ฃผ์ด์ผ ํ๋ ๊ฐ์ด๊ณ ์ค์ ๋์์ ์ฌ์ฉํ dependency๋ฅผ ์ ์ํด ๋๊ธฐ ๋๋ฌธ์ Dependency ์ฃผ์ ์ liveValue๋ฅผ ์ฌ์ฉํ์ต๋๋ค.
๊ทธ๋ ๋ค๋ฉด preview๋ ์ด๋ป๊ฒ ํด์ผ ํ ๊น์? ์ฑ์ ์ค์ ์คํํ ๋๋ liveValue๊ฐ ํ์ํ์ง๋ง preview์์๋ ์ค์ ๋ฐ์ดํฐ๊ฐ ํ์ํ์ง ์์ ์ ์์ด์. ์๋ฅผ ๋ค์ด ๋ก๊ทธ์ธ ๋ค์ ์กฐํ ํ ์ ์๋ api๋ผ๋ฉด ํ์ฌ ํ๋ฉด ๋ถํฐ ๋ณด์ฌ์ฃผ๋ preview์ ๊ฒฝ์ฐ ์ค์ api๋ก ๋ถํฐ ๊ฐ์ ๋ฐ์ ์ค๊ธฐ ํ๋ค ์๋ ์์ต๋๋ค.
#Preview {
AMainView(viewModel: withDependencies {
$0.productClient = .previewValue
} operation: {
MainViewModel()
})
}
preview๋ withDependencies๋ฅผ ์ฌ์ฉํด์ Dependency์ ๊ฐ์ ์ฃผ์ ํด ์ฃผ๋ฉด ๋ฉ๋๋ค. ์ด๋๋ .previewValue๋ฅผ ์ฌ์ฉํด์ mock ๋ฐ์ดํฐ๋ฅผ ๋ณด์ฌ์ฃผ๋ฉด ๋ฉ๋๋ค.
์ผ์ชฝ์ Preview ์ด๊ณ , ์ค๋ฅธ์ชฝ ์ฌ์ง์ ์ค์ ์๋ฎฌ๋ ์ดํฐ์์ ๋๋ ธ์ ๋ ๋์ค๋ ํ๋ฉด์ ๋๋ค. (์๋ ๋๊ฐ api ์ ์๋ ๊ฐ ๋ฆฌ์ ํด๋จ๋๋ด... ์๋ 10๊ฐ ๋ฐ์์์ผ ํ๋๋ฐ ๋ฐ์ดํฐ๊ฐ ์ง์ง ํ๊ฐ๋ฐ์ ์๋ค.. ๋๊ฐ ๊ฑด๋๋ ค์... ใ ใ ใ )
TCA๋ SwiftUI ์ํคํ ์ฒ๋ก ์ฌ์ฉ๋๋๋ฐ, DI๋ฅผ ์ํ Dependency๋ UIKit์์๋ ์ ์ฉ ๊ฐ๋ฅํฉ๋๋ค!
์ฐธ๊ณ ์ฌ์ดํธ ๋ฐ ๋์
https://github.com/pointfreeco/isowords/tree/main
https://axiomatic-fuschia-666.notion.site/Chapter-5-Dependency-de90da4e19554625af3ffc005ab13ed9
'iOS ๐ > Architecture' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[TCA] TCA์์์ ๋น๋๊ธฐ ์ฒ๋ฆฌ (0) | 2023.12.21 |
---|---|
[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] Effect ๊ตฌํ๊ณผ ํ์ฉ (0) | 2023.11.29 |