IT

TCA Dependency 사용 방법 (SwiftUI / Composable Architecture)

초코모찌롤 2026. 3. 15. 13:49
반응형

TCA Dependency 사용 방법 (SwiftUI / Composable Architecture)

TCA(The Composable Architecture)를 사용하다 보면 네트워크 요청, 데이터베이스 접근, 시간 처리, UUID 생성 같은 외부 기능을 어떻게 Feature에 연결해야 하는지 고민하게 된다.

이때 사용하는 것이 바로 Dependency이다.

Dependency는 Feature가 직접 외부 객체를 생성하거나 의존하지 않도록 하고, 필요한 기능을 의존성 주입 방식으로 전달하는 구조다.

이 글에서는 TCA에서 Dependency를 사용하는 방법과 실무에서 어떻게 구조를 설계하면 좋은지 정리한다.

Dependency란 무엇인가

Dependency는 Feature가 필요한 외부 기능을 직접 생성하지 않고 주입받는 구조를 의미한다.

예를 들어 다음과 같은 기능들이 Dependency 대상이 된다.

  • 네트워크 API 요청
  • 데이터베이스 접근
  • 파일 저장
  • UUID 생성
  • 시간 처리
  • Analytics 이벤트

이 기능들을 Feature 내부에서 직접 구현하면 테스트가 어려워지고 코드 결합도가 높아진다.

그래서 TCA에서는 Dependency 시스템을 제공한다.

TCA Dependency 기본 구조

Dependency는 보통 다음 단계로 구성된다.

  1. 클라이언트 정의
  2. DependencyKey 정의
  3. DependencyValues 확장
  4. Feature에서 사용

1. Dependency Client 만들기

먼저 외부 기능을 수행할 클라이언트를 정의한다.

struct TodoClient {
    var fetchTodos: () async throws -> [String]
}

이 구조는 실제 네트워크 호출이나 데이터 로딩을 수행하는 역할을 한다.

2. DependencyKey 정의

Dependency 시스템에 등록하기 위해 DependencyKey를 만든다.

import ComposableArchitecture

private enum TodoClientKey: DependencyKey {

    static let liveValue = TodoClient(
        fetchTodos: {
            return ["Buy Milk", "Write Blog", "Workout"]
        }
    )
}

여기서 liveValue는 실제 앱에서 사용하는 구현이다.

3. DependencyValues 확장

이제 DependencyValues에 접근할 수 있도록 확장한다.

extension DependencyValues {

    var todoClient: TodoClient {
        get { self[TodoClientKey.self] }
        set { self[TodoClientKey.self] = newValue }
    }
}

이렇게 하면 Feature에서 @Dependency를 통해 사용할 수 있다.

4. Feature에서 Dependency 사용

Feature에서는 다음과 같이 Dependency를 가져올 수 있다.

@Dependency(\.todoClient) var todoClient

이제 reducer에서 클라이언트를 사용할 수 있다.

@Reducer
struct TodoFeature {

    @Dependency(\.todoClient) var todoClient

    @ObservableState
    struct State: Equatable {
        var todos: [String] = []
    }

    enum Action {
        case onAppear
        case todosLoaded([String])
    }

    var body: some ReducerOf<Self> {

        Reduce { state, action in

            switch action {

            case .onAppear:

                return .run { send in
                    let todos = try await todoClient.fetchTodos()
                    await send(.todosLoaded(todos))
                }

            case let .todosLoaded(todos):
                state.todos = todos
                return .none
            }
        }
    }
}

이 구조의 핵심은 Feature가 TodoClient를 직접 생성하지 않는다는 점이다. Dependency 시스템을 통해 필요한 기능만 가져온다.

Dependency를 사용하는 장점

  • Feature가 외부 구현에 의존하지 않는다
  • 테스트가 쉬워진다
  • 모듈 분리가 쉬워진다
  • 코드 재사용성이 높아진다

특히 테스트 환경에서 Dependency의 장점이 크게 드러난다.

테스트에서 Dependency 변경하기

테스트에서는 실제 네트워크 대신 Mock 데이터를 사용할 수 있다.

let store = TestStore(
    initialState: TodoFeature.State()
) {
    TodoFeature()
} withDependencies: {
    $0.todoClient.fetchTodos = {
        return ["Test Todo"]
    }
}

이렇게 하면 실제 네트워크 호출 없이 테스트가 가능하다.

실무에서 자주 사용하는 Dependency

실제 프로젝트에서는 다음 Dependency가 많이 사용된다.

  • API Client
  • Database Client
  • Analytics Client
  • Location Client
  • Notification Client
  • Authentication Client

각 기능을 Dependency로 분리하면 Feature가 훨씬 단순해진다.

Dependency 구조 예시

대규모 프로젝트에서는 보통 다음 구조로 관리한다.

Core
 ├── NetworkClient
 ├── DatabaseClient
 ├── AnalyticsClient
 └── DependencyKeys

Feature
 ├── HomeFeature
 ├── RecordFeature
 └── SettingsFeature

Feature는 Dependency 인터페이스만 사용하고, 실제 구현은 Core 계층에 두는 방식이다.

자주 하는 실수

Feature 안에서 직접 네트워크 객체 생성

Feature 내부에서 APIClient() 같은 객체를 직접 만들면 테스트와 모듈 분리가 어려워진다.

Dependency를 너무 세분화

모든 작은 기능을 Dependency로 만들면 오히려 구조가 복잡해질 수 있다. 보통 기능 단위(Client 단위)로 나누는 것이 좋다.

Feature가 Dependency 구현에 의존하는 경우

Feature는 인터페이스만 사용하고, 실제 구현은 다른 계층에서 관리하는 것이 좋다.

Dependency 사용 팁

  • 외부 시스템 접근은 Dependency로 분리한다.
  • Feature는 인터페이스만 사용한다.
  • 테스트에서는 Mock Dependency를 사용한다.
  • 네트워크, DB, Analytics는 대부분 Dependency로 분리한다.

정리

  • TCA Dependency는 외부 기능을 주입받는 구조이다.
  • DependencyKey와 DependencyValues를 통해 등록한다.
  • Feature에서는 @Dependency로 접근한다.
  • 이 구조는 테스트와 모듈화를 쉽게 만든다.

TCA (Composable Architecture) 관련 글

반응형