SwiftUI - TCA Dependency : Client

CodeCat·2024년 9월 9일

IOS SwiftUI TCA

목록 보기
14/20
post-thumbnail

안녕하세요 !

이번에는 TCA Dependency Client 주제로 포스팅 해보려 합니다

Client는 TCA에서 외부 의존성을 추상화하고 관리하는 역할을 해요

Client를 통해 네트워크 요청, 데이터베이스 작업, 또는 기타 외부 서비스와의 상호작용을 캡슐화하고 테스트 가능한 형태로 만들 수 있습니다

Client의 기본 개념

TCA에서 Client는 일반적으로 특정 기능이나 서비스를 추상화한 프로토콜이며 이를 통해 실제 구현과 테스트용 모의 구현을 쉽게 교체할 수 있어요!!

protocol APIClientProtocol {
    func fetch() async throws -> [Item]
    func send(_ item: Item) async throws
}

APIClientProtocol은 데이터를 가져오고 보내는 두 가지 기본 작업을 예시로 정의 했어요

Client 구현하기

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 등록

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에 접근할 수 있어요~~!

Reducer에서 Client 사용하기

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

아래와 같이 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 구현을 쉽게 전환할 수 있어 개발과 테스트 과정이 편리해진다는 점이 있어요

꼭 알아두셔야하니 혹여나 저가 쓴 글을 보고 이해를 못하신다면 다른 글들을 참고하셔서 꼭 이해하고 넘어가셔야해요 (설명이 매끄럽지 못했다면 죄송합니다ㅜㅜ)

이상으로 포스팅 마무리 하겠습니다.

.
.
.

감사합니다.

profile
코드와 고양이의 만남

0개의 댓글