์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐ŸŒ Swift ๋„คํŠธ์›Œํ‚น ์™„์ „ ๊ฐ€์ด๋“œ: URLSession๋ถ€ํ„ฐ Alamofire๊นŒ์ง€ API ํ†ต์‹ ์˜ ๋ชจ๋“  ๊ฒƒ
๐Ÿ“ฑ ํ”„๋กœ๊ทธ๋žจ๊ฐœ๋ฐœ ยท Swift

๐ŸŒ Swift ๋„คํŠธ์›Œํ‚น ์™„์ „ ๊ฐ€์ด๋“œ: URLSession๋ถ€ํ„ฐ Alamofire๊นŒ์ง€ API ํ†ต์‹ ์˜ ๋ชจ๋“  ๊ฒƒ

iOS ์•ฑ ๊ฐœ๋ฐœ์ž๋ผ๋ฉด ๋ฐ˜๋“œ์‹œ ์•Œ์•„์•ผ ํ•  ๋„คํŠธ์›Œํฌ ํ†ต์‹ ์˜ ํ•ต์‹ฌ ๊ฐœ๋…๊ณผ ์‹ค์ „ ์ฝ”๋“œ๋ฅผ ์นœ๊ตฌ์ฒ˜๋Ÿผ ์‰ฝ๊ฒŒ ํ’€์–ด๋“œ๋ฆฝ๋‹ˆ๋‹ค ๐Ÿš€

๐Ÿ“ฑ iOS ์•ฑ Swift ๐Ÿ”— URLSession Apple ๊ธฐ๋ณธ ์ œ๊ณต ๐Ÿ”ฅ Alamofire ์„œ๋“œํŒŒํ‹ฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๐Ÿ–ฅ๏ธ API ์„œ๋ฒ„ iOS ์•ฑ โ†” ๋„คํŠธ์›Œํฌ ํ†ต์‹ ์˜ ํ๋ฆ„ URLSession(๊ธฐ๋ณธ) ๋˜๋Š” Alamofire(ํŽธ์˜)๋ฅผ ํ†ตํ•ด API ์„œ๋ฒ„์™€ ๋ฐ์ดํ„ฐ๋ฅผ ์ฃผ๊ณ ๋ฐ›์Šต๋‹ˆ๋‹ค

๐Ÿค” ์™œ ๋„คํŠธ์›Œํ‚น์„ ์•Œ์•„์•ผ ํ• ๊นŒ?

์•ผ, ์†”์งํžˆ ๋งํ•ด๋ณผ๊ฒŒ. ์š”์ฆ˜ ์•ฑ ์ค‘์— ์ธํ„ฐ๋„ท ํ†ต์‹  ์•ˆ ํ•˜๋Š” ์•ฑ์ด ์žˆ์–ด? ๐Ÿคฃ
๋‚ ์”จ ์•ฑ์€ ์„œ๋ฒ„์—์„œ ๋‚ ์”จ ๋ฐ์ดํ„ฐ ๋ฐ›์•„์˜ค๊ณ , ์‡ผํ•‘ ์•ฑ์€ ์ƒํ’ˆ ๋ชฉ๋ก ๋ถˆ๋Ÿฌ์˜ค๊ณ , SNS๋Š” ํ”ผ๋“œ ์—…๋ฐ์ดํŠธํ•˜๊ณ ...
ํ˜„๋Œ€ iOS ์•ฑ์˜ 90% ์ด์ƒ์€ ์–ด๋–ค ํ˜•ํƒœ๋กœ๋“  ๋„คํŠธ์›Œํฌ ํ†ต์‹ ์„ ์‚ฌ์šฉํ•ด.

๊ทธ๋ž˜์„œ Swift๋กœ iOS ๊ฐœ๋ฐœ์„ ํ•œ๋‹ค๋ฉด ๋„คํŠธ์›Œํ‚น์€ ์„ ํƒ์ด ์•„๋‹ˆ๋ผ ํ•„์ˆ˜ ์Šคํ‚ฌ์ด์•ผ.
์˜ค๋Š˜์€ Apple์ด ๊ธฐ๋ณธ์œผ๋กœ ์ œ๊ณตํ•˜๋Š” URLSession๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด์„œ,
๊ฐœ๋ฐœ์ž๋“ค์ด ์‚ฌ๋ž‘ํ•˜๋Š” ์„œ๋“œํŒŒํ‹ฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ Alamofire๊นŒ์ง€ ์ œ๋Œ€๋กœ ํŒŒํ—ค์ณ๋ณผ ๊ฑฐ์•ผ!

๊ฒ๋จน์ง€ ๋งˆ. ์ฐจ๊ทผ์ฐจ๊ทผ ๊ฐ™์ด ๊ฐ€๋ณด์ž๊ณ  ๐Ÿ˜Ž


๐Ÿ“ก HTTP์™€ REST API ๊ธฐ์ดˆ โ€” ๋จผ์ € ์ด๊ฒƒ๋ถ€ํ„ฐ!

URLSession์ด๊ณ  Alamofire๊ณ  ๊ฐ„์—, ๊ทธ ์ „์— HTTP์™€ REST API๊ฐ€ ๋ญ”์ง€ ์•Œ์•„์•ผ ํ•ด.
์ด๋ฏธ ์•ˆ๋‹ค๊ณ ? ๊ทธ๋ž˜๋„ ํ•œ ๋ฒˆ๋งŒ ๊ฐ™์ด ๋ณต์Šตํ•˜์ž. ๊ธฐ์ดˆ๊ฐ€ ํƒ„ํƒ„ํ•ด์•ผ ๋‚˜์ค‘์— ์•ˆ ํ”๋“ค๋ฆฌ๊ฑฐ๋“ .

๐ŸŒ HTTP ๋ฉ”์„œ๋“œ์˜ ์ข…๋ฅ˜

์„œ๋ฒ„๋ž‘ ๋Œ€ํ™”ํ•  ๋•Œ ์šฐ๋ฆฌ๊ฐ€ ์“ฐ๋Š” "๋™์‚ฌ"๋“ค์ด์•ผ:

๋ฉ”์„œ๋“œ์—ญํ• ์˜ˆ์‹œ
GET๋ฐ์ดํ„ฐ ์กฐํšŒ๊ฒŒ์‹œ๊ธ€ ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ
POST๋ฐ์ดํ„ฐ ์ƒ์„ฑ์ƒˆ ๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ
PUT๋ฐ์ดํ„ฐ ์ „์ฒด ์ˆ˜์ •๊ฒŒ์‹œ๊ธ€ ์ „์ฒด ์—…๋ฐ์ดํŠธ
PATCH๋ฐ์ดํ„ฐ ์ผ๋ถ€ ์ˆ˜์ •๊ฒŒ์‹œ๊ธ€ ์ œ๋ชฉ๋งŒ ๋ณ€๊ฒฝ
DELETE๋ฐ์ดํ„ฐ ์‚ญ์ œ๊ฒŒ์‹œ๊ธ€ ์‚ญ์ œ

๐Ÿ“Š HTTP ์ƒํƒœ ์ฝ”๋“œ

์„œ๋ฒ„๊ฐ€ "๋‚˜ ์ง€๊ธˆ ์–ด๋•Œ?" ๋ผ๊ณ  ์•Œ๋ ค์ฃผ๋Š” ์ˆซ์ž ์ฝ”๋“œ์•ผ:

์ฝ”๋“œ์˜๋ฏธ์ƒํ™ฉ
200OK์š”์ฒญ ์„ฑ๊ณต! ๐ŸŽ‰
201Created์ƒ์„ฑ ์„ฑ๊ณต!
400Bad Request์š”์ฒญ์ด ์ž˜๋ชป๋จ ๐Ÿ˜…
401Unauthorized์ธ์ฆ ํ•„์š”
403Forbidden์ ‘๊ทผ ๊ถŒํ•œ ์—†์Œ
404Not Found์—†๋Š” ๋ฆฌ์†Œ์Šค ์š”์ฒญ
500Internal Server Error์„œ๋ฒ„ ๋‚ด๋ถ€ ์˜ค๋ฅ˜ ๐Ÿ˜ฑ
๐Ÿ’ก ๊ฟ€ํŒ! 2xx๋Š” ์„ฑ๊ณต, 4xx๋Š” ํด๋ผ์ด์–ธํŠธ(๋‚ด) ์ž˜๋ชป, 5xx๋Š” ์„œ๋ฒ„ ์ž˜๋ชป์ด๋ผ๊ณ  ๊ธฐ์–ตํ•˜๋ฉด ๋ผ!

๐Ÿ”‘ JSON โ€” ๋ฐ์ดํ„ฐ ๊ตํ™˜์˜ ํ‘œ์ค€์–ด

์„œ๋ฒ„๋ž‘ ์•ฑ์ด ๋ฐ์ดํ„ฐ๋ฅผ ์ฃผ๊ณ ๋ฐ›์„ ๋•Œ ๊ฐ€์žฅ ๋งŽ์ด ์“ฐ๋Š” ํ˜•์‹์ด JSON(JavaScript Object Notation)์ด์•ผ.
Swift์—์„œ๋Š” Codable ํ”„๋กœํ† ์ฝœ๋กœ JSON์„ ์‰ฝ๊ฒŒ ๋‹ค๋ฃฐ ์ˆ˜ ์žˆ์–ด.

JSON ์˜ˆ์‹œ
{
  "id": 1,
  "name": "๊น€๊ฐœ๋ฐœ",
  "email": "dev@example.com",
  "skills": ["Swift", "iOS", "Xcode"],
  "isActive": true
}

๐Ÿ”— URLSession โ€” Apple์ด ๋งŒ๋“  ๋„คํŠธ์›Œํ‚น์˜ ๊ธฐ๋ณธ๊ธฐ

URLSession์€ Apple์ด iOS SDK์— ๊ธฐ๋ณธ์œผ๋กœ ํฌํ•จ์‹œํ‚จ ๋„คํŠธ์›Œํ‚น ํ”„๋ ˆ์ž„์›Œํฌ์•ผ.
์™ธ๋ถ€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์—†์ด๋„ HTTP ํ†ต์‹ ์„ ์™„๋ฒฝํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด.
์ฒ˜์Œ์—” ์ข€ ๋ณต์žกํ•ด ๋ณด์ผ ์ˆ˜ ์žˆ๋Š”๋ฐ, ๊ตฌ์กฐ๋ฅผ ์ดํ•ดํ•˜๋ฉด ์ƒ๊ฐ๋ณด๋‹ค ๊น”๋”ํ•ด!

URLSession ๊ตฌ์„ฑ ์š”์†Œ URLSession ๋„คํŠธ์›Œํฌ ์„ธ์…˜ ๊ด€๋ฆฌ์ž URLRequest URL, ๋ฉ”์„œ๋“œ, ํ—ค๋” ๋ฐ”๋”” ์„ค์ • URLSessionTask DataTask / UploadTask DownloadTask URLSession Configuration ํƒ€์ž„์•„์›ƒ, ์บ์‹œ ์ •์ฑ… URLSession 3๊ฐ€์ง€ ๊ธฐ๋ณธ ์„ค์ • .default (๊ธฐ๋ณธ ์„ค์ •) .ephemeral (์บ์‹œ ์—†์Œ) .background (๋ฐฑ๊ทธ๋ผ์šด๋“œ)

