diff --git a/README.ko.md b/README.ko.md new file mode 100644 index 0000000..cc0ca59 --- /dev/null +++ b/README.ko.md @@ -0,0 +1,361 @@ +# Nexa + +[![Build](https://github.com/opficdev/Nexa/actions/workflows/build.yml/badge.svg)](https://github.com/opficdev/Nexa/actions/workflows/build.yml) +[![Swift](https://img.shields.io/badge/Swift-6.1-orange?style=flat-square)](https://www.swift.org) +[![Platforms](https://img.shields.io/badge/Platforms-iOS_15%2B_macOS_12%2B-blue?style=flat-square)](https://github.com/opficdev/Nexa) +[![Swift Package Manager](https://img.shields.io/badge/Swift_Package_Manager-compatible-brightgreen?style=flat-square)](https://swift.org/package-manager/) + +[English](README.md) | [한국어](README.ko.md) + +Nexa는 `URLSession` 기반의 SwiftUI 스타일 선언형 네트워킹 라이브러리입니다. + +- [기능](#기능) +- [요구 사항](#요구-사항) +- [설치](#설치) +- [공개 API](#공개-api) +- [빠른 시작](#빠른-시작) +- [Endpoint API](#endpoint-api) +- [설정](#설정) +- [테스트](#테스트) + +## 기능 + +- [x] `GET`, `POST`, `PUT`, `PATCH`, `DELETE`를 위한 선언형 요청 빌더 +- [x] Swift Concurrency를 활용한 타입 안전 응답 디코딩 +- [x] 값 타입 기반 요청 조합 +- [x] 쿼리, 헤더, 타임아웃, 바디, JSON 인코딩 지원 +- [x] 컴파일 타임 응답 타입을 갖는 Endpoint 기반 API +- [x] 요청 단위 및 전역 인터셉터 체인 +- [x] `NXAuthTokenProvider`를 통한 인증 및 토큰 갱신 흐름 내장 +- [x] Fixed backoff 및 exponential backoff 기반 재시도 정책 +- [x] 응답 유효성 검사 및 서버 에러 디코딩 +- [x] 로거 훅 및 테스트를 위한 transport 추상화 + +## 요구 사항 + +| 플랫폼 | Swift | 설치 | +| --- | --- | --- | +| iOS 15.0+ / macOS 12.0+ | Swift 6.1 | [Swift Package Manager](#swift-package-manager) | + +## 설치 + +### Swift Package Manager + +`Package.swift`에 Nexa를 추가하세요: + +```swift +dependencies: [ + .package(url: "https://github.com/opficdev/Nexa.git", branch: "main") +] +``` + +그런 다음 타겟 의존성에 `Nexa`를 추가하세요: + +```swift +.target( + name: "AppModule", + dependencies: [ + .product(name: "Nexa", package: "Nexa") + ] +) +``` + +## 공개 API + +대부분의 코드는 `NXAPIClient`에서 시작하여 `NXRequestBuilder` 또는 `NXTypedRequestBuilder`로 이어집니다. + +나머지 공개 인터페이스는 인증, 로깅, 테스트, 재시도, 유효성 검사, 커스텀 에러 매핑을 위한 확장 포인트로 구성됩니다. + +| API | 사용 시점 | 예시 | +| --- | --- | --- | +| `NXAPIClient` | 동일한 `baseURL`과 설정을 공유하는 요청의 주 진입점 | `client.get("/users", as: User.self).send()` | +| `NXRequestBuilder` | 조립된 `URLRequest`를 직접 확인하거나 디코딩 없이 `NXRawResponse`를 받고 싶을 때 | `try await client.get("/users").raw()` | +| `NXTypedRequestBuilder` | 응답을 `Decodable` 타입으로 바로 디코딩할 때 | `try await client.get("/users/1", as: User.self).send()` | +| `NXEndpoint` | 엔드포인트 정의를 재사용하고 응답 타입을 함께 관리할 때 | `try await client.send(UserEndpoint(identifier: 1))` | +| `NXClientConfiguration` | 공통 헤더, transport, 로거, 인증, 인코더, 디코더, 인터셉터를 한 번에 설정할 때 | `NXClientConfiguration(baseURL: url, authTokenProvider: yourAuthTokenProvider)` | +| `NXRetryPolicy` | 재시도 가능한 상태 코드나 전송 오류 시 재시도할 때 | `.retry(.init(maxAttempts: 3))` | +| `NXValidationPolicy` | 허용할 상태 코드가 기본값(`200..<300`)과 다를 때 | `.validate(.statusCodes([200, 201, 204]))` | +| `NXHTTPTransport` | 테스트용 스텁이 필요하거나 transport 구현을 교체할 때 | `NXClientConfiguration(baseURL: url, transport: yourStubTransport)` | +| `NXHTTPInterceptor` | 트레이싱이나 헤더 주입처럼 요청 전반에 적용되는 처리가 필요할 때 | `.intercept(yourInterceptor)` | +| `NXAuthTokenProvider` | `.authorized()` 요청에 토큰 조회 및 갱신 기능이 필요할 때 | `authTokenProvider: yourAuthTokenProvider` | +| `NXServerErrorDecoder` | 실패 응답을 도메인 에러로 디코딩할 때 | `serverErrorDecoder: yourServerErrorDecoder` | +| `NXLogger` | 구조화된 요청 생명주기 로깅이 필요할 때 | `logger: yourLogger` | +| `NXRawResponse` | `Data`와 `HTTPURLResponse`를 직접 다뤄야 할 때 | `let response = try await client.get("/users").raw()` | +| `NXError` | 호출 코드에서 Nexa 고유 오류를 처리할 때 | `catch let error as NXError` | +| `NXHTTPMethod` | `NXEndpoint`의 메서드를 정의할 때 | `var method: NXHTTPMethod { .post }` | + +### 어디서 시작할까요? + +아래에서 `client`는 이미 설정된 `NXAPIClient`라고 가정합니다. + +대부분의 앱 코드에서는 `NXAPIClient`와 `NXTypedRequestBuilder` 조합을 사용할 수 있습니다. + +```swift +import Foundation +import Nexa + +struct User: Decodable { + let id: Int + let name: String +} + +let user = try await client + .get("/users/42", as: User.self) + .send() +``` + +요청을 직접 확인하거나 원시 응답을 직접 처리할 때는 `NXRequestBuilder`를 사용하면 됩니다. + +```swift +import Foundation +import Nexa + +let request = try await client + .post("/users") + .header("X-Trace-Id", UUID().uuidString) + .preparedURLRequest() +``` + +동일한 엔드포인트 형태가 여러 곳에서 재사용될 때는 `NXEndpoint`를 사용하면 됩니다. + +```swift +import Foundation +import Nexa + +struct User: Decodable { + let id: Int + let name: String +} + +struct UserEndpoint: NXEndpoint { + let identifier: Int + + var method: NXHTTPMethod { .get } + var path: String { "/users/\(identifier)" } + + func configure(_ builder: NXTypedRequestBuilder) -> NXTypedRequestBuilder { + builder.query("include", "profile") + } +} +``` + +기본 동작으로 충분하지 않을 때만 하위 레벨 프로토콜을 사용하면 됩니다. + +- `NXHTTPTransport`: 스텁, 목, 커스텀 네트워크 백엔드 +- `NXHTTPInterceptor`: 트레이싱, 요청 변환, 커스텀 흐름 제어 +- `NXAuthTokenProvider`: bearer token 주입 및 갱신 +- `NXServerErrorDecoder`: 서버 payload를 도메인 에러로 매핑 +- `NXLogger`: 요청 생명주기 로깅 및 관측성 + +## 요청 흐름 + +```mermaid +sequenceDiagram + autonumber + participant App + participant Config as NXClientConfiguration + participant Client as NXAPIClient + participant Builder as Builder + participant Assembler as NXRequestAssembler + participant Chain as NXInterceptorChain + participant Transport as NXHTTPTransport + participant Pipeline as NXResponsePipeline + + App->>Config: 공통 설정 구성 + App->>Client: NXAPIClient(configuration) + + alt method 기반 호출 + App->>Client: get/post/put/patch/delete + Client->>Builder: NXRequestBuilder or NXTypedRequestBuilder + else endpoint 기반 호출 + App->>Client: request(endpoint) / send(endpoint) + Client->>Builder: endpoint.configure(builder) + end + + App->>Builder: query/header/authorized/retry/validate/intercept... + + alt preparedURLRequest() + Builder->>Assembler: assemble() + Assembler-->>Builder: URLRequest + Builder-->>App: URLRequest + else raw() + Builder->>Assembler: assemble() + Assembler-->>Builder: URLRequest + Builder->>Chain: execute(context) + Chain->>Transport: send(request) + Transport-->>Chain: NXRawResponse + Chain-->>Builder: NXRawResponse + Builder->>Pipeline: validate() + Pipeline-->>Builder: validated + Builder-->>App: NXRawResponse + else send() + Builder->>Assembler: assemble() + Assembler-->>Builder: URLRequest + Builder->>Chain: execute(context) + Chain->>Transport: send(request) + Transport-->>Chain: NXRawResponse + Chain-->>Builder: NXRawResponse + Builder->>Pipeline: validate() + Pipeline-->>Builder: validated + Builder->>Pipeline: decode() + Pipeline-->>Builder: Response + Builder-->>App: Response + end +``` + +## 빠른 시작 + +Nexa는 인증, 재시도, 유효성 검사, 디코딩을 하나의 흐름으로 간결하게 표현합니다. + +```swift +import Foundation +import Nexa + +struct User: Decodable { + let id: Int + let name: String +} + +let client = NXAPIClient( + configuration: NXClientConfiguration( + baseURL: URL(string: "https://api.example.com")! + ) +) + +let user = try await client + .get("/users/me", as: User.self) + .query("include", "profile") + .accept("application/json") + .send() +``` + +`.authorized()`는 클라이언트에 `authTokenProvider`가 설정된 경우에만 추가하세요. + +단계별로 요청을 구성할 수도 있습니다: + +```swift +import Foundation +import Nexa + +struct CreateUserPayload: Encodable { + let name: String +} + +struct User: Decodable { + let id: Int + let name: String +} + +let createdUser = try await client + .post("/users", as: User.self) + .header("X-Trace-Id", UUID().uuidString) + .json(CreateUserPayload(name: "opfic")) + .send() +``` + +## Endpoint API + +Moya 스타일의 엔드포인트 추상화를 선호한다면, `NXEndpoint`를 정의하여 응답 타입을 엔드포인트에 직접 연결할 수 있습니다. + +```swift +import Foundation +import Nexa + +struct User: Decodable { + let id: Int + let name: String +} + +struct UserEndpoint: NXEndpoint { + let identifier: Int + + var method: NXHTTPMethod { .get } + var path: String { "/users/\(identifier)" } + + func configure(_ builder: NXTypedRequestBuilder) -> NXTypedRequestBuilder { + builder + .query("include", "profile") + .accept("application/json") + } +} + +let user = try await client.send(UserEndpoint(identifier: 42)) +``` + +## 설정 + +`NXClientConfiguration`은 커스텀 API 레이어에 흩어지기 쉬운 설정을 한 곳에 집중시킵니다. + +```swift +import Foundation +import Nexa + +let configuration = NXClientConfiguration( + baseURL: URL(string: "https://api.example.com")!, + headers: [ + "Accept": "application/json" + ], + transport: NXURLSessionTransport(), + logger: NXNoopLogger(), + interceptors: [], + serverErrorDecoder: NXDefaultServerErrorDecoder(), + authTokenProvider: nil +) + +let client = NXAPIClient(configuration: configuration) +``` + +커스텀 동작이 필요할 때만 기본값을 직접 구현한 타입으로 교체하세요: + +- `NXLogger`: 구조화된 로깅 +- `NXHTTPInterceptor`: 요청 트레이싱 또는 변환 +- `NXServerErrorDecoder`: 실패 응답을 도메인 에러로 매핑 +- `NXAuthTokenProvider`: bearer token 주입 및 갱신 + +현재 Nexa가 지원하는 기능: + +- 전역 헤더 및 요청별 헤더 +- 원시 바디 및 JSON 바디 인코딩 +- 요청 단위 유효성 검사 정책 +- `.authorized()` 요청에 대한 자동 인증 헤더 주입 +- 토큰 갱신 및 재시도 처리 +- 스터빙 및 격리 테스트를 위한 커스텀 transport + +## 테스트 + +Nexa는 요청 실행을 테스트하기 쉽도록 설계되었습니다. `NXHTTPTransport`를 통해 실제 네트워킹을 커스텀 transport로 교체하고 발신 요청과 디코딩된 응답을 검증할 수 있습니다. + +```swift +import Foundation +import Nexa + +struct User: Codable, Equatable { + let id: Int + let name: String +} + +struct StubTransport: NXHTTPTransport { + func send(_ request: URLRequest) async throws -> NXRawResponse { + let response = HTTPURLResponse( + url: request.url!, + statusCode: 200, + httpVersion: nil, + headerFields: nil + )! + + return NXRawResponse( + data: Data(#"{"id":1,"name":"opfic"}"#.utf8), + response: response + ) + } +} + +let client = NXAPIClient( + configuration: NXClientConfiguration( + baseURL: URL(string: "https://example.com")!, + transport: StubTransport() + ) +) + +let user = try await client.get("/users/1", as: User.self).send() +``` diff --git a/README.md b/README.md index 94a2f48..45b46e7 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ [![Platforms](https://img.shields.io/badge/Platforms-iOS_15%2B_macOS_12%2B-blue?style=flat-square)](https://github.com/opficdev/Nexa) [![Swift Package Manager](https://img.shields.io/badge/Swift_Package_Manager-compatible-brightgreen?style=flat-square)](https://swift.org/package-manager/) +[English](README.md) | [한국어](README.ko.md) + Nexa is a SwiftUI-inspired declarative networking library built on `URLSession`. - [Features](#features)