[G1-Sync] Manual knowledge update

This commit is contained in:
Antigravity Agent
2026-05-09 21:08:02 +09:00
parent f0befc887a
commit 93ec7e9056
363 changed files with 68333 additions and 64 deletions
@@ -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]]