๐Ÿš€ URLSession ๊ธฐ๋ณธ GET ์š”์ฒญ

๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ธ GET ์š”์ฒญ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด๋ณด์ž!

Swift - ๊ธฐ๋ณธ GET ์š”์ฒญ
import Foundation

// 1. URL ์ƒ์„ฑ
guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts/1") else {
    print("์ž˜๋ชป๋œ URL์ด์—์š”!")
    return
}

// 2. URLSession ๋ฐ์ดํ„ฐ ํƒœ์Šคํฌ ์ƒ์„ฑ ๋ฐ ์‹คํ–‰
let task = URLSession.shared.dataTask(with: url) { data, response, error in
    
    // 3. ์—๋Ÿฌ ์ฒ˜๋ฆฌ
    if let error = error {
        print("์—๋Ÿฌ ๋ฐœ์ƒ: \(error.localizedDescription)")
        return
    }
    
    // 4. HTTP ์‘๋‹ต ์ƒํƒœ ์ฝ”๋“œ ํ™•์ธ
    guard let httpResponse = response as? HTTPURLResponse,
          (200...299).contains(httpResponse.statusCode) else {
        print("์„œ๋ฒ„ ์‘๋‹ต ์˜ค๋ฅ˜!")
        return
    }
    
    // 5. ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ
    guard let data = data else {
        print("๋ฐ์ดํ„ฐ๊ฐ€ ์—†์–ด์š”!")
        return
    }
    
    // 6. JSON ํŒŒ์‹ฑ
    do {
        let json = try JSONSerialization.jsonObject(with: data, options: [])
        print("๋ฐ›์€ ๋ฐ์ดํ„ฐ: \(json)")
    } catch {
        print("JSON ํŒŒ์‹ฑ ์‹คํŒจ: \(error)")
    }
}

// 7. ํƒœ์Šคํฌ ์‹œ์ž‘ (์ด๊ฑฐ ๋น ๋œจ๋ฆฌ๋ฉด ์•„๋ฌด๊ฒƒ๋„ ์•ˆ ๋จ! ์ฃผ์˜!)
task.resume()
โš ๏ธ ์ฃผ์˜! task.resume()์„ ํ˜ธ์ถœํ•ด์•ผ ์‹ค์ œ๋กœ ์š”์ฒญ์ด ์‹œ์ž‘๋ผ. ์ด๊ฑฐ ๋น ๋œจ๋ฆฌ๊ณ  "์™œ ์•ˆ ๋˜์ง€?" ํ•˜๋Š” ์ดˆ๋ณด์ž๋“ค์ด ์—„์ฒญ ๋งŽ์•„ ๐Ÿ˜‚

๐Ÿ—๏ธ Codable๋กœ JSON ํŒŒ์‹ฑํ•˜๊ธฐ

JSONSerialization๋„ ์ข‹์ง€๋งŒ, Swift์˜ Codable ํ”„๋กœํ† ์ฝœ์„ ์“ฐ๋ฉด ํ›จ์”ฌ ์šฐ์•„ํ•˜๊ฒŒ JSON์„ ๋‹ค๋ฃฐ ์ˆ˜ ์žˆ์–ด.
ํƒ€์ž… ์•ˆ์ „์„ฑ๋„ ๋ณด์žฅ๋˜๊ณ , ์ฝ”๋“œ๋„ ํ›จ์”ฌ ๊น”๋”ํ•ด์ง€๊ฑฐ๋“ !

Swift - Codable ๋ชจ๋ธ ์ •์˜
// ์„œ๋ฒ„์—์„œ ๋ฐ›์„ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ ์ •์˜
struct Post: Codable {
    let id: Int
    let userId: Int
    let title: String
    let body: String
}

struct User: Codable {
    let id: Int
    let name: String
    let email: String
    let username: String
}

// ๋ฐฐ์—ด๋กœ ๋ฐ›์„ ๋•Œ
// [Post] ์ด๋Ÿฐ ์‹์œผ๋กœ ์‚ฌ์šฉ
Swift - Codable + URLSession ์กฐํ•ฉ
func fetchPost(id: Int, completion: @escaping (Result<Post, Error>) -> Void) {
    
    guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts/\(id)") else {
        completion(.failure(NetworkError.invalidURL))
        return
    }
    
    URLSession.shared.dataTask(with: url) { data, response, error in
        
        if let error = error {
            completion(.failure(error))
            return
        }
        
        guard let httpResponse = response as? HTTPURLResponse,
              (200...299).contains(httpResponse.statusCode) else {
            completion(.failure(NetworkError.invalidResponse))
            return
        }
        
        guard let data = data else {
            completion(.failure(NetworkError.noData))
            return
        }
        
        do {
            let decoder = JSONDecoder()
            // ์„œ๋ฒ„๊ฐ€ snake_case๋ฅผ ์“ด๋‹ค๋ฉด ์ด ์„ค์ • ์ถ”๊ฐ€!
            decoder.keyDecodingStrategy = .convertFromSnakeCase
            let post = try decoder.decode(Post.self, from: data)
            completion(.success(post))
        } catch {
            completion(.failure(error))
        }
        
    }.resume()
}

// ์ปค์Šคํ…€ ์—๋Ÿฌ ํƒ€์ž…
enum NetworkError: Error {
    case invalidURL
    case invalidResponse
    case noData
    case decodingError
}
๐Ÿ’ก Result ํƒ€์ž… ํ™œ์šฉ! Swift์˜ Result<Success, Failure> ํƒ€์ž…์„ ์“ฐ๋ฉด ์„ฑ๊ณต/์‹คํŒจ ์ผ€์ด์Šค๋ฅผ ๋ช…ํ™•ํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด. ์ฝ”๋“œ๊ฐ€ ํ›จ์”ฌ ์ฝ๊ธฐ ์ข‹์•„์ง€๊ฑฐ๋“ !

๐Ÿ“ค POST ์š”์ฒญ โ€” ๋ฐ์ดํ„ฐ ๋ณด๋‚ด๊ธฐ

GET์€ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›๋Š” ๊ฑฐ๊ณ , POST๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์„œ๋ฒ„๋กœ ๋ณด๋‚ด๋Š” ๊ฑฐ์•ผ. ์กฐ๊ธˆ ๋” ์„ค์ •์ด ํ•„์š”ํ•ด!

Swift - POST ์š”์ฒญ
func createPost(title: String, body: String, userId: Int, 
                completion: @escaping (Result<Post, Error>) -> Void) {
    
    guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else {
        completion(.failure(NetworkError.invalidURL))
        return
    }
    
    // URLRequest ์„ค์ •
    var request = URLRequest(url: url)
    request.httpMethod = "POST"  // ๋ฉ”์„œ๋“œ ์„ค์ •
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    request.setValue("application/json", forHTTPHeaderField: "Accept")
    
    // ๋ณด๋‚ผ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ์ฒด
    struct NewPost: Encodable {
        let title: String
        let body: String
        let userId: Int
    }
    
    let newPost = NewPost(title: title, body: body, userId: userId)
    
    do {
        let encoder = JSONEncoder()
        request.httpBody = try encoder.encode(newPost)
    } catch {
        completion(.failure(error))
        return
    }
    
    URLSession.shared.dataTask(with: request) { data, response, error in
        
        if let error = error {
            completion(.failure(error))
            return
        }
        
        guard let data = data else {
            completion(.failure(NetworkError.noData))
            return
        }
        
        do {
            let post = try JSONDecoder().decode(Post.self, from: data)
            completion(.success(post))
        } catch {
            completion(.failure(error))
        }
        
    }.resume()
}

โšก async/await โ€” ํ˜„๋Œ€์ ์ธ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ

Swift 5.5๋ถ€ํ„ฐ ๋„์ž…๋œ async/await๋Š” ๋น„๋™๊ธฐ ์ฝ”๋“œ๋ฅผ ๋™๊ธฐ ์ฝ”๋“œ์ฒ˜๋Ÿผ ์ฝ๊ธฐ ์‰ฝ๊ฒŒ ๋งŒ๋“ค์–ด์ค˜.
์ฝœ๋ฐฑ ์ง€์˜ฅ(callback hell)์—์„œ ํ•ด๋ฐฉ๋˜๋Š” ๋А๋‚Œ์ด๋ž„๊นŒ? ๐Ÿ˜
iOS 15+ ์ด์ƒ์„ ํƒ€๊ฒŸ์œผ๋กœ ํ•œ๋‹ค๋ฉด ์ ๊ทน ํ™œ์šฉํ•ด๋ด!

๐Ÿ˜ต ์ฝœ๋ฐฑ ๋ฐฉ์‹ (์˜ˆ์ „)

fetchUser { user in

fetchPosts(user) { posts in

fetchComments(posts) { comments in

// ์ฝœ๋ฐฑ ์ง€์˜ฅ ๐Ÿ˜ฑ

}

}

}
๐Ÿ˜Ž async/await ๋ฐฉ์‹ (ํ˜„๋Œ€)

let user = await fetchUser()

let posts = await fetchPosts(user)

let comments = await fetchComments(posts)

// ํ›จ์”ฌ ๊น”๋”! ๐ŸŽ‰
Swift - async/await + URLSession
// async ํ•จ์ˆ˜ ์ •์˜
func fetchPost(id: Int) async throws -> Post {
    
    guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts/\(id)") else {
        throw NetworkError.invalidURL
    }
    
    // iOS 15+์—์„œ URLSession์ด async/await๋ฅผ ๋„ค์ดํ‹ฐ๋ธŒ ์ง€์›!
    let (data, response) = try await URLSession.shared.data(from: url)
    
    guard let httpResponse = response as? HTTPURLResponse,
          (200...299).contains(httpResponse.statusCode) else {
        throw NetworkError.invalidResponse
    }
    
    let post = try JSONDecoder().decode(Post.self, from: data)
    return post
}

// ์‚ฌ์šฉํ•˜๋Š” ๊ณณ (SwiftUI View๋‚˜ ViewModel์—์„œ)
Task {
    do {
        let post = try await fetchPost(id: 1)
        print("์ œ๋ชฉ: \(post.title)")
    } catch {
        print("์—๋Ÿฌ: \(error)")
    }
}

// POST ์š”์ฒญ๋„ async/await๋กœ!
func createPost(newPost: NewPost) async throws -> Post {
    
    guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else {
        throw NetworkError.invalidURL
    }
    
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    request.httpBody = try JSONEncoder().encode(newPost)
    
    let (data, _) = try await URLSession.shared.data(for: request)
    return try JSONDecoder().decode(Post.self, from: data)
}
๐Ÿ’ก iOS ๋ฒ„์ „ ์ฃผ์˜! URLSession.shared.data(from:) async ๋ฒ„์ „์€ iOS 15.0+๋ถ€ํ„ฐ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•ด. iOS 14 ์ดํ•˜๋ฅผ ์ง€์›ํ•ด์•ผ ํ•œ๋‹ค๋ฉด withCheckedThrowingContinuation์œผ๋กœ ๋ž˜ํ•‘ํ•˜๊ฑฐ๋‚˜ ์ฝœ๋ฐฑ ๋ฐฉ์‹์„ ์จ์•ผ ํ•ด.

