
안녕하세요 !
이번에는 TCA Dependency Client 주제로 포스팅 해보려 합니다
Client는 TCA에서 외부 의존성을 추상화하고 관리하는 역할을 해요
Client를 통해 네트워크 요청, 데이터베이스 작업, 또는 기타 외부 서비스와의 상호작용을 캡슐화하고 테스트 가능한 형태로 만들 수 있습니다
TCA에서 Client는 일반적으로 특정 기능이나 서비스를 추상화한 프로토콜이며 이를 통해 실제 구현과 테스트용 모의 구현을 쉽게 교체할 수 있어요!!
protocol APIClientProtocol {
func fetch() async throws -> [Item]
func send(_ item: Item) async throws
}
APIClientProtocol은 데이터를 가져오고 보내는 두 가지 기본 작업을 예시로 정의 했어요
struct APIClient: APIClientProtocol {
var fetch: () async throws -> [Item]
var send: (Item) async throws -> Void
static let live = Self(
fetch: {
let (data, _) = try await URLSession.shared.data(from: URL(string: "https://api.example.com/items")!)
return try JSONDecoder().decode([Item].self, from: data)
},
send: { item in
var request = URLRequest(url: URL(string: "https://api.example.com/items")!)
request.httpMethod = "POST"
request.httpBody = try JSONEncoder().encode(item)
let (_, response) = try await URLSession.shared.data(for: request)
guard (response as? HTTPURLResponse)?.statusCode == 200 else {
throw APIError.sendFailed
}
}
)
}
fetch와 send는 실제 네트워크 요청을 수행해요
Client를 TCA의 의존성에 등록할때
private enum APIClientKey: DependencyKey {
static let liveValue = APIClient.live
}
extension DependencyValues {
var apiClient: APIClient {
get { self[APIClientKey.self] }
set { self[APIClientKey.self] = newValue }
}
}
이제 @Dependency(.apiClient) 를 통해 어디서든 APIClient에 접근할 수 있어요~~!
struct Feature: Reducer {
struct State { /* ... */ }
enum Action {
case fetchItems
case itemsResponse(TaskResult<[Item]>)
// ...
}
@Dependency(\.apiClient) var apiClient
func reduce(into state: inout State, action: Action) -> Effect<Action> {
switch action {
case .fetchItems:
return .run { send in
await send(.itemsResponse(TaskResult { try await apiClient.fetch() }))
}
case let .itemsResponse(.success(items)):
state.items = items
return .none
case .itemsResponse(.failure):
// Handle error
return .none
// ...
}
}
}
.fetchItems 액션이 발생하면, apiClient.fetch()를 호출하여 아이템을 가져오고, 그 결과를 .itemsResponse 액션으로 전달하는 과정이에요
아래와 같이 Mock Client를 만들 수 있습니다
extension APIClient {
static let mock = Self(
fetch: { [Item(id: "1", name: "Test Item")] },
send: { _ in }
)
}
좀 더 복잡한 Mock Client 예제 입니다
extension APIClient {
static func dynamicMock(items: [Item], shouldFail: Bool = false) -> Self {
Self(
fetch: {
if shouldFail {
throw APIError.fetchFailed
}
return items
},
send: { item in
if shouldFail {
throw APIError.sendFailed
}
print("Item sent: \(item)")
}
)
}
}
TCA의 Client 패턴은 외부 의존성을 효과적으로 관리하고 테스트 가능한 코드를 작성하는 데 큰 도움을 주고 실제 구현과 mock 구현을 쉽게 전환할 수 있어 개발과 테스트 과정이 편리해진다는 점이 있어요
꼭 알아두셔야하니 혹여나 저가 쓴 글을 보고 이해를 못하신다면 다른 글들을 참고하셔서 꼭 이해하고 넘어가셔야해요 (설명이 매끄럽지 못했다면 죄송합니다ㅜㅜ)
이상으로 포스팅 마무리 하겠습니다.
.
.
.
감사합니다.