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://pointfreeco.github.io/swift-dependencies/main/documentation/dependencies/overridingdependencies/

https://pointfreeco.github.io/swift-dependencies/main/documentation/dependencies/designingdependencies/

https://pointfreeco.github.io/swift-dependencies/main/documentation/dependencies/overridingdependencies/#Scoping-dependencies

https://www.pointfree.co/collections/tours/composable-architecture-1-0/ep248-tour-of-the-composable-architecture-1-0-dependencies

https://github.com/pointfreeco/isowords/blob/isowords-deploy-v117/Sources/SettingsFeature/Settings.swift

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