๐Ÿ”„ withCheckedThrowingContinuation โ€” ๊ตฌ๋ฒ„์ „ ํ˜ธํ™˜

iOS 14 ์ดํ•˜๋„ ์ง€์›ํ•ด์•ผ ํ•œ๋‹ค๋ฉด ์ด ๋ฐฉ๋ฒ•์œผ๋กœ ๊ธฐ์กด ์ฝœ๋ฐฑ์„ async/await๋กœ ๋ณ€ํ™˜ํ•  ์ˆ˜ ์žˆ์–ด:

Swift - ์ฝœ๋ฐฑ์„ async๋กœ ๋ณ€ํ™˜
func fetchPostCompat(id: Int) async throws -> Post {
    return try await withCheckedThrowingContinuation { continuation in
        // ๊ธฐ์กด ์ฝœ๋ฐฑ ๋ฐฉ์‹ ํ•จ์ˆ˜ ํ˜ธ์ถœ
        fetchPost(id: id) { result in
            switch result {
            case .success(let post):
                continuation.resume(returning: post)
            case .failure(let error):
                continuation.resume(throwing: error)
            }
        }
    }
}

๐Ÿ›๏ธ ๋„คํŠธ์›Œํฌ ๋ ˆ์ด์–ด ์•„ํ‚คํ…์ฒ˜ ์„ค๊ณ„

์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ๋„คํŠธ์›Œํฌ ์ฝ”๋“œ๋ฅผ ์—ฌ๊ธฐ์ €๊ธฐ ํฉ๋ฟŒ๋ฆฌ๋ฉด ์•ˆ ๋ผ.
์ž˜ ์„ค๊ณ„๋œ ๋„คํŠธ์›Œํฌ ๋ ˆ์ด์–ด๋ฅผ ๋งŒ๋“ค์–ด์•ผ ์œ ์ง€๋ณด์ˆ˜๋„ ์‰ฝ๊ณ , ํ…Œ์ŠคํŠธ๋„ ๊ฐ€๋Šฅํ•ด์ง€๊ฑฐ๋“ !
์‹ค๋ฌด์—์„œ ๋งŽ์ด ์“ฐ๋Š” ํŒจํ„ด์„ ๋ณด์—ฌ์ค„๊ฒŒ.

๋„คํŠธ์›Œํฌ ๋ ˆ์ด์–ด ์•„ํ‚คํ…์ฒ˜ View / ViewModel UI ๋ ˆ์ด์–ด APIService (Protocol) ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ๋ ˆ์ด์–ด NetworkManager URLSession ๋ž˜ํผ URLSession Apple ๋„คํŠธ์›Œํฌ ํ”„๋ ˆ์ž„์›Œํฌ ํ™”๋ฉด ํ‘œ์‹œ API ์ •์˜ ์š”์ฒญ ์ฒ˜๋ฆฌ ์‹ค์ œ ํ†ต์‹  SwiftUI/UIKit Protocol Singleton Foundation
Swift - ํ”„๋กœํ† ์ฝœ ๊ธฐ๋ฐ˜ ๋„คํŠธ์›Œํฌ ๋ ˆ์ด์–ด
// MARK: - Endpoint ํ”„๋กœํ† ์ฝœ
protocol Endpoint {
    var baseURL: String { get }
    var path: String { get }
    var method: HTTPMethod { get }
    var headers: [String: String]? { get }
    var parameters: [String: Any]? { get }
    var body: Encodable? { get }
}

enum HTTPMethod: String {
    case get = "GET"
    case post = "POST"
    case put = "PUT"
    case patch = "PATCH"
    case delete = "DELETE"
}

// MARK: - ๊ธฐ๋ณธ๊ฐ’ ์ œ๊ณต (Extension)
extension Endpoint {
    var baseURL: String { return "https://api.example.com" }
    var headers: [String: String]? { return ["Content-Type": "application/json"] }
    var parameters: [String: Any]? { return nil }
    var body: Encodable? { return nil }
}

// MARK: - ์‹ค์ œ API ์—”๋“œํฌ์ธํŠธ ์ •์˜
enum PostEndpoint: Endpoint {
    case getPost(id: Int)
    case getPosts
    case createPost(title: String, body: String, userId: Int)
    case deletePost(id: Int)
    
    var path: String {
        switch self {
        case .getPost(let id): return "/posts/\(id)"
        case .getPosts: return "/posts"
        case .createPost: return "/posts"
        case .deletePost(let id): return "/posts/\(id)"
        }
    }
    
    var method: HTTPMethod {
        switch self {
        case .getPost, .getPosts: return .get
        case .createPost: return .post
        case .deletePost: return .delete
        }
    }
    
    var body: Encodable? {
        switch self {
        case .createPost(let title, let body, let userId):
            return ["title": title, "body": body, "userId": userId]
        default:
            return nil
        }
    }
}

// MARK: - NetworkManager
class NetworkManager {
    static let shared = NetworkManager()
    private let session: URLSession
    
    private init() {
        let config = URLSessionConfiguration.default
        config.timeoutIntervalForRequest = 30
        config.timeoutIntervalForResource = 60
        self.session = URLSession(configuration: config)
    }
    
    func request<T: Decodable>(_ endpoint: Endpoint) async throws -> T {
        
        // URL ๊ตฌ์„ฑ
        guard let url = URL(string: endpoint.baseURL + endpoint.path) else {
            throw NetworkError.invalidURL
        }
        
        var urlRequest = URLRequest(url: url)
        urlRequest.httpMethod = endpoint.method.rawValue
        
        // ํ—ค๋” ์„ค์ •
        endpoint.headers?.forEach { key, value in
            urlRequest.setValue(value, forHTTPHeaderField: key)
        }
        
        // ๋ฐ”๋”” ์„ค์ •
        if let body = endpoint.body {
            urlRequest.httpBody = try JSONEncoder().encode(body)
        }
        
        // ์š”์ฒญ ์‹คํ–‰
        let (data, response) = try await session.data(for: urlRequest)
        
        guard let httpResponse = response as? HTTPURLResponse else {
            throw NetworkError.invalidResponse
        }
        
        // ์ƒํƒœ ์ฝ”๋“œ ์ฒ˜๋ฆฌ
        switch httpResponse.statusCode {
        case 200...299:
            break
        case 401:
            throw NetworkError.unauthorized
        case 404:
            throw NetworkError.notFound
        case 500...599:
            throw NetworkError.serverError(httpResponse.statusCode)
        default:
            throw NetworkError.unknown(httpResponse.statusCode)
        }
        
        return try JSONDecoder().decode(T.self, from: data)
    }
}

// MARK: - ์‚ฌ์šฉ ์˜ˆ์‹œ
class PostViewModel: ObservableObject {
    @Published var posts: [Post] = []
    @Published var isLoading = false
    @Published var errorMessage: String?
    
    func loadPosts() async {
        isLoading = true
        defer { isLoading = false }
        
        do {
            posts = try await NetworkManager.shared.request(PostEndpoint.getPosts)
        } catch {
            errorMessage = error.localizedDescription
        }
    }
}

๐Ÿ”ฅ Alamofire โ€” ๋„คํŠธ์›Œํ‚น์„ ๋” ์‰ฝ๊ฒŒ!

์ž, ์ด์ œ ๋งŽ์€ iOS ๊ฐœ๋ฐœ์ž๋“ค์ด ์‚ฌ๋ž‘ํ•˜๋Š” Alamofire๋ฅผ ์•Œ์•„๋ณผ ์‹œ๊ฐ„์ด์•ผ! ๐ŸŽ‰
Alamofire๋Š” URLSession์„ ๊ธฐ๋ฐ˜์œผ๋กœ ๋งŒ๋“ค์–ด์ง„ ์„œ๋“œํŒŒํ‹ฐ ๋„คํŠธ์›Œํ‚น ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์•ผ.
GitHub ์Šคํƒ€๊ฐ€ 4๋งŒ ๊ฐœ ์ด์ƒ์ธ ์—„์ฒญ๋‚œ ์ธ๊ธฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ง€!

URLSession๋ณด๋‹ค ํ›จ์”ฌ ๊ฐ„๊ฒฐํ•œ ์ฝ”๋“œ๋กœ ๊ฐ™์€ ์ž‘์—…์„ ํ•  ์ˆ˜ ์žˆ์–ด.
ํŠนํžˆ ๋ณต์žกํ•œ ์š”์ฒญ, ํŒŒ์ผ ์—…๋กœ๋“œ, ์ธ์ฆ ์ฒ˜๋ฆฌ ๋“ฑ์—์„œ ๋น›์„ ๋ฐœํ•ด.

URLSession vs Alamofire ๋น„๊ต ๐Ÿ”— URLSession Apple ๊ธฐ๋ณธ ์ œ๊ณต โœ… ์™ธ๋ถ€ ์˜์กด์„ฑ ์—†์Œ โœ… Apple ๊ณต์‹ ์ง€์› โš ๏ธ ์ฝ”๋“œ๊ฐ€ ๋‹ค์†Œ ์žฅํ™ฉํ•จ โš ๏ธ ๋ณต์žกํ•œ ๊ธฐ๋Šฅ ์ง์ ‘ ๊ตฌํ˜„ โœ… ์ตœ์‹  async/await ์ง€์› ๐Ÿ”ฅ Alamofire ์„œ๋“œํŒŒํ‹ฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ โœ… ๊ฐ„๊ฒฐํ•˜๊ณ  ์ฝ๊ธฐ ์‰ฌ์šด ์ฝ”๋“œ โœ… ํŒŒ์ผ ์—…๋กœ๋“œ/๋‹ค์šด๋กœ๋“œ ํŽธ๋ฆฌ โœ… ์ธํ„ฐ์…‰ํ„ฐ, ์ธ์ฆ ์ฒ˜๋ฆฌ ๊ฐ•๋ ฅ โš ๏ธ ์™ธ๋ถ€ ์˜์กด์„ฑ ์ถ”๊ฐ€๋จ โœ… async/await ์™„๋ฒฝ ์ง€์›

๐Ÿ“ฆ Alamofire ์„ค์น˜

Swift Package Manager(SPM)๋กœ ์„ค์น˜ํ•˜๋Š” ๊ฒŒ ๊ฐ€์žฅ ์‰ฌ์›Œ:

Package.swift ๋˜๋Š” Xcode SPM ์„ค์ •
// Package.swift์— ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒฝ์šฐ
dependencies: [
    .package(
        url: "https://github.com/Alamofire/Alamofire.git",
        .upToNextMajor(from: "5.9.0")
    )
]

// Xcode์—์„œ๋Š”:
// File โ†’ Add Package Dependencies โ†’ 
// https://github.com/Alamofire/Alamofire.git ์ž…๋ ฅ
๐Ÿ’ก CocoaPods๋ฅผ ์“ด๋‹ค๋ฉด Podfile์— pod 'Alamofire', '~> 5.9' ์ถ”๊ฐ€ ํ›„ pod install!

๐Ÿš€ Alamofire ๊ธฐ๋ณธ ์‚ฌ์šฉ๋ฒ•

