Skip to content

TCA 통합 가이드

WeaveDI를 The Composable Architecture (TCA)와 통합하는 완벽한 가이드입니다. 이 가이드는 의존성 주입 패턴, 상태 관리, 그리고 확장 가능한 TCA 앱을 구축하기 위한 고급 기법을 다룹니다.

개요

The Composable Architecture (TCA)는 일관되고 이해하기 쉬운 방식으로 애플리케이션을 구축하기 위한 라이브러리로, 컴포지션, 테스팅, 인체공학을 염두에 두고 설계되었습니다. WeaveDI는 TCA의 아키텍처와 완벽하게 작동하는 강력한 의존성 주입 기능을 제공합니다.

왜 WeaveDI + TCA인가?

측면TCA만 사용WeaveDI + TCA장점
의존성 관리init에서 수동 주입@Injected로 자동 주입🎯 더 깔끔한 코드, 보일러플레이트 감소
테스팅서비스 수동 모킹자동 모킹 주입🧪 더 쉬운 단위 테스팅
모듈화강한 결합프로토콜을 통한 느슨한 결합🔗 더 나은 관심사 분리
Swift Concurrency기본 지원완전한 async/await + actor 최적화⚡ 향상된 성능
환경 관리제한된 범위다중 범위 의존성 관리🌍 유연한 환경 처리

Swift 버전 호환성

Swift 6.0+ (권장)

  • 완전한 strict concurrency 지원
  • 리듀서의 actor 격리
  • Sendable 준수 검증
  • 향상된 성능 최적화

Swift 5.9+

  • 완전한 async/await 지원
  • 프로퍼티 래퍼 통합
  • 성능 모니터링

Swift 5.8+

  • 핵심 의존성 주입
  • 기본 TCA 통합
  • 제한적인 동시성 기능

설치

Package.swift 구성

swift
// Package.swift
dependencies: [
    .package(url: "https://github.com/pointfreeco/swift-composable-architecture", from: "1.5.0"),
    .package(url: "https://github.com/Roy-wonji/WeaveDI.git", from: "3.1.0")
],
targets: [
    .target(
        name: "YourApp",
        dependencies: [
            .product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
            "WeaveDI"
        ]
    )
]

Xcode 통합

  1. File → Add Package Dependencies를 통해 두 패키지 모두 추가
  2. Swift 파일에서 두 프레임워크 모두 import:
swift
import ComposableArchitecture
import WeaveDI

Component 빠른 시작

swift
import WeaveDI

@Component
struct UserComponent {
    @Provide var repository: UserRepository = UserRepositoryImpl()
    @Provide(scope: .singleton) var service: UserService = UserServiceImpl(repository: repository)
}

// 공유 컨테이너에 등록
UserComponent.registerAll()

@Component 매크로는 구조체 내부의 @Provide 속성을 분석해 의존성 순서를 정렬하고 DIContainer 등록 코드를 컴파일 타임에 생성합니다.

기본 통합 패턴

1. 서비스 레이어 설정

먼저 서비스를 정의하고 WeaveDI에 등록합니다:

swift
// MARK: - 서비스 프로토콜
protocol UserService {
    func fetchUser(id: String) async throws -> User
    func updateUser(_ user: User) async throws -> User
}

protocol AnalyticsService {
    func track(event: String, parameters: [String: Any])
    func setUserId(_ userId: String)
}

protocol NetworkService {
    func request<T: Codable>(_ endpoint: Endpoint) async throws -> T
}

// MARK: - 서비스 구현
class UserServiceImpl: UserService {
    @Injected private var networkService: NetworkService?
    @Injected private var analytics: AnalyticsService?

    func fetchUser(id: String) async throws -> User {
        analytics?.track(event: "user_fetch_started", parameters: ["user_id": id])

        guard let network = networkService else {
            throw ServiceError.networkServiceUnavailable
        }

        let user: User = try await network.request(.user(id: id))
        analytics?.track(event: "user_fetch_completed", parameters: ["user_id": id])

        return user
    }

    func updateUser(_ user: User) async throws -> User {
        analytics?.track(event: "user_update_started", parameters: ["user_id": user.id])

        guard let network = networkService else {
            throw ServiceError.networkServiceUnavailable
        }

        let updatedUser: User = try await network.request(.updateUser(user))
        analytics?.track(event: "user_update_completed", parameters: ["user_id": user.id])

        return updatedUser
    }
}

// MARK: - 의존성 등록
extension WeaveDI {
    static func registerTCADependencies() async {
        await WeaveDI.Container.bootstrap { container in
            // 핵심 서비스
            container.register(NetworkService.self) {
                URLSessionNetworkService()
            }

            container.register(AnalyticsService.self) {
                FirebaseAnalyticsService()
            }

            // 비즈니스 서비스
            container.register(UserService.self) {
                UserServiceImpl()
            }
        }
    }
}

