[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
---
|
||||
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<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
|
||||
```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<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
|
||||
```swift
|
||||
let task = Task { try await api.send(.user(id: "1")) }
|
||||
// view disappear
|
||||
task.cancel()
|
||||
```
|
||||
|
||||
### Retry with backoff
|
||||
```swift
|
||||
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 자동화 명시.
|
||||
|
||||
## 🔗 관련 문서
|
||||
- [[iOS_Swift_Concurrency_async_await]]
|
||||
- [[iOS_Swift_Result_Type]]
|
||||
- [[Web_JWT_Patterns]]
|
||||
Reference in New Issue
Block a user