URLSession๊ณผ ๋น„๊ตํ•ด๋ณด๋ฉด ์–ผ๋งˆ๋‚˜ ์ฝ”๋“œ๊ฐ€ ์ค„์–ด๋“œ๋Š”์ง€ ๋ฐ”๋กœ ๋А๊ปด์งˆ ๊ฑฐ์•ผ!

Swift - Alamofire GET ์š”์ฒญ
import Alamofire

// ๊ธฐ๋ณธ GET ์š”์ฒญ - ์ด๊ฒŒ ์ „๋ถ€์•ผ! ๐Ÿ˜ฒ
AF.request("https://jsonplaceholder.typicode.com/posts/1")
    .responseDecodable(of: Post.self) { response in
        switch response.result {
        case .success(let post):
            print("์ œ๋ชฉ: \(post.title)")
        case .failure(let error):
            print("์—๋Ÿฌ: \(error)")
        }
    }

// ๋ฐฐ์—ด ๋ฐ›๊ธฐ
AF.request("https://jsonplaceholder.typicode.com/posts")
    .responseDecodable(of: [Post].self) { response in
        switch response.result {
        case .success(let posts):
            print("๊ฒŒ์‹œ๊ธ€ ์ˆ˜: \(posts.count)")
        case .failure(let error):
            print("์—๋Ÿฌ: \(error)")
        }
    }
Swift - Alamofire POST ์š”์ฒญ
// POST ์š”์ฒญ
let parameters: [String: Any] = [
    "title": "์ƒˆ ๊ฒŒ์‹œ๊ธ€",
    "body": "๋‚ด์šฉ์ž…๋‹ˆ๋‹ค",
    "userId": 1
]

AF.request(
    "https://jsonplaceholder.typicode.com/posts",
    method: .post,
    parameters: parameters,
    encoding: JSONEncoding.default,
    headers: ["Content-Type": "application/json"]
)
.responseDecodable(of: Post.self) { response in
    switch response.result {
    case .success(let post):
        print("์ƒ์„ฑ๋œ ๊ฒŒ์‹œ๊ธ€ ID: \(post.id)")
    case .failure(let error):
        print("์—๋Ÿฌ: \(error)")
    }
}

// Encodable ๊ตฌ์กฐ์ฒด ์ง์ ‘ ์‚ฌ์šฉ (๋” ํƒ€์ž… ์•ˆ์ „!)
struct CreatePostRequest: Encodable {
    let title: String
    let body: String
    let userId: Int
}

let newPost = CreatePostRequest(title: "์ œ๋ชฉ", body: "๋‚ด์šฉ", userId: 1)

AF.request(
    "https://jsonplaceholder.typicode.com/posts",
    method: .post,
    parameters: newPost,
    encoder: JSONParameterEncoder.default
)
.responseDecodable(of: Post.self) { response in
    // ์ฒ˜๋ฆฌ...
}

โšก Alamofire + async/await

Alamofire 5.5+๋ถ€ํ„ฐ async/await๋ฅผ ์™„๋ฒฝํ•˜๊ฒŒ ์ง€์›ํ•ด!

Swift - Alamofire async/await
// async/await ๋ฐฉ์‹
func fetchPost(id: Int) async throws -> Post {
    return try await AF.request(
        "https://jsonplaceholder.typicode.com/posts/\(id)"
    )
    .serializingDecodable(Post.self)
    .value
}

// ๋ฐฐ์—ด ๋ฐ›๊ธฐ
func fetchAllPosts() async throws -> [Post] {
    return try await AF.request(
        "https://jsonplaceholder.typicode.com/posts"
    )
    .serializingDecodable([Post].self)
    .value
}

// ์‚ฌ์šฉ
Task {
    do {
        let post = try await fetchPost(id: 1)
        print(post.title)
    } catch {
        print(error)
    }
}

๐Ÿ›ก๏ธ Alamofire ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ

Alamofire๊ฐ€ URLSession๋ณด๋‹ค ์ง„์งœ ๋น›๋‚˜๋Š” ๋ถ€๋ถ„์ด ์—ฌ๊ธฐ์•ผ.
์ธํ„ฐ์…‰ํ„ฐ, ์ธ์ฆ ์ฒ˜๋ฆฌ, ์žฌ์‹œ๋„ ๋กœ์ง ๊ฐ™์€ ๋ณต์žกํ•œ ๊ธฐ๋Šฅ๋“ค์„ ์—„์ฒญ ์‰ฝ๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๊ฑฐ๋“ !

๐Ÿ” RequestInterceptor โ€” ์ธ์ฆ ํ† ํฐ ์ž๋™ ์ฒ˜๋ฆฌ

๋ชจ๋“  ์š”์ฒญ์— Authorization ํ—ค๋”๋ฅผ ์ž๋™์œผ๋กœ ๋ถ™์ด๊ฑฐ๋‚˜,
ํ† ํฐ์ด ๋งŒ๋ฃŒ๋์„ ๋•Œ ์ž๋™์œผ๋กœ ๊ฐฑ์‹ ํ•˜๋Š” ๋กœ์ง์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด.

Swift - Alamofire Interceptor
import Alamofire

// MARK: - ์ธ์ฆ ์ธํ„ฐ์…‰ํ„ฐ
class AuthInterceptor: RequestInterceptor {
    
    private let tokenManager: TokenManager
    
    init(tokenManager: TokenManager = .shared) {
        self.tokenManager = tokenManager
    }
    
    // ์š”์ฒญ ์ „์— ์‹คํ–‰ - ํ—ค๋” ์ถ”๊ฐ€
    func adapt(_ urlRequest: URLRequest, 
               for session: Session, 
               completion: @escaping (Result<URLRequest, Error>) -> Void) {
        
        var request = urlRequest
        
        // ์•ก์„ธ์Šค ํ† ํฐ์ด ์žˆ์œผ๋ฉด ํ—ค๋”์— ์ถ”๊ฐ€
        if let token = tokenManager.accessToken {
            request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
        }
        
        completion(.success(request))
    }
    
    // ์š”์ฒญ ์‹คํŒจ ์‹œ ์žฌ์‹œ๋„ ๋กœ์ง
    func retry(_ request: Request, 
               for session: Session, 
               dueTo error: Error,
               completion: @escaping (RetryResult) -> Void) {
        
        guard let response = request.task?.response as? HTTPURLResponse,
              response.statusCode == 401 else {
            // 401์ด ์•„๋‹ˆ๋ฉด ์žฌ์‹œ๋„ ์•ˆ ํ•จ
            completion(.doNotRetry)
            return
        }
        
        // ํ† ํฐ ๊ฐฑ์‹  ์‹œ๋„
        tokenManager.refreshToken { [weak self] success in
            if success {
                // ํ† ํฐ ๊ฐฑ์‹  ์„ฑ๊ณต โ†’ ์š”์ฒญ ์žฌ์‹œ๋„
                completion(.retry)
            } else {
                // ํ† ํฐ ๊ฐฑ์‹  ์‹คํŒจ โ†’ ๋กœ๊ทธ์•„์›ƒ ์ฒ˜๋ฆฌ
                self?.handleLogout()
                completion(.doNotRetry)
            }
        }
    }
    
    private func handleLogout() {
        // ๋กœ๊ทธ์•„์›ƒ ์ฒ˜๋ฆฌ ๋กœ์ง
        NotificationCenter.default.post(name: .userDidLogout, object: nil)
    }
}

// MARK: - ํ† ํฐ ๋งค๋‹ˆ์ € (๊ฐ„๋‹จ ์˜ˆ์‹œ)
class TokenManager {
    static let shared = TokenManager()
    
    var accessToken: String? {
        return UserDefaults.standard.string(forKey: "accessToken")
    }
    
    func refreshToken(completion: @escaping (Bool) -> Void) {
        // ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์œผ๋กœ ์ƒˆ ์•ก์„ธ์Šค ํ† ํฐ ์š”์ฒญ
        // ์‹ค์ œ ๊ตฌํ˜„์—์„œ๋Š” ์„œ๋ฒ„ API ํ˜ธ์ถœ
        completion(true)
    }
}

// MARK: - ์ธํ„ฐ์…‰ํ„ฐ๊ฐ€ ์ ์šฉ๋œ Session ์ƒ์„ฑ
let authenticatedSession = Session(interceptor: AuthInterceptor())

// ์‚ฌ์šฉ
authenticatedSession.request("https://api.example.com/profile")
    .responseDecodable(of: UserProfile.self) { response in
        // ์ž๋™์œผ๋กœ ํ† ํฐ์ด ๋ถ™์–ด์„œ ์š”์ฒญ๋จ!
    }

๐Ÿ“ ํŒŒ์ผ ์—…๋กœ๋“œ

์ด๋ฏธ์ง€๋‚˜ ํŒŒ์ผ ์—…๋กœ๋“œ๋„ Alamofire๋กœ ์—„์ฒญ ์‰ฝ๊ฒŒ ํ•  ์ˆ˜ ์žˆ์–ด!

Swift - Alamofire ํŒŒ์ผ ์—…๋กœ๋“œ
// ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ (multipart/form-data)
func uploadImage(_ image: UIImage, completion: @escaping (Result<String, Error>) -> Void) {
    
    guard let imageData = image.jpegData(compressionQuality: 0.8) else {
        completion(.failure(NetworkError.noData))
        return
    }
    
    AF.upload(
        multipartFormData: { formData in
            // ์ด๋ฏธ์ง€ ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€
            formData.append(
                imageData,
                withName: "image",
                fileName: "photo.jpg",
                mimeType: "image/jpeg"
            )
            // ์ถ”๊ฐ€ ํŒŒ๋ผ๋ฏธํ„ฐ
            formData.append(
                "profile".data(using: .utf8)!,
                withName: "type"
            )
        },
        to: "https://api.example.com/upload"
    )
    .uploadProgress { progress in
        // ์—…๋กœ๋“œ ์ง„ํ–‰๋ฅ  (0.0 ~ 1.0)
        print("์—…๋กœ๋“œ ์ง„ํ–‰๋ฅ : \(Int(progress.fractionCompleted * 100))%")
    }
    .responseDecodable(of: UploadResponse.self) { response in
        switch response.result {
        case .success(let uploadResponse):
            completion(.success(uploadResponse.imageURL))
        case .failure(let error):
            completion(.failure(error))
        }
    }
}

struct UploadResponse: Decodable {
    let imageURL: String
    let success: Bool
}

๐Ÿ“ฅ ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ

Swift - Alamofire ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ
func downloadFile(from url: String) {
    
    let destination: DownloadRequest.Destination = { _, _ in
        let documentsURL = FileManager.default.urls(
            for: .documentDirectory, 
            in: .userDomainMask
        )[0]
        let fileURL = documentsURL.appendingPathComponent("downloaded_file.pdf")
        return (fileURL, [.removePreviousFile, .createIntermediateDirectories])
    }
    
    AF.download(url, to: destination)
        .downloadProgress { progress in
            print("๋‹ค์šด๋กœ๋“œ ์ง„ํ–‰๋ฅ : \(Int(progress.fractionCompleted * 100))%")
        }
        .response { response in
            if let error = response.error {
                print("๋‹ค์šด๋กœ๋“œ ์‹คํŒจ: \(error)")
            } else if let fileURL = response.fileURL {
                print("๋‹ค์šด๋กœ๋“œ ์™„๋ฃŒ! ์ €์žฅ ์œ„์น˜: \(fileURL)")
            }
        }
}

