--- id: ios-network-urlsession-patterns title: URLSession 네트워킹 실전 패턴 category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [ios, networking, urlsession, swift, vibe-coding] tech_stack: { language: "Swift", applicable_to: ["iOS", "macOS"] } applied_in: [] aliases: [networking layer, URLRequest, decoding, retry] --- # URLSession 네트워킹 실전 > URLSession 직접 쓰면 OK. 단 **(1) Endpoint 추상화, (2) decoding 통합, (3) 인증 헤더 자동, (4) 재시도/취소** 4가지가 있어야 production-grade. ## 📖 핵심 개념 - 단일 `URLSession` 인스턴스 (custom config) — 테스트 + 실제 분리. - `URLRequest` builder 또는 Endpoint enum. - `JSONDecoder` 공유 (snake_case 변환 등). - async/await 가 기본 — `data(for:)`, `data(from:)`. ## 💻 코드 패턴 ### Endpoint 추상화 ```swift struct Endpoint { let path: String let method: String let body: Data? } extension Endpoint where Response == User { static func user(id: String) -> Endpoint { Endpoint(path: "/users/\(id)", method: "GET", body: nil) } } ``` ### APIClient ```swift actor APIClient { private let session: URLSession private let baseURL: URL private let decoder: JSONDecoder private var token: String? init(baseURL: URL, session: URLSession = .shared) { self.session = session self.baseURL = baseURL self.decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase decoder.dateDecodingStrategy = .iso8601 } func setToken(_ t: String?) { self.token = t } func send(_ ep: Endpoint) async throws -> R { var req = URLRequest(url: baseURL.appendingPathComponent(ep.path)) req.httpMethod = ep.method req.httpBody = ep.body req.setValue("application/json", forHTTPHeaderField: "Content-Type") if let token { req.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") } let (data, resp) = try await session.data(for: req) guard let http = resp as? HTTPURLResponse else { throw APIError.invalidResponse } guard (200..<300).contains(http.statusCode) else { throw APIError.status(http.statusCode, data) } return try decoder.decode(R.self, from: data) } } // 사용 let user = try await api.send(.user(id: "1")) ``` ### Cancellation ```swift let task = Task { try await api.send(.user(id: "1")) } // view disappear task.cancel() ``` ### Retry with backoff ```swift func withRetry(_ op: @escaping () async throws -> T, max: Int = 3) async throws -> T { var last: Error? for attempt in 0..