2. 의존성 주입을 사용하는 리듀서

Swift 6 패턴 (권장)

swift
import ComposableArchitecture
import WeaveDI

@Reducer
struct UserFeature {
    @ObservableState
    struct State: Equatable, Sendable {
        var user: User?
        var isLoading = false
        var errorMessage: String?
    }

    enum Action: Sendable {
        case loadUser(String)
        case userLoaded(User)
        case userLoadFailed(String)
        case updateUser(User)
        case userUpdated(User)
    }

    // WeaveDI를 사용한 의존성 주입
    @Injected private var userService: UserService?
    @Injected private var analytics: AnalyticsService?

    var body: some ReducerOf<Self> {
        Reduce { state, action in
            switch action {
            case .loadUser(let userId):
                state.isLoading = true
                state.errorMessage = nil

                return .run { send in
                    do {
                        guard let service = userService else {
                            throw ServiceError.userServiceUnavailable
                        }

                        let user = try await service.fetchUser(id: userId)
                        await send(.userLoaded(user))
                    } catch {
                        await send(.userLoadFailed(error.localizedDescription))
                    }
                }

            case .userLoaded(let user):
                state.isLoading = false
                state.user = user
                analytics?.track(event: "user_loaded_in_view", parameters: ["user_id": user.id])
                return .none

            case .userLoadFailed(let message):
                state.isLoading = false
                state.errorMessage = message
                analytics?.track(event: "user_load_failed", parameters: ["error": message])
                return .none

            case .updateUser(let user):
                return .run { send in
                    do {
                        guard let service = userService else {
                            throw ServiceError.userServiceUnavailable
                        }

                        let updatedUser = try await service.updateUser(user)
                        await send(.userUpdated(updatedUser))
                    } catch {
                        await send(.userLoadFailed(error.localizedDescription))
                    }
                }

            case .userUpdated(let user):
                state.user = user
                analytics?.track(event: "user_updated", parameters: ["user_id": user.id])
                return .none
            }
        }
    }
}

Swift 5.9 패턴 (호환)

swift
import ComposableArchitecture
import WeaveDI

struct UserFeature: Reducer {
    struct State: Equatable {
        var user: User?
        var isLoading = false
        var errorMessage: String?
    }

    enum Action: Equatable {
        case loadUser(String)
        case userLoaded(User)
        case userLoadFailed(String)
        case updateUser(User)
        case userUpdated(User)
    }

    // WeaveDI를 사용한 의존성 주입
    @Injected private var userService: UserService?
    @Injected private var analytics: AnalyticsService?

    func reduce(into state: inout State, action: Action) -> Effect<Action> {
        switch action {
        case .loadUser(let userId):
            state.isLoading = true
            state.errorMessage = nil

            return .run { send in
                do {
                    guard let service = userService else {
                        throw ServiceError.userServiceUnavailable
                    }

                    let user = try await service.fetchUser(id: userId)
                    await send(.userLoaded(user))
                } catch {
                    await send(.userLoadFailed(error.localizedDescription))
                }
            }

        case .userLoaded(let user):
            state.isLoading = false
            state.user = user
            analytics?.track(event: "user_loaded_in_view", parameters: ["user_id": user.id])
            return .none

        case .userLoadFailed(let message):
            state.isLoading = false
            state.errorMessage = message
            analytics?.track(event: "user_load_failed", parameters: ["error": message])
            return .none

        case .updateUser(let user):
            return .run { send in
                do {
                    guard let service = userService else {
                        throw ServiceError.userServiceUnavailable
                    }

                    let updatedUser = try await service.updateUser(user)
                    await send(.userUpdated(updatedUser))
                } catch {
                    await send(.userLoadFailed(error.localizedDescription))
                }
            }

        case .userUpdated(let user):
            state.user = user
            analytics?.track(event: "user_updated", parameters: ["user_id": user.id])
            return .none
        }
    }
}

3. SwiftUI 뷰 통합

swift
import SwiftUI
import ComposableArchitecture
import WeaveDI

struct UserProfileView: View {
    let store: StoreOf<UserFeature>

    var body: some View {
        WithViewStore(store, observe: { $0 }) { viewStore in
            NavigationView {
                VStack(spacing: 20) {
                    if viewStore.isLoading {
                        ProgressView("사용자 로딩 중...")
                            .frame(maxWidth: .infinity, maxHeight: .infinity)
                    } else if let user = viewStore.user {
                        UserDetailView(user: user) { updatedUser in
                            viewStore.send(.updateUser(updatedUser))
                        }
                    } else if let error = viewStore.errorMessage {
                        ErrorView(message: error) {
                            // 재시도 로직이 여기에 들어갑니다
                        }
                    } else {
                        EmptyView()
                    }
                }
                .navigationTitle("사용자 프로필")
                .onAppear {
                    // 뷰가 나타날 때 사용자 로드
                    viewStore.send(.loadUser("current-user-id"))
                }
            }
        }
    }
}

