title: Registering dependencies description: @DependencyEntry와 DependencyKey로 사용자 정의 의존성을 등록하고 key path 및 간접 키 타입으로 접근 범위를 설계하는 방법을 설명합니다.

Registering dependencies

사용자 정의 의존성을 라이브러리에 등록해 코드베이스 어디에서나 바로 사용할 수 있게 하는 방법을 알아봐요.

원문: Registering dependencies

개요

라이브러리는 제어 가능한 의존성을 기본으로 많이 제공하지만, Dependency 프로퍼티 래퍼로 사용하기 위해 직접 만든 의존성을 등록해야 할 때도 있어요. 이를 구현하는 방법은 몇 가지가 있고, SwiftUI의 environment에 값을 등록하는 방식과 상당히 비슷해요.

@DependencyEntry 매크로

의존성을 등록하는 가장 간단한 방법은 @DependencyEntry 매크로를 사용하는 것이에요. DependencyValues 타입을 확장하고 기본값이 있는 mutable property에 매크로를 적용하세요.

import Dependencies
import DependenciesMacros

extension DependencyValues {
  @DependencyEntry
  var apiClient: any APIClient = MockAPIClient()
}

그러면 TestDependencyKey 프로토콜을 준수하고 MockAPIClientTestDependencyKey/testValue로 제공하는 private 내부 타입이 생성돼요.

DependencyKey/liveValueTestDependencyKey/testValue와 같은 모듈에 정의해도 괜찮다면 liveValue 인자로 제공할 수 있어요.

import Dependencies
import DependenciesMacros

extension DependencyValues {
  @DependencyEntry(liveValue: LiveAPIClient())
  var apiClient: any APIClient = MockAPIClient()
}

하지만 의존성의 live 구현을 앱의 entry point에서만 정의하는 것이 적절하거나 live 구현과 의존성 interface를 분리해야 한다면 이 인자를 제공하지 않아요. 대신 interface와 구현 분리하기의 기법을 사용하세요.

DependencyKey 직접 준수하기

TestDependencyKeyDependencyKey를 직접 준수할 수도 있어요. 먼저 DependencyKey 프로토콜 준수를 만들어요. 최소한 구현해야 할 것은 DependencyKey/liveValue예요. simulator나 기기에서 앱을 실행할 때 사용하는 값이므로 외부 서버에 실제로 네트워크 요청을 보내는 구현이 적합해요. 일반적으로 의존성 타입이 이 프로토콜을 직접 준수하게 만드는 것이 편리해요.

extension APIClient: DependencyKey {
  static let liveValue = APIClient(/*
    Construct the "live" API client that actually makes network
    requests and communicates with the outside world.
  */)
}

팁: 의존성에 제공할 수 있는 값은 두 가지 더 있어요. DependencyKey/testValue를 구현하면 테스트에서 기능을 실행할 때 사용하고, previewValue를 구현하면 Xcode Preview에서 기능을 실행할 때 사용해요. 처음 시작할 때는 이 값을 신경 쓰지 않아도 되고 나중에 추가할 수 있어요. 자세한 내용은 Live, preview, and test dependencies를 참고하세요.

이제 코드베이스 어디에서나 API client 의존성에 바로 접근할 수 있어요.

@Observable
final class TodosModel {
  @ObservationIgnored
  @Dependency(APIClient.self) var apiClient
  // ...
}

Preview, simulator, 기기에서는 live 의존성을 자동으로 사용하고, 테스트에서는 mock 데이터를 반환하도록 의존성을 재정의할 수 있어요.

@MainActor
@Test
func fetchUser() async {
  let model = withDependencies {
    $0[APIClient.self].fetchTodos = { _ in Todo(id: 1, title: "Get milk") }
  } operation: {
    TodosModel()
  }

  await store.loadButtonTapped()
  #expect(
    model.todos == [Todo(id: 1, title: "Get milk")]
  )
}

고급 기법

의존성 key path

특정 key path에 의존성 값을 등록하는 단계를 하나 더 거칠 수 있어요. DependencyValues를 프로퍼티로 확장하세요.

extension DependencyValues {
  var apiClient: APIClient {
    get { self[APIClientKey.self] }
    set { self[APIClientKey.self] = newValue }
  }
}

그러면 SwiftUI environment value와 비슷하게 자동 완성에서 찾을 수 있는 프로퍼티로 의존성에 접근하고 재정의할 수 있어요.

-@Dependency(APIClient.self) var apiClient
+@Dependency(\.apiClient) var apiClient

 let model = withDependencies {
-  $0[APIClient.self].fetchTodos = { _ in Todo(id: 1, title: "Get milk") }
+  $0.apiClient.fetchTodos = { _ in Todo(id: 1, title: "Get milk") }
 } operation: {
   TodosModel()
 }

이 스타일의 또 다른 장점은 @Dependency의 범위를 특정 하위 프로퍼티로 좁힐 수 있다는 것이에요.

// This feature only needs to access the API client's logged-in user
@Dependency(\.apiClient.currentUser) var currentUser

간접 의존성 키 준수

소유하지 않은 타입처럼 의존성 타입이 DependencyKey 프로토콜을 직접 준수하게 만드는 것이 적절하지 않을 때도 있어요. 이럴 때는 DependencyKey를 준수하는 별도의 타입을 정의할 수 있어요.

enum UserDefaultsKey: DependencyKey {
  static let liveValue = UserDefaults.standard
}

그런 다음 값의 타입 대신 이 키 타입을 통해 의존성에 접근하고 재정의할 수 있어요.

@Dependency(UserDefaultsKey.self) var userDefaults

let model = withDependencies {
  let defaults = UserDefaults(suiteName: "test-defaults")
  defaults.removePersistentDomain(forName: "test-defaults")
  $0[UserDefaultsKey.self] = defaults
} operation: {
  TodosModel()
}

전용 key path로 dependency value를 확장하면 이 키를 private으로 만들 수도 있어요.

-enum UserDefaultsKey: DependencyKey { /* ... */ }
+private enum UserDefaultsKey: DependencyKey { /* ... */ }
+
+extension DependencyValues {
+  var userDefaults: UserDefaults {
+    get { self[UserDefaultsKey.self] }
+    set { self[UserDefaultsKey.self] = newValue }
+  }
+}