Notice
Recent Posts
Recent Comments
Link
ยซ   2025/03   ยป
์ผ ์›” ํ™” ์ˆ˜ ๋ชฉ ๊ธˆ ํ† 
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31
Archives
Today
Total
๊ด€๋ฆฌ ๋ฉ”๋‰ด

J

[SwiftUI] SwiftUI + TCA: Effect? DependencyValues? (์˜ˆ์ œ ํฌํ•จ) ๋ณธ๋ฌธ

SwiftUI

[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 ์ฃผ์ž…
                )
            )
        }
    }
}

๐Ÿš€ ๊ฒฐ๋ก 

  1. Effect๋Š” Reducer์—์„œ ์‹คํ–‰ํ•  ์ˆ˜ ์—†๋Š” ๋น„๋™๊ธฐ ์ž‘์—…์„ ์ฒ˜๋ฆฌํ•˜๋Š” ์—ญํ•  ์„ ํ•œ๋‹ค.
    • .run {}์„ ์‚ฌ์šฉํ•˜์—ฌ ๋น„๋™๊ธฐ ์ž‘์—… ์‹คํ–‰ ํ›„ send(action)์œผ๋กœ Reducer์— ์‘๋‹ต์„ ๋ณด๋ƒ„.
    • .fireAndForget {}์„ ์‚ฌ์šฉํ•˜๋ฉด ๊ฒฐ๊ณผ๋ฅผ Reducer์— ์ „๋‹ฌํ•˜์ง€ ์•Š๊ณ  ๋‹จ์ˆœํžˆ ์‹คํ–‰๋งŒ ํ•จ.
  2. DependencyValues๋Š” ์ตœ์‹  TCA์—์„œ Environment๋ฅผ ๋Œ€์ฒดํ•˜๋Š” ์˜์กด์„ฑ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ ์ด๋‹ค.
    • @Dependency(\\.randomNumberClient)๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋„คํŠธ์›Œํฌ ํด๋ผ์ด์–ธํŠธ ๋“ฑ ์™ธ๋ถ€ ์˜์กด์„ฑ์„ ์ฃผ์ž…ํ•  ์ˆ˜ ์žˆ์Œ.
    • DependencyKey์™€ DependencyValues๋ฅผ ํ™œ์šฉํ•˜๋ฉด ํ…Œ์ŠคํŠธ์šฉ Mock ๋ฐ์ดํ„ฐ๋„ ์‰ฝ๊ฒŒ ์„ค์ • ๊ฐ€๋Šฅ.

โžก TCA์—์„œ ๋น„๋™๊ธฐ ์ž‘์—…์„ ๊ด€๋ฆฌํ•˜๋ ค๋ฉด Effect๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๊ณ , ์™ธ๋ถ€ ์˜์กด์„ฑ์€ DependencyValues๋ฅผ ํ†ตํ•ด ๊ด€๋ฆฌํ•ด์•ผ ํ•œ๋‹ค!


๐Ÿง˜ ๋งˆ์น˜๋ฉฐ…

์ด๋ฒˆ ๊ธ€์—์„œ๋Š” API ํ†ต์‹ ์„ ํฌํ•จํ•œ ์˜ˆ์ œ๋ฅผ ๋‹ค๋ฃจ๋ฉด์„œ TCA์˜ Effect์™€ DependencyValues๋ฅผ ์„ค๋ช…ํ–ˆ๋Š”๋ฐ์š”.

์‚ฌ์‹ค ๋‹ค๋ฅธ ๋ธ”๋กœ๊ทธ ๊ธ€์—์„œ๋Š” Effect์™€ ๊ด€๋ จํ•˜์—ฌ Environment๊ฐ€ ๋งŽ์ด ๋“ฑ์žฅํ•ด์„œ ์ € ๋˜ํ•œ ์ฒ˜์Œ ํ•™์Šต์„ ์ง„ํ–‰ํ•  ๋•Œ ์• ๋ฅผ ๋งŽ์ด ๋จน์—ˆ๋Š”๋ฐ์š”.

๋‹ค์Œ ๊ธ€์—์„œ Environment์™€ DependencyValuse์˜ ์ฐจ์ด์— ๋Œ€ํ•ด ์„ค๋ช…ํ•  ์˜ˆ์ •์ด๋‹ˆ ๋ด์ฃผ์‹œ๋ฉด ์ข‹์„ ๊ฒƒ ๊ฐ™์•„์š”!