struct UserDetailView: View {
    let user: User
    let onUpdate: (User) -> Void

    var body: some View {
        Form {
            Section("사용자 정보") {
                HStack {
                    AsyncImage(url: user.avatarURL) { image in
                        image
                            .resizable()
                            .aspectRatio(contentMode: .fill)
                    } placeholder: {
                        Circle()
                            .fill(Color.gray.opacity(0.3))
                    }
                    .frame(width: 60, height: 60)
                    .clipShape(Circle())

                    VStack(alignment: .leading) {
                        Text(user.name)
                            .font(.headline)
                        Text(user.email)
                            .font(.subheadline)
                            .foregroundColor(.secondary)
                    }

                    Spacer()
                }
            }

            Section("작업") {
                Button("프로필 수정") {
                    // 프로필 수정 작업
                    var updatedUser = user
                    updatedUser.name = "업데이트된 이름"
                    onUpdate(updatedUser)
                }
            }
        }
    }
}

고급 패턴

1. 멀티 모듈 아키텍처

대형 애플리케이션의 경우 기능을 모듈로 조직화합니다:

swift
// MARK: - 피처 모듈 프로토콜
protocol FeatureModule {
    static func registerDependencies() async
}

// MARK: - 사용자 피처 모듈
struct UserFeatureModule: FeatureModule {
    static func registerDependencies() async {
        await WeaveDI.Container.bootstrap { container in
            // 사용자 전용 서비스
            container.register(UserRepository.self) {
                CoreDataUserRepository()
            }

            container.register(UserService.self) {
                UserServiceImpl()
            }
        }
    }
}

// MARK: - 주문 피처 모듈
struct OrderFeatureModule: FeatureModule {
    static func registerDependencies() async {
        await WeaveDI.Container.bootstrap { container in
            container.register(OrderRepository.self) {
                APIOrderRepository()
            }

            container.register(OrderService.self) {
                OrderServiceImpl()
            }

            container.register(PaymentService.self) {
                StripePaymentService()
            }
        }
    }
}

// MARK: - 앱 모듈 등록
extension App {
    static func registerAllFeatures() async {
        await UserFeatureModule.registerDependencies()
        await OrderFeatureModule.registerDependencies()
        // 다른 피처 모듈들 추가...
    }
}

2. 환경 기반 의존성 구성

swift
import ComposableArchitecture
import WeaveDI

// MARK: - 환경 구성
enum AppEnvironment {
    case development
    case staging
    case production

    var apiBaseURL: String {
        switch self {
        case .development: return "https://dev-api.example.com"
        case .staging: return "https://staging-api.example.com"
        case .production: return "https://api.example.com"
        }
    }
}

// MARK: - 환경별 등록
extension WeaveDI {
    static func registerForEnvironment(_ environment: AppEnvironment) async {
        await WeaveDI.Container.bootstrap { container in
            switch environment {
            case .development:
                // 개발 서비스
                container.register(NetworkService.self) {
                    MockNetworkService() // 개발에서는 모킹 사용
                }
                container.register(AnalyticsService.self) {
                    ConsoleAnalyticsService() // 콘솔에만 로그
                }

            case .staging:
                // 스테이징 서비스
                container.register(NetworkService.self) {
                    URLSessionNetworkService(baseURL: environment.apiBaseURL)
                }
                container.register(AnalyticsService.self) {
                    TestAnalyticsService() // 테스트 애널리틱스
                }

            case .production:
                // 프로덕션 서비스
                container.register(NetworkService.self) {
                    URLSessionNetworkService(baseURL: environment.apiBaseURL)
                }
                container.register(AnalyticsService.self) {
                    FirebaseAnalyticsService() // 완전한 애널리틱스
                }
            }

            // 모든 환경에 공통인 서비스
            container.register(UserService.self) {
                UserServiceImpl()
            }
        }
    }
}

3. WeaveDI + TCA를 사용한 테스팅

테스트용 모킹 서비스

swift
import XCTest
import ComposableArchitecture
import WeaveDI
@testable import YourApp

// MARK: - 모킹 서비스
class MockUserService: UserService {
    var mockUsers: [String: User] = [:]
    var shouldThrowError = false
    var errorToThrow: Error = ServiceError.unknown

    func fetchUser(id: String) async throws -> User {
        if shouldThrowError {
            throw errorToThrow
        }

        return mockUsers[id] ?? User.mockUser(id: id)
    }

    func updateUser(_ user: User) async throws -> User {
        if shouldThrowError {
            throw errorToThrow
        }

        mockUsers[user.id] = user
        return user
    }
}

class MockAnalyticsService: AnalyticsService {
    var trackedEvents: [(event: String, parameters: [String: Any])] = []
    var currentUserId: String?