๐Ÿ”„ EventMonitor โ€” ์š”์ฒญ/์‘๋‹ต ๋กœ๊น…

๋””๋ฒ„๊น…ํ•  ๋•Œ ๋ชจ๋“  ๋„คํŠธ์›Œํฌ ์š”์ฒญ์„ ๋กœ๊ทธ๋กœ ์ฐ๊ณ  ์‹ถ๋‹ค๋ฉด EventMonitor๋ฅผ ์จ๋ด!

Swift - ๋„คํŠธ์›Œํฌ ๋กœ๊ฑฐ
class NetworkLogger: EventMonitor {
    
    let queue = DispatchQueue(label: "com.app.networkLogger")
    
    func requestDidFinish(_ request: Request) {
        print("\n๐ŸŒ ===== ๋„คํŠธ์›Œํฌ ์š”์ฒญ =====")
        print("๐Ÿ“ URL: \(request.request?.url?.absoluteString ?? "์—†์Œ")")
        print("๐Ÿ“‹ Method: \(request.request?.httpMethod ?? "์—†์Œ")")
        
        if let headers = request.request?.allHTTPHeaderFields {
            print("๐Ÿ“จ Headers: \(headers)")
        }
        
        if let body = request.request?.httpBody,
           let bodyString = String(data: body, encoding: .utf8) {
            print("๐Ÿ“ฆ Body: \(bodyString)")
        }
    }
    
    func request<Value>(_ request: DataRequest, 
                         didParseResponse response: DataResponse<Value, AFError>) {
        print("\n๐Ÿ“ฌ ===== ๋„คํŠธ์›Œํฌ ์‘๋‹ต =====")
        print("๐Ÿ“Š Status: \(response.response?.statusCode ?? 0)")
        
        if let data = response.data,
           let jsonString = String(data: data, encoding: .utf8) {
            print("๐Ÿ“„ Response: \(jsonString.prefix(500))") // ์ฒ˜์Œ 500์ž๋งŒ
        }
        print("===========================\n")
    }
}

// ๋กœ๊ฑฐ๊ฐ€ ์ ์šฉ๋œ Session
let loggingSession = Session(eventMonitors: [NetworkLogger()])

๐Ÿ—๏ธ Alamofire ๊ธฐ๋ฐ˜ ์‹ค์ „ ๋„คํŠธ์›Œํฌ ๋ ˆ์ด์–ด

์ž, ์ด์ œ ์ง„์งœ ์‹ค๋ฌด์—์„œ ์“ธ ์ˆ˜ ์žˆ๋Š” ์ˆ˜์ค€์˜ ๋„คํŠธ์›Œํฌ ๋ ˆ์ด์–ด๋ฅผ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ.
ํ”„๋กœํ† ์ฝœ ์ง€ํ–ฅ ์„ค๊ณ„ + Alamofire + async/await์˜ ์กฐํ•ฉ์ด์•ผ!

Swift - ์‹ค์ „ Alamofire ๋„คํŠธ์›Œํฌ ๋ ˆ์ด์–ด
import Alamofire
import Foundation

// MARK: - API ๋ผ์šฐํ„ฐ (Router Pattern)
enum APIRouter: URLRequestConvertible {
    
    // ์—”๋“œํฌ์ธํŠธ ์ผ€์ด์Šค๋“ค
    case login(email: String, password: String)
    case getPosts
    case getPost(id: Int)
    case createPost(title: String, body: String)
    case updatePost(id: Int, title: String, body: String)
    case deletePost(id: Int)
    case uploadImage(data: Data)
    
    // ๊ธฐ๋ณธ URL
    static let baseURL = "https://api.example.com/v1"
    
    // HTTP ๋ฉ”์„œ๋“œ
    var method: HTTPMethod {
        switch self {
        case .login, .createPost, .uploadImage:
            return .post
        case .getPosts, .getPost:
            return .get
        case .updatePost:
            return .put
        case .deletePost:
            return .delete
        }
    }
    
    // ๊ฒฝ๋กœ
    var path: String {
        switch self {
        case .login:
            return "/auth/login"
        case .getPosts:
            return "/posts"
        case .getPost(let id):
            return "/posts/\(id)"
        case .createPost:
            return "/posts"
        case .updatePost(let id, _, _):
            return "/posts/\(id)"
        case .deletePost(let id):
            return "/posts/\(id)"
        case .uploadImage:
            return "/media/upload"
        }
    }
    
    // ํŒŒ๋ผ๋ฏธํ„ฐ
    var parameters: Parameters? {
        switch self {
        case .login(let email, let password):
            return ["email": email, "password": password]
        case .createPost(let title, let body):
            return ["title": title, "body": body]
        case .updatePost(_, let title, let body):
            return ["title": title, "body": body]
        default:
            return nil
        }
    }
    
    // URLRequest ์ƒ์„ฑ
    func asURLRequest() throws -> URLRequest {
        let url = try APIRouter.baseURL.asURL().appendingPathComponent(path)
        var request = URLRequest(url: url)
        request.method = method
        request.timeoutInterval = 30
        
        // ๊ณตํ†ต ํ—ค๋”
        request.setValue("application/json", forHTTPHeaderField: "Accept")
        request.setValue("iOS", forHTTPHeaderField: "X-Platform")
        
        // ํŒŒ๋ผ๋ฏธํ„ฐ ์ธ์ฝ”๋”ฉ
        switch method {
        case .get:
            return try URLEncoding.default.encode(request, with: parameters)
        default:
            return try JSONEncoding.default.encode(request, with: parameters)
        }
    }
}

// MARK: - API ์„œ๋น„์Šค ํ”„๋กœํ† ์ฝœ
protocol APIServiceProtocol {
    func login(email: String, password: String) async throws -> LoginResponse
    func getPosts() async throws -> [Post]
    func getPost(id: Int) async throws -> Post
    func createPost(title: String, body: String) async throws -> Post
    func deletePost(id: Int) async throws
}

// MARK: - API ์„œ๋น„์Šค ๊ตฌํ˜„
class APIService: APIServiceProtocol {
    
    static let shared = APIService()
    private let session: Session
    
    private init() {
        let interceptor = AuthInterceptor()
        let logger = NetworkLogger()
        
        session = Session(
            interceptor: interceptor,
            eventMonitors: [logger]
        )
    }
    
    // ์ œ๋„ค๋ฆญ ์š”์ฒญ ๋ฉ”์„œ๋“œ
    private func request<T: Decodable>(_ router: APIRouter) async throws -> T {
        return try await session.request(router)
            .validate(statusCode: 200...299)
            .serializingDecodable(T.self)
            .value
    }
    
    // ์‘๋‹ต ์—†๋Š” ์š”์ฒญ (DELETE ๋“ฑ)
    private func requestVoid(_ router: APIRouter) async throws {
        let response = await session.request(router)
            .validate(statusCode: 200...299)
            .serializingData()
            .response
        
        if let error = response.error {
            throw error
        }
    }
    
    // MARK: - ์‹ค์ œ API ๋ฉ”์„œ๋“œ๋“ค
    func login(email: String, password: String) async throws -> LoginResponse {
        return try await request(.login(email: email, password: password))
    }
    
    func getPosts() async throws -> [Post] {
        return try await request(.getPosts)
    }
    
    func getPost(id: Int) async throws -> Post {
        return try await request(.getPost(id: id))
    }
    
    func createPost(title: String, body: String) async throws -> Post {
        return try await request(.createPost(title: title, body: body))
    }
    
    func deletePost(id: Int) async throws {
        try await requestVoid(.deletePost(id: id))
    }
}

// MARK: - ์‘๋‹ต ๋ชจ๋ธ๋“ค
struct LoginResponse: Decodable {
    let accessToken: String
    let refreshToken: String
    let user: User
}

// MARK: - ViewModel์—์„œ ์‚ฌ์šฉ
@MainActor
class PostListViewModel: ObservableObject {
    @Published var posts: [Post] = []
    @Published var isLoading = false
    @Published var errorMessage: String?
    
    private let apiService: APIServiceProtocol
    
    init(apiService: APIServiceProtocol = APIService.shared) {
        self.apiService = apiService
    }
    
    func loadPosts() async {
        isLoading = true
        errorMessage = nil
        
        do {
            posts = try await apiService.getPosts()
        } catch let error as AFError {
            handleAFError(error)
        } catch {
            errorMessage = error.localizedDescription
        }
        
        isLoading = false
    }
    
    private func handleAFError(_ error: AFError) {
        switch error {
        case .responseValidationFailed(let reason):
            switch reason {
            case .unacceptableStatusCode(let code):
                errorMessage = "์„œ๋ฒ„ ์˜ค๋ฅ˜ (์ฝ”๋“œ: \(code))"
            default:
                errorMessage = "์‘๋‹ต ๊ฒ€์ฆ ์‹คํŒจ"
            }
        case .sessionTaskFailed(let error):
            if (error as NSError).code == NSURLErrorNotConnectedToInternet {
                errorMessage = "์ธํ„ฐ๋„ท ์—ฐ๊ฒฐ์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”"
            } else {
                errorMessage = "๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜: \(error.localizedDescription)"
            }
        default:
            errorMessage = error.localizedDescription
        }
    }
}

๐Ÿงช ๋„คํŠธ์›Œํฌ ์ฝ”๋“œ ํ…Œ์ŠคํŠธํ•˜๊ธฐ

์‹ค๋ฌด์—์„œ ์ง„์งœ ์ค‘์š”ํ•œ ๋ถ€๋ถ„์ด์•ผ. ๋„คํŠธ์›Œํฌ ์ฝ”๋“œ๋ฅผ ํ…Œ์ŠคํŠธํ•˜๋ ค๋ฉด Mock ๊ฐ์ฒด๋ฅผ ํ™œ์šฉํ•ด์•ผ ํ•ด.
์‹ค์ œ ์„œ๋ฒ„์— ์˜์กดํ•˜์ง€ ์•Š๊ณ  ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ์–ด์„œ ๋น ๋ฅด๊ณ  ์•ˆ์ •์ ์ด๊ฑฐ๋“ !

Swift - Mock์„ ํ™œ์šฉํ•œ ํ…Œ์ŠคํŠธ
import XCTest
@testable import MyApp

// MARK: - Mock API ์„œ๋น„์Šค
class MockAPIService: APIServiceProtocol {
    
    // ํ…Œ์ŠคํŠธ์šฉ ๋”๋ฏธ ๋ฐ์ดํ„ฐ
    var mockPosts: [Post] = [
        Post(id: 1, userId: 1, title: "ํ…Œ์ŠคํŠธ ๊ฒŒ์‹œ๊ธ€", body: "๋‚ด์šฉ"),
        Post(id: 2, userId: 1, title: "๋‘ ๋ฒˆ์งธ ๊ฒŒ์‹œ๊ธ€", body: "๋‚ด์šฉ2")
    ]
    
