SwiftMesh AI 스킬은(는) 무엇을 하나요?
SwiftMesh 사용을 위한 종합 AI 레퍼런스 — Alamofire + Codable 래퍼, async/await, Combine, 플루언트 설정, 파일 업로드/다운로드, JSON 키 경로 파싱, 복원력 있는 Codable 래퍼, 내장 로깅을 지원합니다.
빠른 참조 카드
| 기능 | 메서드 | 설명 |
|---|---|---|
| GET 요청 | .request(of: Model.self) | Codable 모델로 응답 디코딩 |
| 키 경로 | .request(of: Model.self, modelKeyPath: "data.user") | 중첩 JSON 추출 |
| 파일 업로드 | .upload(of: Result.self) | 파일/데이터/스트림/멀티파트 업로드 |
| 파일 다운로드 | .download() | 파일 다운로드 또는 재개 다운로드 |
| 원시 데이터 | .requestData() | 원시 응답 데이터 가져오기 |
| 원시 문자열 | .requestString() | 문자열로 응답 가져오기 |
| 재시도 정책 | .setInterceptor(RetryPolicy()) | 백오프로 자동 재시도 |
| 로깅 | Mesh.enableLog() | 네트워크 로깅 활성화 |
핵심 아키텍처
SwiftMesh는 Mesh 클래스에서 빌더 패턴을 사용합니다. 모든 설정 메서드는 Self를 반환하여 플루언트 체이닝을 가능하게 합니다. 흐름은 다음과 같습니다:
설정 (Mesh + Config) → 실행 (Request/Upload/Download) → 처리 (Handle)
파일 구조
| 파일 | 목적 |
|---|---|
Mesh.swift | 모든 속성이 있는 코어 빌더 클래스 |
Config.swift | 플루언트 체인 가능 세터 + 전역 설정 |
Request.swift | async/await 요청 실행 |
Handle.swift | URL 구성, 에러 처리, 응답 처리, RetryPolicy |
Upload.swift | 파일 업로드 (파일, 데이터, 스트림, 멀티파트) |
Download.swift | 파일 다운로드 (표준, 재개 가능) |
KeyPath.swift | 중첩 추출을 위한 JSON 키 경로 디코더 |
Codable+.swift | 복원력 있는 프로퍼티 래퍼 (@Default, @IgnoreError, @ConvertTo*) |
Log.swift | 네트워크 로거 (cURL, 상태, 타이밍, JSON) |
사용 패턴
1. 기본 GET 요청
let result = try await Mesh()
.setRequestMethod(.get)
.setUrlHost("https://api.example.com")
.setUrlPath("/weather/city/101030100")
.request(of: Weather.self)
2. JSON 키 경로 추출을 이용한 GET
전체 구조를 파싱하지 않고 JSON 응답의 중첩된 부분만 추출합니다:
// JSON: { "code": 200, "data": { "yesterday": { "temp": 25, "notice": "Sunny" } } }
let yesterday = try await Mesh()
.setRequestMethod(.get)
.setUrlHost("https://api.example.com")
.setUrlPath("/weather")
.request(of: Forecast.self, modelKeyPath: "data.yesterday")
3. 매개변수를 사용한 POST 요청
let result = try await Mesh()
.setRequestMethod(.post)
.setUrlHost("https://api.example.com")
.setUrlPath("/login")
.setParameters(["username": "admin", "password": "123456"])
.request(of: LoginResult.self)
4. JSON 인코딩을 사용한 POST
let result = try await Mesh()
.setRequestMethod(.post)
.setUrlHost("https://api.example.com")
.setUrlPath("/api/data")
.setRequestEncoding(JSONEncoding.default)
.setParameters(["key": "value"])
.request(of: Response.self)
5. 커스텀 헤더를 사용한 요청
let result = try await Mesh()
.setUrlHost("https://api.example.com")
.setUrlPath("/secure/data")
.setHeads(["Authorization": "Bearer token123"])
.request(of: SecureData.self)
6. 재시도 정책을 사용한 요청
let result = try await Mesh()
.setUrlHost("https://api.example.com")
.setUrlPath("/unstable-api")
.setInterceptor(RetryPolicy(maxRetryCount: 3))
.request(of: Data.self)
7. URLRequest 기반 요청
이미 URLRequestConvertible이 있는 경우:
let urlRequest = try URLRequest(url: URL(string: "https://api.example.com/data")!, method: .get)
let result = try await Mesh()
.urlRequest(urlRequest, type: Response.self)
8. 원시 데이터 응답
let data = try await Mesh()
.setRequestMethod(.get)
.setUrlHost("https://api.example.com")
.setUrlPath("/raw")
.requestData()
9. 원시 문자열 응답
let string = try await Mesh()
.setRequestMethod(.get)
.setUrlHost("https://api.example.com")
.setUrlPath("/text")
.requestString()
10. 파일 다운로드
let fileURL = try await Mesh()
.setRequestMethod(.get)
.setUrlHost("https://example.com")
.setUrlPath("/files/document.pdf")
.setDestination { _, _ in
let dest = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
.appendingPathComponent("document.pdf")
return (dest, [.removePreviousFile, .createIntermediateDirectories])
}
.download()
11. 재개 가능한 다운로드
let fileURL = try await Mesh()
.setUrlHost("https://example.com")
.setUrlPath("/files/large-file.zip")
.setDownloadType(.resume)
.setResumeData(savedResumeData) // 이전에 중단된 다운로드에서
.download()
12. 단일 파일 업로드 (URL)
let result = try await Mesh()
.setRequestMethod(.post)
.setUrlHost("https://api.example.com")
.setUrlPath("/upload")
.setUploadType(.file)
.setFileURL(fileURL)
.upload(of: UploadResult.self)
13. 단일 파일 업로드 (데이터)
let result = try await Mesh()
.setRequestMethod(.post)
.setUrlHost("https://api.example.com")
.setUrlPath("/upload")
.setUploadType(.data)
.setFileData(imageData)
.upload(of: UploadResult.self)
14. 멀티파트 폼 업로드
let result = try await Mesh()
.setRequestMethod(.post)
.setUrlHost("https://api.example.com")
.setUrlPath("/upload/multi")
.setUploadType(.multipart)
.setAddformData(name: "file",
fileName: "photo.jpg",
fileData: imageData,
mimeType: "image/jpeg")
.setAddformData(name: "description",
fileData: "My photo".data(using: .utf8))
.upload(of: UploadResult.self)
15. 사전 빌드된 UploadDatas로 멀티파트 업로드
let uploads = [
MultipleUpload.formData(name: "file1", fileName: "a.jpg", fileData: data1, mimeType: "image/jpeg"),
MultipleUpload.formData(name: "file2", fileName: "b.pdf", fileURL: fileURL)
]
let result = try await Mesh()
.setRequestMethod(.post)
.setUrlHost("https://api.example.com")
.setUrlPath("/upload/batch")
.setUploadType(.multipart)
.setUploadDatas(uploads)
.setParameters(["userId": "123"]) // 추가 폼 필드
.upload(of: BatchResult.self)
16. 커스텀 타임아웃
let result = try await Mesh()
.setUrlHost("https://api.example.com")
.setUrlPath("/slow-api")
.setTimeout(60) // 60초
.request(of: Response.self)
전역 설정
앱 시작 시 한 번만 설정합니다 (예: AppDelegate에서):
// 네트워크 로깅 활성화
Mesh.enableLog(.log) // 또는 .print
// 전역 기본 헤더 설정
Mesh.setHeaders(["Authorization": "Bearer token", "App-Version": "1.0"])
// 전역 기본 매개변수 설정
Mesh.setParameters(["platform": "ios", "sdk_version": "2.0"])
// 전역 URL 호스트 설정
Mesh.setUrlHost("https://api.example.com")
그러면 각 요청별 설정은 경로만 필요합니다:
// 전역 헤더, 매개변수, urlHost 사용
let result = try await Mesh()
.setUrlPath("/weather")
.request(of: Weather.self)
복원력 있는 Codable 프로퍼티 래퍼
디코딩 실패 없이 일관성 없는 API 응답을 우아하게 처리합니다.
@Default 래퍼
필드가 누락되거나 유효하지 않을 때 대체 값을 제공합니다:
struct Response: Codable {
@Default.True var isEnabled: Bool // 누락 → true
@Default.False var isDeleted: Bool // 누락 → false
@Default.EmptyString var name: String // 누락 → ""
@Default.EmptyInt var count: Int // 누락 → 0
@Default.EmptyArray var tags: [String] // 누락 → []
@Default.EmptyDictionary var meta: [String: Int] // 누락 → [:]
@Default.Now var createdAt: Date // 누락 → Date()
}
@IgnoreError
필드 디코딩에 실패해도 오류를 던지지 않고 nil을 반환합니다:
struct Response: Codable {
@IgnoreError var description: String? // 잘못된 타입 → nil (충돌 없음)
@IgnoreError var nested: NestedModel? // 누락/손상 → nil
}
@ConvertToString
JSON에서 String, Int, Double을 허용하고 String?로 변환합니다:
struct Response: Codable {
@ConvertToString var version: String? // "1.0", 1, 또는 1.0 → "1.0", "1", "1.0"
}
@ConvertToInt
JSON에서 Int, String, Double을 허용하고 Int?로 변환합니다:
struct Response: Codable {
@ConvertToInt var count: Int? // 42, "42", 또는 42.9 → 42, 42, 42
}
@ConvertToDouble
JSON에서 Double, Int, Float, String을 허용하고 Double?로 변환합니다:
struct Response: Codable {
@ConvertToDouble var price: Double? // 9.99, 10, "9.99" → 9.99, 10.0, 9.99
}
@ConvertToFloat
JSON에서 Float, Int, Double, String을 허용하고 Float?로 변환합니다:
struct Response: Codable {
@ConvertToFloat var rating: Float? // 4.5, 5, "4.5" → 4.5, 5.0, 4.5
}
JSON 키 경로 디코더
전체 응답을 파싱하지 않고 중첩된 JSON 값을 추출합니다:
// JSON: { "code": 200, "data": { "list": [{ "id": 1 }, { "id": 2 }] } }
// 단일 중첩 객체 추출
let firstItem = try await Mesh()
.setRequestMethod(.get)
.setUrlPath("/items")
.request(of: Item.self, modelKeyPath: "data.list.0")
// KeyPath 디코더는 직접 사용할 수도 있습니다:
let decoder = JSONDecoder.default
let item = try decoder.decode(Item.self, from: jsonData, keyPath: "data.list.0")
let items = try decoder.decodeArray([Item].self, from: jsonData, keyPath: "data.list")
기본 디코더는 자동으로 설정됩니다:
keyDecodingStrategy = .convertFromSnakeCase(snake_case → camelCase)dateDecodingStrategy = .iso8601
Combine + SwiftUI 통합
ObservableObject 패턴
class RequestModel: ObservableObject {
@MainActor @Published var weather: Weather?
@MainActor @Published var errorMessage: String?
func fetchWeather() {
Task {
do {
let result = try await Mesh()
.setRequestMethod(.get)
.setUrlHost("https://api.example.com")
.setUrlPath("/weather")
.request(of: Weather.self)
await MainActor.run { self.weather = result }
} catch {
await MainActor.run { self.errorMessage = error.localizedDescription }
}
}
}
}
SwiftUI 사용
struct WeatherView: View {
@StateObject private var model = RequestModel()
var body: some View {
VStack {
if let weather = model.weather {
Text("\(weather.temperature)°C")
}
if let error = model.errorMessage {
Text(error).foregroundColor(.red)
}
}
.onAppear { model.fetchWeather() }
}
}
UIKit + Combine 사용
class ViewController: UIViewController {
private var model = RequestModel()
private var cancellables: Set<AnyCancellable> = []
override func viewDidLoad() {
super.viewDidLoad()
model.$weather
.receive(on: RunLoop.main)
.sink { weather in
print("Weather: \(String(describing: weather))")
}
.store(in: &cancellables)
model.fetchWeather()
}
}
에러 처리
SwiftMesh는 일반적인 네트워크 오류를 사용자 친화적인 NSError로 정규화합니다:
| 에러 코드 | 설명 |
|---|---|
NSURLErrorNotConnectedToInternet | 인터넷 연결 없음 |
NSURLErrorTimedOut | 요청 시간 초과 |
NSURLErrorCannotFindHost | 호스트를 찾을 수 없음 |
NSURLErrorCannotConnectToHost | 호스트에 연결할 수 없음 |
NSURLErrorNetworkConnectionLost | 요청 중 연결 끊김 |
모든 정규화된 오류는 다음 메시지를 반환합니다: "Unable to connect to the server"
비동기 코드에서 오류를 캐치합니다:
do {
let result = try await Mesh()
.setUrlPath("/api")
.request(of: Response.self)
} catch let error as NSError {
print("Error \(error.code): \(error.localizedDescription)")
}
RetryPolicy
선형 백오프(1초, 2초, 3초...)가 내장된 재시도:
// 기본: 3회 재시도
let policy = RetryPolicy()
// 커스텀: 5회 재시도
let policy = RetryPolicy(maxRetryCount: 5)
// 요청과 함께 사용
let result = try await Mesh()
.setUrlPath("/unstable-api")
.setInterceptor(policy)
.request(of: Response.self)
로깅
앱 시작 시 활성화:
// Apple의 통합 Logger 사용 (os.log)
Mesh.enableLog(.log)
// Swift print() 사용
Mesh.enableLog(.print)
출력에는 다음이 포함됩니다:
- cURL 명령어 (쉬운 재현을 위해)
- HTTP 상태 코드
- 경과 시간
- 가독성 좋게 출력된 JSON 응답
설정 메서드 참조
모든 메서드는 체인 가능합니다 (Self 반환):
기본 설정
| 메서드 | 타입 | 설명 |
|---|---|---|
.setTimeout(_:) | TimeInterval | 요청 시간 초과(초) (기본값: 15) |
.setInterceptor(_:) | RequestInterceptor? | 재시도 정책/자격 증명 처리기 |
.setRequestMethod(_:) | HTTPMethod | GET, POST, PUT, DELETE 등 |
.setHeads(_:) | [String: String] | 요청별 헤더 |
.setRequestEncoding(_:) | ParameterEncoding | URLEncoding, JSONEncoding 등 |
.setUrlHost(_:) | String? | 요청별 URL 호스트 |
.setUrlPath(_:) | String? | URL 경로 |
.setParameters(_:) | [String: Any]? | 요청 매개변수 |
다운로드 설정
| 메서드 | 타입 | 설명 |
|---|---|---|
.setDownloadType(_:) | DownloadType | .download 또는 .resume |
.setDestination(_:) | DownloadRequest.Destination? | 다운로드 대상 경로 |
.setResumeData(_:) | Data? | 재개 다운로드를 위한 저장된 데이터 |
업로드 설정
| 메서드 | 타입 | 설명 |
|---|---|---|
.setUploadType(_:) | UploadType | .file, .data, .stream, .multipart |
.setFileURL(_:) | URL? | 업로드할 파일 URL |
.setFileData(_:) | Data? | 업로드할 데이터 |
.setStream(_:) | InputStream? | 업로드할 스트림 |
.setAddformData(_:) | MultipleUpload | 멀티파트 폼 데이터 추가 |
.setUploadDatas(_:) | [MultipleUpload] | 멀티파트 데이터 배열 설정 |
설치 (Swift Package Manager)
Package.swift에 종속성으로 추가:
dependencies: [
.package(url: "https://github.com/zjinhu/SwiftMesh.git", from: "1.0.0")
]
또는 Xcode에서 **File > Add Packages...**로 이동하여 다음 URL을 검색:
https://github.com/zjinhu/SwiftMesh.git
추가 자료
- GitHub 저장소: zjinhu/SwiftMesh
- 라이선스: MIT