    func track(event: String, parameters: [String: Any]) {
        trackedEvents.append((event: event, parameters: parameters))
    }

    func setUserId(_ userId: String) {
        currentUserId = userId
    }
}

// MARK: - 테스트 케이스
class UserFeatureTests: XCTestCase {
    var mockUserService: MockUserService!
    var mockAnalytics: MockAnalyticsService!

    override func setUp() async throws {
        await super.setUp()

        // 각 테스트마다 WeaveDI 컨테이너 리셋
        WeaveDI.Container.live = WeaveDI.Container()

        // 모킹 서비스 생성 및 등록
        mockUserService = MockUserService()
        mockAnalytics = MockAnalyticsService()

        await WeaveDI.Container.bootstrap { container in
            container.register(UserService.self, instance: mockUserService)
            container.register(AnalyticsService.self, instance: mockAnalytics)
        }
    }

    @MainActor
    func testLoadUserSuccess() async {
        // Given
        let expectedUser = User(id: "test-123", name: "테스트 사용자", email: "test@example.com")
        mockUserService.mockUsers["test-123"] = expectedUser

        let store = TestStore(initialState: UserFeature.State()) {
            UserFeature()
        }

        // When
        await store.send(.loadUser("test-123")) {
            $0.isLoading = true
            $0.errorMessage = nil
        }

        // Then
        await store.receive(.userLoaded(expectedUser)) {
            $0.isLoading = false
            $0.user = expectedUser
        }

        // 애널리틱스 검증
        XCTAssertEqual(mockAnalytics.trackedEvents.count, 2) // fetch_started + loaded_in_view
        XCTAssertEqual(mockAnalytics.trackedEvents.last?.event, "user_loaded_in_view")
    }

    @MainActor
    func testLoadUserFailure() async {
        // Given
        mockUserService.shouldThrowError = true
        mockUserService.errorToThrow = ServiceError.networkError("연결 실패")

        let store = TestStore(initialState: UserFeature.State()) {
            UserFeature()
        }

        // When
        await store.send(.loadUser("test-123")) {
            $0.isLoading = true
            $0.errorMessage = nil
        }

        // Then
        await store.receive(.userLoadFailed("연결 실패")) {
            $0.isLoading = false
            $0.errorMessage = "연결 실패"
        }

        // 애널리틱스 검증
        XCTAssertEqual(mockAnalytics.trackedEvents.last?.event, "user_load_failed")
    }

    @MainActor
    func testUpdateUser() async {
        // Given
        let initialUser = User(id: "test-123", name: "테스트 사용자", email: "test@example.com")
        let updatedUser = User(id: "test-123", name: "업데이트된 사용자", email: "test@example.com")

        let store = TestStore(initialState: UserFeature.State(user: initialUser)) {
            UserFeature()
        }

        // When
        await store.send(.updateUser(updatedUser))

        // Then
        await store.receive(.userUpdated(updatedUser)) {
            $0.user = updatedUser
        }

        // 모킹 서비스에서 사용자가 실제로 업데이트되었는지 검증
        XCTAssertEqual(mockUserService.mockUsers["test-123"]?.name, "업데이트된 사용자")

        // 애널리틱스 검증
        let updateEvents = mockAnalytics.trackedEvents.filter { $0.event.contains("user_update") }
        XCTAssertEqual(updateEvents.count, 2) // started + completed
    }
}

4. 복잡한 상태 관리

복잡한 상태 요구사항을 가진 앱의 경우:

swift
@Reducer
struct AppFeature {
    @ObservableState
    struct State: Equatable {
        var user: UserFeature.State = .init()
        var orders: OrderFeature.State = .init()
        var settings: SettingsFeature.State = .init()
        var isAuthenticated = false
    }

    enum Action {
        case user(UserFeature.Action)
        case orders(OrderFeature.Action)
        case settings(SettingsFeature.Action)
        case authenticate
        case logout
    }

    @Injected private var authService: AuthService?
    @Injected private var analytics: AnalyticsService?

    var body: some ReducerOf<Self> {
        Scope(state: \.user, action: \.user) {
            UserFeature()
        }

        Scope(state: \.orders, action: \.orders) {
            OrderFeature()
        }

        Scope(state: \.settings, action: \.settings) {
            SettingsFeature()
        }

        Reduce { state, action in
            switch action {
            case .authenticate:
                return .run { send in
                    // 인증 로직
                    guard let auth = authService else { return }

                    do {
                        let isAuthenticated = try await auth.authenticate()
                        if isAuthenticated {
                            analytics?.track(event: "user_authenticated", parameters: [:])
                        }
                    } catch {
                        analytics?.track(event: "authentication_failed", parameters: ["error": error.localizedDescription])
                    }
                }

            case .logout:
                state.isAuthenticated = false
                analytics?.track(event: "user_logged_out", parameters: [:])
                return .none

            case .user, .orders, .settings:
                return .none
            }
        }
    }
}