    var shouldThrowError = false
    var mockError: Error = NetworkError.serverError(500)
    
    func login(email: String, password: String) async throws -> LoginResponse {
        if shouldThrowError { throw mockError }
        return LoginResponse(
            accessToken: "mock_token",
            refreshToken: "mock_refresh",
            user: User(id: 1, name: "ํ…Œ์ŠคํŠธ์œ ์ €", email: email, username: "test")
        )
    }
    
    func getPosts() async throws -> [Post] {
        if shouldThrowError { throw mockError }
        return mockPosts
    }
    
    func getPost(id: Int) async throws -> Post {
        if shouldThrowError { throw mockError }
        guard let post = mockPosts.first(where: { $0.id == id }) else {
            throw NetworkError.notFound
        }
        return post
    }
    
    func createPost(title: String, body: String) async throws -> Post {
        if shouldThrowError { throw mockError }
        let newPost = Post(id: mockPosts.count + 1, userId: 1, title: title, body: body)
        mockPosts.append(newPost)
        return newPost
    }
    
    func deletePost(id: Int) async throws {
        if shouldThrowError { throw mockError }
        mockPosts.removeAll { $0.id == id }
    }
}

// MARK: - ViewModel ํ…Œ์ŠคํŠธ
class PostListViewModelTests: XCTestCase {
    
    var viewModel: PostListViewModel!
    var mockService: MockAPIService!
    
    override func setUp() {
        super.setUp()
        mockService = MockAPIService()
        viewModel = PostListViewModel(apiService: mockService)
    }
    
    // ๊ฒŒ์‹œ๊ธ€ ๋กœ๋“œ ์„ฑ๊ณต ํ…Œ์ŠคํŠธ
    func testLoadPostsSuccess() async {
        await viewModel.loadPosts()
        
        XCTAssertEqual(viewModel.posts.count, 2)
        XCTAssertNil(viewModel.errorMessage)
        XCTAssertFalse(viewModel.isLoading)
    }
    
    // ๊ฒŒ์‹œ๊ธ€ ๋กœ๋“œ ์‹คํŒจ ํ…Œ์ŠคํŠธ
    func testLoadPostsFailure() async {
        mockService.shouldThrowError = true
        
        await viewModel.loadPosts()
        
        XCTAssertTrue(viewModel.posts.isEmpty)
        XCTAssertNotNil(viewModel.errorMessage)
        XCTAssertFalse(viewModel.isLoading)
    }
}
๐Ÿ’ก ํ”„๋กœํ† ์ฝœ ๊ธฐ๋ฐ˜ ์„ค๊ณ„์˜ ์ง„์งœ ์ด์œ ! ์œ„์—์„œ APIServiceProtocol์„ ๋งŒ๋“  ์ด์œ ๊ฐ€ ๋ฐ”๋กœ ์ด๊ฑฐ์•ผ. ํ”„๋กœํ† ์ฝœ์ด ์žˆ์œผ๋ฉด Mock ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค์–ด์„œ ์‹ค์ œ ์„œ๋ฒ„ ์—†์ด๋„ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ์–ด. ์˜์กด์„ฑ ์ฃผ์ž…(DI)์˜ ํ•ต์‹ฌ์ด์ง€!

๐ŸŒ ์‹ค์ „ ์‹œ๋‚˜๋ฆฌ์˜ค: ํŽ˜์ด์ง€๋„ค์ด์…˜ ๊ตฌํ˜„

์‹ค์ œ ์•ฑ์—์„œ ์ž์ฃผ ์“ฐ์ด๋Š” ๋ฌดํ•œ ์Šคํฌ๋กค(ํŽ˜์ด์ง€๋„ค์ด์…˜)์„ ๊ตฌํ˜„ํ•ด๋ณผ๊ฒŒ.
์ด๊ฑด ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ๋„ ์ฝ˜ํ…์ธ  ๋ชฉ๋ก์„ ๋ถˆ๋Ÿฌ์˜ฌ ๋•Œ ๊ผญ ํ•„์š”ํ•œ ๊ธฐ๋Šฅ์ด์•ผ!

Swift - ํŽ˜์ด์ง€๋„ค์ด์…˜ ViewModel
@MainActor
class PaginatedPostViewModel: ObservableObject {
    
    @Published var posts: [Post] = []
    @Published var isLoading = false
    @Published var isLoadingMore = false
    @Published var hasMorePages = true
    @Published var errorMessage: String?
    
    private var currentPage = 1
    private let pageSize = 20
    private var isFetching = false
    
    // ์ฒ˜์Œ ๋กœ๋“œ
    func loadInitial() async {
        guard !isFetching else { return }
        
        currentPage = 1
        posts = []
        hasMorePages = true
        
        await fetchPosts(page: currentPage)
    }
    
    // ๋‹ค์Œ ํŽ˜์ด์ง€ ๋กœ๋“œ (๋ฌดํ•œ ์Šคํฌ๋กค)
    func loadMore() async {
        guard !isFetching, hasMorePages else { return }
        
        currentPage += 1
        await fetchPosts(page: currentPage)
    }
    
    private func fetchPosts(page: Int) async {
        isFetching = true
        
        if page == 1 {
            isLoading = true
        } else {
            isLoadingMore = true
        }
        
        defer {
            isFetching = false
            isLoading = false
            isLoadingMore = false
        }
        
        do {
            let newPosts = try await fetchPostsFromAPI(page: page, size: pageSize)
            
            if page == 1 {
                posts = newPosts
            } else {
                posts.append(contentsOf: newPosts)
            }
            
            // ๋ฐ›์€ ๋ฐ์ดํ„ฐ๊ฐ€ pageSize๋ณด๋‹ค ์ ์œผ๋ฉด ๋งˆ์ง€๋ง‰ ํŽ˜์ด์ง€
            hasMorePages = newPosts.count == pageSize
            
        } catch {
            errorMessage = error.localizedDescription
            if page > 1 {
                currentPage -= 1 // ์‹คํŒจ ์‹œ ํŽ˜์ด์ง€ ๋กค๋ฐฑ
            }
        }
    }
    
    private func fetchPostsFromAPI(page: Int, size: Int) async throws -> [Post] {
        // Alamofire๋กœ ํŽ˜์ด์ง€๋„ค์ด์…˜ API ํ˜ธ์ถœ
        let parameters: Parameters = [
            "page": page,
            "size": size
        ]
        
        return try await AF.request(
            "https://api.example.com/posts",
            parameters: parameters
        )
        .serializingDecodable([Post].self)
        .value
    }
}

// SwiftUI์—์„œ ์‚ฌ์šฉ
struct PostListView: View {
    @StateObject private var viewModel = PaginatedPostViewModel()
    
    var body: some View {
        List {
            ForEach(viewModel.posts) { post in
                PostRow(post: post)
                    .onAppear {
                        // ๋งˆ์ง€๋ง‰ ์•„์ดํ…œ์ด ๋ณด์ด๋ฉด ๋‹ค์Œ ํŽ˜์ด์ง€ ๋กœ๋“œ
                        if post.id == viewModel.posts.last?.id {
                            Task { await viewModel.loadMore() }
                        }
                    }
            }
            
            if viewModel.isLoadingMore {
                ProgressView()
                    .frame(maxWidth: .infinity)
            }
        }
        .task {
            await viewModel.loadInitial()
        }
    }
}

๐Ÿ”’ ๋ณด์•ˆ ๊ณ ๋ ค์‚ฌํ•ญ

๋„คํŠธ์›Œํ‚น ์ฝ”๋“œ๋ฅผ ์งค ๋•Œ ๋ณด์•ˆ๋„ ์ ˆ๋Œ€ ๋น ๋œจ๋ฆฌ๋ฉด ์•ˆ ๋ผ!
ํŠนํžˆ iOS ์•ฑ์—์„œ ์ค‘์š”ํ•œ ๋ณด์•ˆ ํฌ์ธํŠธ๋“ค์„ ์•Œ์•„๋ณด์ž.

๐Ÿ”‘ ๋ฏผ๊ฐํ•œ ์ •๋ณด ๊ด€๋ฆฌ

API ํ‚ค, ์‹œํฌ๋ฆฟ ํ‚ค ๊ฐ™์€ ๋ฏผ๊ฐํ•œ ์ •๋ณด๋Š” ์ ˆ๋Œ€ ์ฝ”๋“œ์— ํ•˜๋“œ์ฝ”๋”ฉํ•˜๋ฉด ์•ˆ ๋ผ!
Keychain์„ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜, ์„œ๋ฒ„์—์„œ ๋™์ ์œผ๋กœ ๋ฐ›์•„์˜ค๋Š” ๋ฐฉ์‹์„ ์จ์•ผ ํ•ด.

Swift - Keychain ํ™œ์šฉ
import Security

class KeychainManager {
    static let shared = KeychainManager()
    
    func save(key: String, value: String) {
        let data = value.data(using: .utf8)!
        
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key,
            kSecValueData as String: data
        ]
        
        SecItemDelete(query as CFDictionary) // ๊ธฐ์กด ๊ฐ’ ์‚ญ์ œ
        SecItemAdd(query as CFDictionary, nil)
    }
    
    func load(key: String) -> String? {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key,
            kSecReturnData as String: true,
            kSecMatchLimit as String: kSecMatchLimitOne
        ]
        
        var result: AnyObject?
        SecItemCopyMatching(query as CFDictionary, &result)
        
        guard let data = result as? Data else { return nil }
        return String(data: data, encoding: .utf8)
    }
    
    func delete(key: String) {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key
        ]
        SecItemDelete(query as CFDictionary)
    }
}

// ์‚ฌ์šฉ
KeychainManager.shared.save(key: "accessToken", value: "your_token_here")
let token = KeychainManager.shared.load(key: "accessToken")

๐Ÿ›ก๏ธ SSL Pinning

์ค‘๊ฐ„์ž ๊ณต๊ฒฉ(MITM)์„ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด SSL Pinning์„ ์ ์šฉํ•  ์ˆ˜ ์žˆ์–ด.
Alamofire์—์„œ๋Š” ServerTrustManager๋กœ ์‰ฝ๊ฒŒ ๊ตฌํ˜„ ๊ฐ€๋Šฅํ•ด!

Swift - SSL Pinning with Alamofire
// SSL Pinning ์„ค์ •
let evaluators: [String: ServerTrustEvaluating] = [
    "api.example.com": PinnedCertificatesTrustEvaluator(
        certificates: Bundle.main.af.certificates
    )
]

let serverTrustManager = ServerTrustManager(evaluators: evaluators)

let secureSession = Session(
    serverTrustManager: serverTrustManager
)

// ์ด์ œ ์ด session์œผ๋กœ ์š”์ฒญํ•˜๋ฉด ์ธ์ฆ์„œ ๊ฒ€์ฆ์ด ์ž๋™์œผ๋กœ ๋จ!

๐Ÿ“‹ ATS (App Transport Security)

iOS๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ HTTPS๋งŒ ํ—ˆ์šฉํ•ด (ATS ์ •์ฑ…).
HTTP๋ฅผ ์จ์•ผ ํ•œ๋‹ค๋ฉด Info.plist์— ์˜ˆ์™ธ๋ฅผ ์ถ”๊ฐ€ํ•ด์•ผ ํ•˜๋Š”๋ฐ,
๊ฐ€๋Šฅํ•˜๋ฉด ๋ชจ๋“  ํ†ต์‹ ์€ HTTPS๋ฅผ ์“ฐ๋Š” ๊ฒŒ ๋งž์•„!

