Files
2nd/10_Wiki/Topics/Coding/iOS_Network_URLSession_Patterns.md
T
2026-05-09 21:08:02 +09:00

4.2 KiB

id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
id title category status source_trust_level verification_status created_at updated_at tags tech_stack applied_in aliases
ios-network-urlsession-patterns URLSession 네트워킹 실전 패턴 Coding draft B conceptual 2026-05-09 2026-05-09
ios
networking
urlsession
swift
vibe-coding
language applicable_to
Swift
iOS
macOS
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 추상화

struct Endpoint<Response: Decodable> {
    let path: String
    let method: String
    let body: Data?
}

extension Endpoint where Response == User {
    static func user(id: String) -> Endpoint<User> {
        Endpoint(path: "/users/\(id)", method: "GET", body: nil)
    }
}

APIClient

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<R>(_ ep: Endpoint<R>) 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

let task = Task { try await api.send(.user(id: "1")) }
// view disappear
task.cancel()

Retry with backoff

func withRetry<T>(_ op: @escaping () async throws -> T,
                   max: Int = 3) async throws -> T {
    var last: Error?
    for attempt in 0..<max {
        do { return try await op() }
        catch let e where shouldRetry(e) {
            last = e
            try await Task.sleep(nanoseconds: UInt64(pow(2.0, Double(attempt)) * 1e9))
        }
    }
    throw last ?? APIError.unknown
}

🤔 의사결정 기준

상황 권장
단순 앱 (10 endpoint 내외) URLSession + Endpoint enum
복잡 / GraphQL Apollo / Relay
Reactive (Combine) URLSession.dataTaskPublisher
배경 다운로드 URLSession background config
실시간 URLSessionWebSocketTask 또는 Starscream

안티패턴

  • URLSession.shared 에 직접 의존: 테스트 어려움. 주입.
  • status code 검증 누락: 4xx/5xx body 를 모델로 decode 시도 → 잘못된 에러 메시지.
  • JSON 키 case 불일치 처리 안 함: snake_case 응답 + camelCase struct → decode 실패. keyDecodingStrategy.
  • Date decoding 기본 (UNIX timestamp): API 가 ISO8601 이면 깨짐. 명시.
  • 에러를 그냥 print: 디버깅 어려움. 구조화 로깅 + Sentry.
  • Background 작업을 일반 dataTask 로: 앱 suspend 시 끊김. background config 또는 URLSession.shared.
  • 인증 토큰 만료 처리 누락: 401 에 그냥 fail. 401 → refresh → 재시도 로직.

🤖 LLM 활용 힌트

  • Endpoint enum + APIClient actor 권장.
  • 401 → refresh → retry 자동화 명시.

🔗 관련 문서