성능 최적화

1. 런타임 최적화

더 나은 성능을 위해 WeaveDI의 런타임 최적화를 활성화하세요:

swift
// App.swift나 AppDelegate에서
@main
struct MyApp: App {
    init() {
        Task {
            // 의존성을 등록하기 전에 최적화 활성화
            UnifiedRegistry.shared.enableOptimization()

            // 모든 의존성 등록
            await WeaveDI.registerTCADependencies()
        }
    }

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

2. 지연 로딩 패턴

더 나은 성능을 위해 비용이 많이 드는 의존성에는 지연 로딩을 사용하세요:

swift
@Reducer
struct DataProcessingFeature {
    // 지연 주입 - 처음 접근할 때만 생성됩니다
    @Factory private var dataProcessor: ExpensiveDataProcessor
    @Injected private var cache: CacheService?

    var body: some ReducerOf<Self> {
        Reduce { state, action in
            switch action {
            case .processLargeDataSet(let data):
                return .run { send in
                    // dataProcessor는 필요할 때만 생성됩니다
                    let processor = dataProcessor
                    let result = await processor.process(data)
                    await send(.dataProcessed(result))
                }

            default:
                return .none
            }
        }
    }
}

모범 사례

1. 의존성 조직화

swift
// 관련된 의존성을 그룹화
protocol UserDependencies {
    var userService: UserService { get }
    var userRepository: UserRepository { get }
    var userCache: UserCacheService { get }
}

class UserDependenciesImpl: UserDependencies {
    @Injected var userService: UserService
    @Injected var userRepository: UserRepository
    @Injected var userCache: UserCacheService
}

@Reducer
struct UserFeature {
    @Injected private var dependencies: UserDependencies?

    // 그룹화된 의존성 사용
    private var userService: UserService? {
        dependencies?.userService
    }
}

2. 에러 처리

swift
enum ReducerError: LocalizedError {
    case dependencyNotFound(String)
    case serviceUnavailable(String)

    var errorDescription: String? {
        switch self {
        case .dependencyNotFound(let dependency):
            return "필수 의존성 '\(dependency)'를 찾을 수 없습니다"
        case .serviceUnavailable(let service):
            return "서비스 '\(service)'를 현재 사용할 수 없습니다"
        }
    }
}

@Reducer
struct SafeUserFeature {
    @SafeInject private var userService: UserService?

    var body: some ReducerOf<Self> {
        Reduce { state, action in
            switch action {
            case .loadUser(let id):
                return .run { send in
                    do {
                        let service = try userService.getValue() // nil이면 throw
                        let user = try await service.fetchUser(id: id)
                        await send(.userLoaded(user))
                    } catch {
                        await send(.userLoadFailed(error.localizedDescription))
                    }
                }
            }
        }
    }
}

3. 모듈식 테스팅

swift
// 테스트 전용 모듈 생성
struct TestUserModule: FeatureModule {
    static func registerDependencies() async {
        await WeaveDI.Container.bootstrap { container in
            // 테스트용 모킹 서비스 등록
            container.register(UserService.self) {
                MockUserService()
            }

            container.register(NetworkService.self) {
                MockNetworkService()
            }
        }
    }
}

// 테스트에서 사용
class UserFeatureIntegrationTests: XCTestCase {
    override func setUp() async throws {
        await TestUserModule.registerDependencies()
    }
}

마이그레이션 가이드

TCA Dependencies에서 WeaveDI로

현재 TCA의 의존성 시스템을 사용하고 있다면, 다음과 같이 마이그레이션할 수 있습니다:

이전 (TCA Dependencies)

swift
struct UserFeature: Reducer {
    @Dependency(\.userService) var userService
    @Dependency(\.analytics) var analytics

    // ... 리듀서 구현
}

extension DependencyValues {
    var userService: UserService {
        get { self[UserServiceKey.self] }
        set { self[UserServiceKey.self] = newValue }
    }
}

이후 (WeaveDI)

swift
struct UserFeature: Reducer {
    @Injected private var userService: UserService?
    @Injected private var analytics: AnalyticsService?

