Communitygithub.com

SwiftMesh AI 스킬

SwiftMesh를 사용하기 위한 종합 AI 레퍼런스입니다. Alamofire + Codable 래퍼로 async/await, Combine, 플루언트 설정, 파일 업로드/다운로드, JSON 키 경로 파싱, 복원력 있는 Codable 래퍼, 내장 로깅을 지원합니다.

SwiftMesh AI 스킬란 무엇인가요?

이 스킬은 Swift 개발자를 위한 SwiftMesh 라이브러리의 포괄적인 참조 가이드입니다. SwiftMesh는 Alamofire와 Codable을 기반으로 구축된 강력한 네트워킹 래퍼로, async/await 구문, Combine 프레임워크, 플루언트 빌더 패턴을 완벽히 지원합니다. 주요 기능으로는 GET/POST 요청, 파일 업로드/다운로드, JSON 키 경로 파싱을 통한 중첩 데이터 추출, @Default, @IgnoreError, @ConvertTo* 와 같은 복원력 있는 Codable 프로퍼티 래퍼, RetryPolicy를 통한 자동 재시도, 그리고 상세한 네트워크 로깅이 포함됩니다. 이 가이드는 애플의 플랫폼(iOS, macOS, tvOS, watchOS)에서 SwiftUI 및 UIKit과의 통합을 위한 실용적인 사용 패턴, 전역 설정 옵션, 그리고 에러 처리 전략을 상세히 설명합니다.

지원 대상~Claude Code~Codex CLI~Cursor
npx skills add zjinhu/SwiftMesh

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

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.swiftasync/await 요청 실행
Handle.swiftURL 구성, 에러 처리, 응답 처리, 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(_:)HTTPMethodGET, POST, PUT, DELETE 등
.setHeads(_:)[String: String]요청별 헤더
.setRequestEncoding(_:)ParameterEncodingURLEncoding, 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

추가 자료

관련 스킬