Dependency?
Dependency.. 의존성.. iOS 개발을 하면서 수없이 들어봤을 것이다. 간략하게 이야기 해보자면, 의존성이란 어떤 객체가 자신의 역할을 수행하기 위해 다른 객체에 의존하는 관계를 이야기한다. TCA에서 Dependency를 어떻게 관리하는지 이야기 하기 위해서는 의존성에 대한 기초 개념을 알고 있어야하니 간단하게 알아보자.
class UserViewModel {
let network = NetworkManager()
}
예를 들어, 위 코드와 같이 네트워크를 요청하는 ViewModel은 NetworkManager 없이는 동작할 수 없다. 이 경우 UserViewModel은 NetworkManager에 의존하고 있다.
iOS 개발에서 오랫동안 의존성은 보통 아래와 같은 방식으로 관리되어 왔다.
1. 내부에서 직접 생성
class UserViewModel {
let network = NetworkManager()
}
2. 싱글톤 사용
class UserViewModel {
let network = NetworkManager.shared
}
3. static 인스턴스 접근
class UserViewModel {
let network = Dependencies.network
}
이러한 방식들은 구현이 간단하지만, 객체 간 결합도가 높아지고 테스트가 어렵다는 한계가 있다.
그래서 등장한 DI(Dependency Injection)
위와 같은 문제를 해결하기 위해 iOS 개발에서도 의존성 주입이 사용되기 시작했다.
class UserViewModel {
let network: NetworkProtocol
init(network: NetworkProtocol) {
self.network = network
}
}
위 코드처럼 의존성을 외부에서 주입하고, protocol로 추상화하며 테스트에서는 mock을 사용한다. 이 방식은 분명 이전보다 나아졌지만, 규모가 커질수록 protocol 수가 급격히 늘어난다는 점과 구조를 파악하기 어렵다는 한계가 있다.
TCA에서의 Dependency
그럼 TCA는 Dependency를 어떻게 다룰까?
class UserViewModel {
private let network: NetworkService
init(network: NetworkService) {
self.network = network
}
}
let viewModel = UserViewModel(network: NetworkManager())
기존 iOS 개발에서 의존성은 보통 객체 생성 시점에 주입해야 하는 것으로 다뤄졌다. 위 코드처럼 init 파라미터나 프로퍼티를 통해 외부에서 주입했었다. 즉, 의존성은 객체가 가지고 있는 것이었다.
하지만 TCA에서는 조금 다르다. TCA에서는 생성 시점이 아닌 사용 시점에 의존성을 가져온다.
struct UserFeature: Reducer {
@Dependency(\.networkClient) var networkClient
}
Reducer는 생성자를 통해 의존성을 주입받지 않는다. 대신, 실행되는 순간 필요한 의존성을 @Dependency 매크로를 통해 꺼내 사용한다. 즉, Reducer는 의존성을 가지지 않는다.
TCA에서의 의존성 관리
TCA의 모든 의존성은 DependencyKey를 통해 정의된다.
struct NetworkClient {
var request: () async throws -> Data
}
extension NetworkClient: DependencyKey {
static let liveValue: Self = {
return Self(
request: {
// 네트워크 요청
}
)
}()
}
정의한 의존성을 DependencyValues에 등록하면 된다.
extension DependencyValues {
var networkClient: NetworkClient {
get { self[NetworkClient.self] }
set { self[NetworkClient.self] }
}
}
이렇게 등록된 의존성을 Reducer에서 @Dependency를 통해 접근하여 사용한다.
@Dependency(\.networkClient) var networkClient
이러한 구조 덕분에 Reducer는 의존성의 구현을 전혀 알 필요가 없다.
Live / Test / Preview
Documentation
pointfreeco.github.io
TCA 의존성 관리의 가장 큰 특징은 환경에 따라 의존성을 쉽게 교체할 수 있다는 점이다.

1. liveValue : 실제 앱에서 사용되는 구현
2. testValue : 테스트를 위한 구현
3. previewValue : SwiftUI Preview용 구현
extension NetworkClient: DependencyKey {
static let liveValue: Self = {
return Self(
fetchUser: { id in
try await Task.sleep(nanoseconds: 500_000_000)
return "Live User \(id)"
}
)
}()
static let testValue: Self = {
return Self(
fetchUser: { id in
return "Test User \(id)"
}
)
}()
static let previewValue = NetworkClient: Self = {
return Self(
fetchUser: { _ in
return "Preview User"
}
)
}()
}
Test에서 의존성 교체
let store = TestStore(
initialState: UserFeature.State(),
reducer: UserFeature()
) {
$0.networkClient.fetchUser = { _ in
"Mock User"
}
}
TestStore를 생성하면 TCA는 테스트 환경임을 인지하고 모든 Dependency를 testValue 기준으로 초기화 한다. 위 코드는 TestStore로 생성되었기 때문에, DependencyValue가 text context로 설정되어 해당 Reducer는 이미 testValue를 사용하고 있다.
TestStore에서 후행 클로저는 뭘까?

TestStore의 생성자를 살펴보면 후행 클로저는 withDependencies이다. 간단하게 testValue로 초기화된 의존성을 추가로 커스터마이징하는 클로저라 생각하면 쉽다.
Preview에서 의존성 교체
#Preview {
UserView(
store: Store(
initialState: UserFeature.State(),
reducer: UserFeature()
)
)
}
마찬가지로 Preview에서는 자동으로 previewValue가 사용된다.
'iOS > TCA' 카테고리의 다른 글
| [TCA] 04. TCA의 네비게이션 방식에 대해 알아보자. (0) | 2026.01.19 |
|---|---|
| [TCA] 03. TCA의 바인딩 방식을 알아보자. (0) | 2026.01.13 |
| [TCA] 02. Store를 알아보자. (2) | 2026.01.13 |
| [TCA] 1. The Composable Architecture ? (0) | 2025.10.17 |