    // ... 동일한 리듀서 구현
}

// 앱 시작 시 한 번만 등록
await WeaveDI.registerTCADependencies()

일반적인 문제와 해결책

문제 1: 서비스를 찾을 수 없음

문제: @Injectednil을 반환합니다

해결책: 스토어를 만들기 전에 의존성이 등록되었는지 확인하세요:

swift
// ❌ 잘못됨 - 등록 전에 스토어 생성
let store = Store(initialState: UserFeature.State()) {
    UserFeature()
}
await WeaveDI.registerTCADependencies()

// ✅ 올바름 - 의존성을 먼저 등록
await WeaveDI.registerTCADependencies()
let store = Store(initialState: UserFeature.State()) {
    UserFeature()
}

문제 2: Swift 6 Sendable 에러

문제: 주입된 서비스와 관련된 Sendable 준수 오류

해결책: 모든 서비스가 Sendable을 준수하도록 하세요:

swift
// ✅ 서비스를 Sendable로 만들기
protocol UserService: Sendable {
    func fetchUser(id: String) async throws -> User
}

class UserServiceImpl: UserService, Sendable {
    // 구현...
}

문제 3: Factory의 메모리 문제

문제: @Factory가 너무 많은 인스턴스를 생성합니다

해결책: 상태가 없는 서비스는 @Injected를 사용하고, @Factory는 필요할 때만 사용하세요:

swift
// ✅ 싱글톤 서비스는 @Injected 사용
@Injected private var apiClient: APIClient?

// ✅ 상태가 있거나 임시 객체는 @Factory 사용
@Factory private var documentGenerator: DocumentGenerator

결론

WeaveDI와 TCA는 함께 매우 잘 작동하여, 유지보수 가능하고 테스트 가능하며 성능이 뛰어난 iOS 애플리케이션을 구축하기 위한 견고한 아키텍처를 제공합니다. 이 조합은 다음을 제공합니다:

  • 깔끔한 아키텍처: 비즈니스 로직과 의존성 관리 간의 명확한 분리
  • 타입 안전성: 의존성의 컴파일 타임 검증
  • 쉬운 테스팅: 자동 모킹 주입과 격리된 테스트 환경
  • 성능: 최소한의 오버헤드로 최적화된 의존성 해결
  • Swift 6 준비: 현대적인 Swift 동시성 기능과의 완전한 호환성

이 가이드의 패턴과 모범 사례를 따르면, 강력한 의존성 주입 기능을 가진 확장 가능한 TCA 애플리케이션을 구축할 수 있습니다.

다음 단계

Swift-Dependencies 연동

WeaveDI는 Point-Free의 swift-dependencies 패키지와 완벽하게 통합되어, 두 시스템을 함께 사용하거나 점진적으로 마이그레이션할 수 있습니다.

설정 및 구성

Package.swift에 두 패키지 모두 추가:

swift
dependencies: [
    .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.0.0"),
    .package(url: "https://github.com/Roy-wonji/WeaveDI.git", from: "3.2.0")
],
targets: [
    .target(
        name: "YourApp",
        dependencies: [
            .product(name: "Dependencies", package: "swift-dependencies"),
            "WeaveDI"
        ]
    )
]

실제 사용 패턴

1. 구체적 타입 직접 주입 (사용자 패턴)

swift
import WeaveDI

class ExchangeFeature: Reducer {
    // 실제 사용 중인 패턴 - 구체적 타입 직접 주입
    @Injected(ExchangeUseCaseImpl.self) private var injectedExchangeUseCase
    @Injected(FavoriteCurrencyUseCaseImpl.self) private var injectedFavoriteUseCase
    @Injected(ExchangeRateCacheUseCaseImpl.self) private var injectedCacheUseCase

    var body: some ReducerOf<Self> {
        Reduce { state, action in
            switch action {
            case .loadExchangeRates:
                return .run { send in
                    // 직접 타입 사용으로 타입 안전성 보장
                    guard let exchangeUseCase = injectedExchangeUseCase else { return }

                    let rates = try await exchangeUseCase.fetchRates()
                    await send(.exchangeRatesLoaded(rates))
                }
            }
        }
    }
}

2. Protocol 기반 주입 (권장 패턴)

swift
// 프로토콜 정의
protocol ExchangeUseCase: Sendable {
    func fetchRates() async throws -> [ExchangeRate]
}

protocol FavoriteCurrencyUseCase: Sendable {
    func getFavorites() async -> [Currency]
    func addFavorite(_ currency: Currency) async
}

// InjectedKey 정의
struct ExchangeUseCaseKey: InjectedKey {
    static let liveValue: ExchangeUseCase = ExchangeUseCaseImpl()
    static let testValue: ExchangeUseCase = MockExchangeUseCase()
}

struct FavoriteUseCaseKey: InjectedKey {
    static let liveValue: FavoriteCurrencyUseCase = FavoriteCurrencyUseCaseImpl()
    static let testValue: FavoriteCurrencyUseCase = MockFavoriteUseCase()
}

// 사용 방법
class CurrencyFeature: Reducer {
    @Injected(\.exchangeUseCase) var exchangeUseCase
    @Injected(\.favoriteUseCase) var favoriteUseCase

    // 또는 타입으로 직접 주입
    @Injected(ExchangeUseCaseKey.self) var directExchangeUseCase
}

// InjectedValues 확장
extension InjectedValues {
    var exchangeUseCase: ExchangeUseCase {
        get { self[ExchangeUseCaseKey.self] }
        set { self[ExchangeUseCaseKey.self] = newValue }
    }