Info.plist - ATS ์˜ˆ์™ธ ์„ค์ • (๊ผญ ํ•„์š”ํ•œ ๊ฒฝ์šฐ๋งŒ!)
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSExceptionDomains</key>
    <dict>
        <key>api.example.com</key>
        <dict>
            <key>NSExceptionAllowsInsecureHTTPLoads</key>
            <true/>
        </dict>
    </dict>
</dict>

<!-- ์ „์ฒด HTTP ํ—ˆ์šฉ์€ ์ ˆ๋Œ€ ํ•˜์ง€ ๋งˆ! (๋ณด์•ˆ ์œ„ํ—˜) -->
<!-- NSAllowsArbitraryLoads: true โ† ์ด๊ฑด ์•ฑ์Šคํ† ์–ด ์‹ฌ์‚ฌ์—์„œ๋„ ๋ฌธ์ œ๋จ -->

โšก ์„ฑ๋Šฅ ์ตœ์ ํ™” ํŒ

โšก ๋„คํŠธ์›Œํฌ ์„ฑ๋Šฅ ์ตœ์ ํ™” ํฌ์ธํŠธ ๐Ÿ’พ ์บ์‹ฑ URLCache ํ™œ์šฉ ์ค‘๋ณต ์š”์ฒญ ๋ฐฉ์ง€ ์˜คํ”„๋ผ์ธ ์ง€์› ๐Ÿ”„ ์š”์ฒญ ์ทจ์†Œ Task ์ทจ์†Œ ์ง€์› ํ™”๋ฉด ์ดํƒˆ ์‹œ ๋ถˆํ•„์š” ์š”์ฒญ ์ œ๊ฑฐ ๐Ÿ“ฆ ๋ฐฐ์น˜ ์š”์ฒญ ์—ฌ๋Ÿฌ ์š”์ฒญ ๋ฌถ๊ธฐ async let ํ™œ์šฉ ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ ๐Ÿ—œ๏ธ ๋ฐ์ดํ„ฐ ์••์ถ• Gzip ์••์ถ• ์š”์ฒญ ์ด๋ฏธ์ง€ ์ตœ์ ํ™” ํŽ˜์ด๋กœ๋“œ ์ตœ์†Œํ™”

๐Ÿ”„ ์š”์ฒญ ์ทจ์†Œ (Task Cancellation)

Swift - ์š”์ฒญ ์ทจ์†Œ ์ฒ˜๋ฆฌ
class SearchViewModel: ObservableObject {
    @Published var results: [Post] = []
    private var searchTask: Task<Void, Never>?
    
    func search(query: String) {
        // ์ด์ „ ๊ฒ€์ƒ‰ ์ทจ์†Œ
        searchTask?.cancel()
        
        searchTask = Task {
            // ๋””๋ฐ”์šด์‹ฑ (300ms ๋Œ€๊ธฐ)
            try? await Task.sleep(nanoseconds: 300_000_000)
            
            guard !Task.isCancelled else { return }
            
            do {
                let posts = try await searchPosts(query: query)
                
                guard !Task.isCancelled else { return }
                
                await MainActor.run {
                    self.results = posts
                }
            } catch is CancellationError {
                // ์ทจ์†Œ๋Š” ์—๋Ÿฌ๊ฐ€ ์•„๋‹˜
            } catch {
                print("๊ฒ€์ƒ‰ ์—๋Ÿฌ: \(error)")
            }
        }
    }
    
    private func searchPosts(query: String) async throws -> [Post] {
        return try await AF.request(
            "https://api.example.com/posts/search",
            parameters: ["q": query]
        )
        .serializingDecodable([Post].self)
        .value
    }
}

// Alamofire ์š”์ฒญ ์ทจ์†Œ
class DataFetcher {
    private var currentRequest: DataRequest?
    
    func fetchData() {
        currentRequest = AF.request("https://api.example.com/data")
            .responseDecodable(of: [Post].self) { response in
                // ์ฒ˜๋ฆฌ
            }
    }
    
    func cancelFetch() {
        currentRequest?.cancel()
    }
}

โšก ๋ณ‘๋ ฌ ์š”์ฒญ (async let)

Swift - ๋ณ‘๋ ฌ API ์š”์ฒญ
// ์ˆœ์ฐจ ์‹คํ–‰ (๋А๋ฆผ ๐Ÿ˜ด)
func loadDataSequential() async throws {
    let user = try await fetchUser(id: 1)        // ๊ธฐ๋‹ค๋ฆผ...
    let posts = try await fetchPosts(userId: 1)   // ๋˜ ๊ธฐ๋‹ค๋ฆผ...
    let comments = try await fetchComments()       // ๋˜๋˜ ๊ธฐ๋‹ค๋ฆผ...
    
    // ์ด ์‹œ๊ฐ„ = A + B + C
}

// ๋ณ‘๋ ฌ ์‹คํ–‰ (๋น ๋ฆ„ ๐Ÿš€)
func loadDataParallel() async throws {
    async let user = fetchUser(id: 1)
    async let posts = fetchPosts(userId: 1)
    async let comments = fetchComments()
    
    // ์„ธ ์š”์ฒญ์ด ๋™์‹œ์— ์‹คํ–‰๋จ!
    let (userData, postsData, commentsData) = try await (user, posts, comments)
    
    // ์ด ์‹œ๊ฐ„ = max(A, B, C)
    print("์œ ์ €: \(userData.name)")
    print("๊ฒŒ์‹œ๊ธ€: \(postsData.count)๊ฐœ")
    print("๋Œ“๊ธ€: \(commentsData.count)๊ฐœ")
}

// TaskGroup์œผ๋กœ ๋™์  ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ
func fetchMultiplePosts(ids: [Int]) async throws -> [Post] {
    return try await withThrowingTaskGroup(of: Post.self) { group in
        for id in ids {
            group.addTask {
                try await self.fetchPost(id: id)
            }
        }
        
        var posts: [Post] = []
        for try await post in group {
            posts.append(post)
        }
        return posts.sorted { $0.id < $1.id }
    }
}

๐Ÿ’พ URLCache ํ™œ์šฉ

Swift - ์บ์‹ฑ ์„ค์ •
// ์•ฑ ์‹œ์ž‘ ์‹œ ์บ์‹œ ์„ค์ • (AppDelegate ๋˜๋Š” App struct)
func setupURLCache() {
    let memoryCapacity = 50 * 1024 * 1024  // 50MB ๋ฉ”๋ชจ๋ฆฌ ์บ์‹œ
    let diskCapacity = 200 * 1024 * 1024   // 200MB ๋””์Šคํฌ ์บ์‹œ
    
    URLCache.shared = URLCache(
        memoryCapacity: memoryCapacity,
        diskCapacity: diskCapacity
    )
}

// ์บ์‹œ ์ •์ฑ… ์„ค์ •
var request = URLRequest(url: url)
request.cachePolicy = .returnCacheDataElseLoad  // ์บ์‹œ ์šฐ์„ , ์—†์œผ๋ฉด ๋„คํŠธ์›Œํฌ

// Alamofire์—์„œ ์บ์‹œ ํ™œ์šฉ
let config = URLSessionConfiguration.default
config.urlCache = URLCache.shared
config.requestCachePolicy = .useProtocolCachePolicy

let cachedSession = Session(configuration: config)

๐ŸŽฏ URLSession vs Alamofire โ€” ์–ธ์ œ ๋ญ˜ ์จ์•ผ ํ•ด?

์ด๊ฒŒ ์ง„์งœ ๋งŽ์ด ๋ฐ›๋Š” ์งˆ๋ฌธ์ด์•ผ. "URLSession ์“ธ๊นŒ์š”, Alamofire ์“ธ๊นŒ์š”?"
์ •๋‹ต์€ ์—†์–ด. ์ƒํ™ฉ์— ๋”ฐ๋ผ ๋‹ค๋ฅด๊ฑฐ๋“ . ๋‚ด ๊ธฐ์ค€์„ ๊ณต์œ ํ•ด์ค„๊ฒŒ!

โœ… URLSession์„ ์„ ํƒํ•ด์•ผ ํ•  ๋•Œ

1. ์™ธ๋ถ€ ์˜์กด์„ฑ์„ ์ตœ์†Œํ™”ํ•˜๊ณ  ์‹ถ์„ ๋•Œ
๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋ฒ„์ „ ๊ด€๋ฆฌ, ์—…๋ฐ์ดํŠธ ์ด์Šˆ ๋“ฑ์„ ํ”ผํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด URLSession์ด ๋‹ต์ด์•ผ.

2. ๊ฐ„๋‹จํ•œ ์•ฑ์ด๋‚˜ SDK๋ฅผ ๋งŒ๋“ค ๋•Œ
SDK๋‚˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ๋งŒ๋“ ๋‹ค๋ฉด ์™ธ๋ถ€ ์˜์กด์„ฑ์„ ์ถ”๊ฐ€ํ•˜๋ฉด ์•ˆ ๋ผ.

3. ์ตœ์‹  Swift ๊ธฐ๋Šฅ์„ ์ตœ๋Œ€ํ•œ ํ™œ์šฉํ•˜๊ณ  ์‹ถ์„ ๋•Œ
async/await, Actor ๋“ฑ Swift ์ตœ์‹  ๊ธฐ๋Šฅ๊ณผ ๊ฐ€์žฅ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ํ†ตํ•ฉ๋ผ.

4. ํ•™์Šต ๋ชฉ์ 
๋„คํŠธ์›Œํ‚น์˜ ๊ธฐ๋ณธ ์›๋ฆฌ๋ฅผ ์ดํ•ดํ•˜๋ ค๋ฉด URLSession๋ถ€ํ„ฐ ์‹œ์ž‘ํ•˜๋Š” ๊ฒŒ ๋งž์•„!

๐Ÿ”ฅ Alamofire๋ฅผ ์„ ํƒํ•ด์•ผ ํ•  ๋•Œ

1. ๋ณต์žกํ•œ ์ธ์ฆ ๋กœ์ง์ด ํ•„์š”ํ•  ๋•Œ
ํ† ํฐ ๊ฐฑ์‹ , ์ธํ„ฐ์…‰ํ„ฐ ๋“ฑ ๋ณต์žกํ•œ ์ธ์ฆ ์ฒ˜๋ฆฌ๊ฐ€ ํ•„์š”ํ•˜๋‹ค๋ฉด Alamofire๊ฐ€ ํ›จ์”ฌ ํŽธํ•ด.

2. ํŒŒ์ผ ์—…๋กœ๋“œ/๋‹ค์šด๋กœ๋“œ๊ฐ€ ๋งŽ์„ ๋•Œ
multipart ์—…๋กœ๋“œ, ์ง„ํ–‰๋ฅ  ์ถ”์  ๋“ฑ์ด ํ•„์š”ํ•˜๋‹ค๋ฉด Alamofire๊ฐ€ ์••๋„์ ์œผ๋กœ ํŽธ๋ฆฌํ•ด.