    var favoriteUseCase: FavoriteCurrencyUseCase {
        get { self[FavoriteUseCaseKey.self] }
        set { self[FavoriteUseCaseKey.self] = newValue }
    }
}

Swift-Dependencies와 함께 사용

Component 빠른 시작

swift
import WeaveDI

@Component
struct UserComponent {
    @Provide var repository: UserRepository = UserRepositoryImpl()
    @Provide(scope: .singleton) var service: UserService = UserServiceImpl(repository: repository)
}

// 공유 컨테이너에 등록
UserComponent.registerAll()

@Component 매크로는 구조체 내부의 @Provide 속성을 분석해 의존성 순서를 정렬하고 DIContainer 등록 코드를 컴파일 타임에 생성합니다.

기본 통합 패턴

swift
import Dependencies
import WeaveDI

// 1. 서비스 프로토콜 정의
protocol UserService: Sendable {
    func fetchUser(id: String) async throws -> User
}

// 2. WeaveDI 키 생성
struct UserServiceKey: InjectedKey {
    static let liveValue: UserService = UserServiceImpl()
    static let testValue: UserService = MockUserService()
}

// 3. DependencyValues 확장으로 브리지 생성
extension DependencyValues {
    var userService: UserService {
        get { self[UserServiceKey.self] }
        set { self[UserServiceKey.self] = newValue }
    }
}

// 4. 리듀서에서 두 시스템 모두 사용 가능
struct UserFeature: Reducer {
    // 옵션 A: swift-dependencies 사용 (새 프로젝트 권장)
    @Dependency(\.userService) var userService

    // 옵션 B: WeaveDI 직접 사용 (기존 코드와 같은 패턴)
    // @Injected(UserServiceImpl.self) private var injectedUserService

    var body: some ReducerOf<Self> {
        Reduce { state, action in
            switch action {
            case .loadUser(let id):
                return .run { send in
                    let user = try await userService.fetchUser(id: id)
                    await send(.userLoaded(user))
                }
            }
        }
    }
}

실제 환전 앱 마이그레이션 예시

현재 패턴에서 개선된 패턴으로

swift
// MARK: - 현재 사용 중인 패턴
class CurrentExchangeFeature: Reducer {
    @Injected(ExchangeUseCaseImpl.self) private var injectedExchangeUseCase
    @Injected(FavoriteCurrencyUseCaseImpl.self) private var injectedFavoriteUseCase
    @Injected(ExchangeRateCacheUseCaseImpl.self) private var injectedCacheUseCase
}

// MARK: - 개선된 하이브리드 패턴
class ImprovedExchangeFeature: Reducer {
    // 기존 패턴 유지 (안전성을 위해)
    @Injected(ExchangeUseCaseImpl.self) private var exchangeUseCase
    @Injected(FavoriteCurrencyUseCaseImpl.self) private var favoriteUseCase
    @Injected(ExchangeRateCacheUseCaseImpl.self) private var cacheUseCase

    // 새로운 서비스들은 프로토콜 기반으로
    @Injected(\.analyticsService) var analytics
    @Injected(\.networkMonitor) var networkMonitor

    var body: some ReducerOf<Self> {
        Reduce { state, action in
            switch action {
            case .loadExchangeRates(let baseCurrency):
                return .run { send in
                    // 기존 서비스 사용
                    guard let useCase = exchangeUseCase,
                          let cache = cacheUseCase else { return }

                    // 네트워크 상태 확인 (새 서비스)
                    guard networkMonitor?.isConnected == true else {
                        // 캐시에서 가져오기
                        let cachedRates = try await cache.getCachedRates(for: baseCurrency)
                        await send(.exchangeRatesLoaded(cachedRates))
                        return
                    }

                    // 실시간 환율 가져오기
                    let rates = try await useCase.fetchRates(baseCurrency: baseCurrency)

                    // 캐시에 저장
                    try await cache.cacheRates(rates, for: baseCurrency)

                    // 분석 이벤트 전송
                    analytics?.track(event: "exchange_rates_loaded",
                                   parameters: ["base_currency": baseCurrency.code])

                    await send(.exchangeRatesLoaded(rates))
                }
            }
        }
    }
}

UseCase 등록 및 설정

swift
// MARK: - UseCase 등록
extension WeaveDI {
    static func registerExchangeUseCases() async {
        await WeaveDI.Container.bootstrap { container in
            // 구체적 타입 등록 (현재 패턴)
            container.register(ExchangeUseCaseImpl.self) {
                ExchangeUseCaseImpl(
                    apiClient: container.resolve(APIClient.self)!,
                    database: container.resolve(Database.self)!
                )
            }

            container.register(FavoriteCurrencyUseCaseImpl.self) {
                FavoriteCurrencyUseCaseImpl(
                    userDefaults: UserDefaults.standard
                )
            }

            container.register(ExchangeRateCacheUseCaseImpl.self) {
                ExchangeRateCacheUseCaseImpl(
                    cache: container.resolve(CacheService.self)!
                )
            }

            // 프로토콜 기반 등록 (개선된 패턴)
            container.register(AnalyticsService.self) {
                FirebaseAnalyticsService()
            }

            container.register(NetworkMonitorProtocol.self) {
                SystemNetworkMonitor()
            }
        }
    }
}

// MARK: - 앱 시작 시 등록
@main
struct ExchangeApp: App {
    init() {
        Task {
            await WeaveDI.registerExchangeUseCases()
        }
    }