3. ํŒ€ ํ”„๋กœ์ ํŠธ์—์„œ ์ƒ์‚ฐ์„ฑ์ด ์ค‘์š”ํ•  ๋•Œ
ํŒ€์›๋“ค์ด ์ต์ˆ™ํ•˜๊ณ , ๋น ๋ฅธ ๊ฐœ๋ฐœ์ด ํ•„์š”ํ•˜๋‹ค๋ฉด Alamofire๊ฐ€ ์ข‹์€ ์„ ํƒ์ด์•ผ.

4. ๋ ˆ๊ฑฐ์‹œ ์ฝ”๋“œ ์œ ์ง€๋ณด์ˆ˜
์ด๋ฏธ Alamofire๋ฅผ ์“ฐ๊ณ  ์žˆ๋Š” ํ”„๋กœ์ ํŠธ๋ผ๋ฉด ๊ณ„์† ์“ฐ๋Š” ๊ฒŒ ๋งž์•„.

๐Ÿ’ก ๋‚ด ์ถ”์ฒœ! ์ƒˆ ํ”„๋กœ์ ํŠธ๋ฅผ ์‹œ์ž‘ํ•œ๋‹ค๋ฉด, iOS 15+ ํƒ€๊ฒŸ์ด๋ฉด URLSession + async/await๋กœ ์ถฉ๋ถ„ํ•ด. ๋ณต์žกํ•œ ์ธ์ฆ์ด๋‚˜ ํŒŒ์ผ ์ฒ˜๋ฆฌ๊ฐ€ ๋งŽ๋‹ค๋ฉด Alamofire๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒŒ ํ˜„๋ช…ํ•œ ์„ ํƒ์ด์•ผ. ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ iOS ์•ฑ์„ ๊ฐœ๋ฐœํ•œ๋‹ค๋ฉด ๋‘ ๊ฐ€์ง€ ๋ชจ๋‘ ์•Œ์•„๋‘๋ฉด ์–ด๋–ค ํ”„๋กœ์ ํŠธ์—๋„ ๋Œ€์‘ํ•  ์ˆ˜ ์žˆ์–ด!

๐Ÿ› ์ž์ฃผ ๋งŒ๋‚˜๋Š” ์—๋Ÿฌ์™€ ํ•ด๊ฒฐ๋ฒ•

๊ฐœ๋ฐœํ•˜๋‹ค ๋ณด๋ฉด ๋ฐ˜๋“œ์‹œ ๋งŒ๋‚˜๊ฒŒ ๋˜๋Š” ์—๋Ÿฌ๋“ค์ด ์žˆ์–ด.
๋ฏธ๋ฆฌ ์•Œ์•„๋‘๋ฉด ์‚ฝ์งˆ ์‹œ๊ฐ„์„ ์—„์ฒญ ์ค„์ผ ์ˆ˜ ์žˆ์–ด!

์—๋Ÿฌ์›์ธํ•ด๊ฒฐ๋ฒ•
NSURLErrorNotConnectedToInternet์ธํ„ฐ๋„ท ์—ฐ๊ฒฐ ์—†์ŒNetwork ์ƒํƒœ ๋ชจ๋‹ˆํ„ฐ๋ง ์ถ”๊ฐ€
NSURLErrorTimedOut์š”์ฒญ ํƒ€์ž„์•„์›ƒtimeoutInterval ์กฐ์ •
DecodingErrorJSON ํŒŒ์‹ฑ ์‹คํŒจ๋ชจ๋ธ ๊ตฌ์กฐ ํ™•์ธ, keyDecodingStrategy ์„ค์ •
401 Unauthorized์ธ์ฆ ํ† ํฐ ๋งŒ๋ฃŒํ† ํฐ ๊ฐฑ์‹  ๋กœ์ง ์ถ”๊ฐ€
ATS ์˜ค๋ฅ˜HTTP ์š”์ฒญ ์ฐจ๋‹จHTTPS ์‚ฌ์šฉ ๋˜๋Š” ATS ์˜ˆ์™ธ ์„ค์ •
task.resume() ๋ˆ„๋ฝ์š”์ฒญ ์‹œ์ž‘ ์•ˆ ๋จ๋ฐ˜๋“œ์‹œ .resume() ํ˜ธ์ถœ
๋ฉ”์ธ ์Šค๋ ˆ๋“œ UI ์—…๋ฐ์ดํŠธ ๊ฒฝ๊ณ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šค๋ ˆ๋“œ์—์„œ UI ์ ‘๊ทผDispatchQueue.main.async ๋˜๋Š” @MainActor ์‚ฌ์šฉ
Swift - ๋„คํŠธ์›Œํฌ ์ƒํƒœ ๋ชจ๋‹ˆํ„ฐ๋ง (NWPathMonitor)
import Network

class NetworkMonitor: ObservableObject {
    static let shared = NetworkMonitor()
    
    @Published var isConnected = true
    @Published var connectionType: ConnectionType = .unknown
    
    private let monitor = NWPathMonitor()
    private let queue = DispatchQueue(label: "NetworkMonitor")
    
    enum ConnectionType {
        case wifi, cellular, ethernet, unknown
    }
    
    private init() {
        monitor.pathUpdateHandler = { [weak self] path in
            DispatchQueue.main.async {
                self?.isConnected = path.status == .satisfied
                
                if path.usesInterfaceType(.wifi) {
                    self?.connectionType = .wifi
                } else if path.usesInterfaceType(.cellular) {
                    self?.connectionType = .cellular
                } else if path.usesInterfaceType(.wiredEthernet) {
                    self?.connectionType = .ethernet
                } else {
                    self?.connectionType = .unknown
                }
            }
        }
        monitor.start(queue: queue)
    }
    
    deinit {
        monitor.cancel()
    }
}

// SwiftUI์—์„œ ์‚ฌ์šฉ
struct ContentView: View {
    @StateObject private var networkMonitor = NetworkMonitor.shared
    
    var body: some View {
        VStack {
            if !networkMonitor.isConnected {
                Text("โš ๏ธ ์ธํ„ฐ๋„ท ์—ฐ๊ฒฐ์ด ์—†์Šต๋‹ˆ๋‹ค")
                    .foregroundColor(.red)
                    .padding()
                    .background(Color.yellow.opacity(0.3))
                    .cornerRadius(8)
            }
            // ๋‚˜๋จธ์ง€ ์ฝ˜ํ…์ธ 
        }
    }
}

๐Ÿ“š ์ •๋ฆฌ โ€” ์˜ค๋Š˜ ๋ฐฐ์šด ๊ฒƒ๋“ค

๐Ÿ—บ๏ธ Swift ๋„คํŠธ์›Œํ‚น ํ•™์Šต ๋กœ๋“œ๋งต 1๋‹จ๊ณ„ HTTP ๊ธฐ์ดˆ + REST API ์ดํ•ด GET/POST/PUT/DELETE, ์ƒํƒœ ์ฝ”๋“œ, JSON ํ˜•์‹ 2๋‹จ๊ณ„ URLSession ๊ธฐ๋ณธ ๋งˆ์Šคํ„ฐ dataTask, Codable, Result ํƒ€์ž…, ์—๋Ÿฌ ์ฒ˜๋ฆฌ 3๋‹จ๊ณ„ async/await ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ async throws, Task, async let, TaskGroup 4๋‹จ๊ณ„ Alamofire ํ™œ์šฉ ์ธํ„ฐ์…‰ํ„ฐ, ํŒŒ์ผ ์—…๋กœ๋“œ, EventMonitor, Router ํŒจํ„ด 5๋‹จ๊ณ„ ์•„ํ‚คํ…์ฒ˜ + ํ…Œ์ŠคํŠธ + ๋ณด์•ˆ + ์„ฑ๋Šฅ ์ตœ์ ํ™”

์˜ค๋Š˜ ์ •๋ง ๋งŽ์€ ๋‚ด์šฉ์„ ๋‹ค๋ค˜์–ด! ํ•œ ๋ฒˆ์— ๋‹ค ์™ธ์šฐ๋ ค๊ณ  ํ•˜์ง€ ๋ง๊ณ ,
์‹ค์ œ๋กœ ์ฝ”๋“œ๋ฅผ ์ณ๋ณด๋ฉด์„œ ์ตํžˆ๋Š” ๊ฒŒ ๊ฐ€์žฅ ์ค‘์š”ํ•ด.

์ฒ˜์Œ์—” URLSession์œผ๋กœ ๊ฐ„๋‹จํ•œ GET ์š”์ฒญ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด์„œ,
์ ์  ๋ณต์žกํ•œ ๊ธฐ๋Šฅ๋“ค์„ ์ถ”๊ฐ€ํ•ด๋‚˜๊ฐ€๋Š” ๋ฐฉ์‹์œผ๋กœ ์—ฐ์Šตํ•ด๋ด!

๊ทธ๋ฆฌ๊ณ  ์žฌ๋Šฅ๋„ท์—์„œ iOS ๊ฐœ๋ฐœ ๊ด€๋ จ ์žฌ๋Šฅ์„ ์ฐพ์•„๋ณด๋ฉด,
์‹ค๋ฌด ๊ฒฝํ—˜์ด ์žˆ๋Š” ๊ฐœ๋ฐœ์ž๋“ค์—๊ฒŒ ์ง์ ‘ ์ฝ”๋“œ ๋ฆฌ๋ทฐ๋‚˜ ๋ฉ˜ํ† ๋ง์„ ๋ฐ›์„ ์ˆ˜๋„ ์žˆ์–ด.
ํ˜ผ์ž ๊ณต๋ถ€ํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค ํ›จ์”ฌ ๋น ๋ฅด๊ฒŒ ์„ฑ์žฅํ•  ์ˆ˜ ์žˆ๊ฑฐ๋“ ! ๐Ÿ’ช

๐ŸŽฏ ํ•ต์‹ฌ ํฌ์ธํŠธ ์š”์•ฝ

URLSession Apple ๊ธฐ๋ณธ ์ œ๊ณต, ์™ธ๋ถ€ ์˜์กด์„ฑ ์—†์Œ
Codable JSON ํŒŒ์‹ฑ์˜ ํ‘œ์ค€, ํƒ€์ž… ์•ˆ์ „์„ฑ ๋ณด์žฅ
async/await ํ˜„๋Œ€์  ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ, iOS 15+
Alamofire ๋ณต์žกํ•œ ๋„คํŠธ์›Œํ‚น์„ ๊ฐ„๊ฒฐํ•˜๊ฒŒ
Interceptor ์ธ์ฆ ํ† ํฐ ์ž๋™ ์ฒ˜๋ฆฌ
Protocol ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•œ ์„ค๊ณ„์˜ ํ•ต์‹ฌ
Mock ์„œ๋ฒ„ ์—†์ด ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ
SSL Pinning ๋ณด์•ˆ ๊ฐ•ํ™”
NWPathMonitor ๋„คํŠธ์›Œํฌ ์ƒํƒœ ๊ฐ์ง€
async let ๋ณ‘๋ ฌ ์š”์ฒญ์œผ๋กœ ์„ฑ๋Šฅ ํ–ฅ์ƒ

๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.