    var body: some Scene {
        WindowGroup {
            ExchangeRootView()
        }
    }
}

테스트에서의 활용

swift
import XCTest
import Dependencies
import WeaveDI
@testable import ExchangeApp

class ExchangeFeatureTests: XCTestCase {

    override func setUp() async throws {
        await super.setUp()

        // WeaveDI 컨테이너 초기화
        WeaveDI.Container.live = WeaveDI.Container()

        // 모킹된 UseCase들 등록
        await WeaveDI.Container.bootstrap { container in
            container.register(ExchangeUseCaseImpl.self) {
                MockExchangeUseCaseImpl()
            }
            container.register(FavoriteCurrencyUseCaseImpl.self) {
                MockFavoriteCurrencyUseCaseImpl()
            }
            container.register(ExchangeRateCacheUseCaseImpl.self) {
                MockCacheUseCaseImpl()
            }
        }
    }

    @MainActor
    func testLoadExchangeRates() async {
        // Given
        let store = TestStore(initialState: ExchangeFeature.State()) {
            ExchangeFeature()
        } withDependencies: {
            // swift-dependencies 모킹
            $0.analyticsService = MockAnalyticsService()
            $0.networkMonitor = MockNetworkMonitor(isConnected: true)
        }

        // When
        await store.send(.loadExchangeRates(.usd))

        // Then
        await store.receive(.exchangeRatesLoaded(expectedRates)) {
            $0.exchangeRates = expectedRates
            $0.isLoading = false
        }
    }
}

// MARK: - Mock 구현들
class MockExchangeUseCaseImpl: ExchangeUseCaseImpl {
    override func fetchRates(baseCurrency: Currency) async throws -> [ExchangeRate] {
        return [
            ExchangeRate(from: .usd, to: .krw, rate: 1350.0),
            ExchangeRate(from: .usd, to: .jpy, rate: 149.0)
        ]
    }
}

성능 비교 및 최적화

측면구체적 타입 주입프로토콜 기반swift-dependencies
타입 안전성매우 강함강함강함
해결 속도매우 빠름빠름빠름
테스트 용이성보통매우 좋음매우 좋음
유연성낮음높음높음
코드 결합도높음낮음낮음

권장 사용 패턴

1. 기존 코드 유지 + 점진적 개선

swift
// 현재 안정적으로 동작하는 코드는 그대로 유지
@Injected(ExchangeUseCaseImpl.self) private var exchangeUseCase
@Injected(FavoriteCurrencyUseCaseImpl.self) private var favoriteUseCase

// 새로운 기능은 프로토콜 기반으로
@Injected(\.pushNotificationService) var pushService
@Injected(\.userPreferences) var preferences

2. 새 프로젝트 권장 패턴

swift
// 모든 의존성을 프로토콜 기반으로
@Injected(\.exchangeUseCase) var exchangeUseCase
@Injected(\.favoriteUseCase) var favoriteUseCase
@Injected(\.cacheUseCase) var cacheUseCase

자주 묻는 질문

Q: 현재 사용 중인 구체적 타입 주입 방식이 잘못된 건가요?

A: 아닙니다! 구체적 타입 주입은 완전히 유효한 패턴이며, 특히 타입 안전성이 중요한 상황에서 매우 효과적입니다. 다만 프로토콜 기반 접근법이 테스트와 유연성 면에서 더 유리할 수 있습니다.

Q: 기존 코드를 바꿔야 하나요?

A: 기존 코드가 잘 동작한다면 바꿀 필요가 없습니다. 새로운 기능을 추가할 때 점진적으로 프로토콜 기반 패턴을 도입하시면 됩니다.

Q: 두 패턴을 함께 사용해도 되나요?

A: 네! 완전히 안전하며 실제로 권장되는 접근법입니다.

결론

WeaveDI와 swift-dependencies의 통합을 통해:

  • 점진적 마이그레이션: 자신만의 속도로 이전
  • 성능 최적화: WeaveDI로 무거운 작업 처리
  • 최대 호환성: 기존 코드 유지
  • 향상된 테스트: 통합된 모킹 관리

이런 하이브리드 접근법은 특히 기존 코드가 안정적으로 동작하는 상황에서 위험을 최소화하면서 개선할 수 있는 방법입니다.

MIT 라이선스 하에 릴리스됨.