iOS/Swift

[Swift] 네트워크 통신(2) - URLSession(GET)과 Decodable 프로토콜

YEN_ 2024. 1. 4. 12:18

2024.01.04 - [iOS/SWIFT] - [Swift] 네트워크 통신(1) - URL과 REST API

 

🩵 URLSession

데이터를 다운로드하거나 업로드하고, 웹 서버와의 통신을 위한 HTTP 요청을 생성하고 처리하는 데 활용됩니다.

  1. URLSession은 비동기 방식으로 작동하며
  2. 네트워크 작업이 완료되면 클로저를 사용하여 결과를 처리합니다.

 

 

✔️ GET

import Foundation

// URLSession 인스턴스 생성
let session = URLSession.shared

// URL 생성
if let url = URL(string: "https://api.example.com/data") {
    // URLSessionDataTask를 사용하여 비동기적으로 데이터 요청
    let task = session.dataTask(with: url) { (data, response, error) in
        if let error = error {
            print("Error: \(error)")
        } else if let data = data {
            // 데이터를 받아온 후 처리하는 로직을 작성
            print("Received data: \(data)")
        }
    }
    
    // 네트워크 요청 시작
    task.resume()
}

 

 

  • URLSession의 인스턴스를 생성하는데, shared 속성을 사용하여 기본 세션을 공유합니다.
    • 네트워크 요청과 응답을 처리하는 데 사용하기 위한 앱 전체에서 사용할 수 있는 공유 세션을 가져오는 것
let session = URLSession.shared

 

 

  • URL 객체를 생성하는 부분입니다.
  • 여기서는 주어진 문자열을 사용하여 URL을 만들고, 옵셔널 바인딩을 통해 안전하게 처리하고 있습니다.
if let url = URL(string: "https://api.example.com/data")

 

 

  • URLSessiondataTask 메서드를 사용하여 비동기적으로 데이터를 요청하는 작업을 생성합니다.
  • 클로저 내부에서는 네트워크 응답에 대한 처리를 수행하고 있습니다.
let task = session.dataTask(with: url) { (data, response, error) in 
    .....
}

 

 

  • 네트워크 요청이 시작되는 부분입니다.
  • resume 메서드를 호출함으로써 요청이 서버로 전송되고, 클로저 내부의 코드가 비동기적으로 실행됩니다.
task.resume()

 

 

 

🩵 Decodable 프로토콜

JSON과 같은 외부 데이터를 Swift의 데이터 모델, 객체로 디코딩할 수 있도록 해주는 프로토콜입니다.

 

 

Decodable을 준수하는 객체는 외부 데이터를 해석하고 그 데이터를 객체의 프로퍼티로 매핑할 수 있어야 합니다.

네트워크에서 데이터를 받아올 때, 그 데이터를 앱 내의 데이터 모델로 변환하는 과정을 의미합니다.

 

Decodable을 준수하는 객체의 프로퍼티는 JSON 데이터의 키에 해당하는 이름을 가져야하며,

해당 프로퍼티의 타입은 JSON 데이터에서 예상되는 타입과 일치해야 합니다.

이를 통해 JSON 데이터의 각 필드가 Swift 객체의 프로퍼티로 매핑되어 사용될 수 있게 됩니다.

 

struct User: Decodable {
    let id: Int
    let name: String	
    
    public enum CodingKeys: String, CodingKey {
     	case id = "key"
      	case name
    }

    // Decoding - 디코딩
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decode(Int.self, forKey: .id)
        name = try container.decode(String.self, forKey: .name)
    }
}

 

외부 데이터에서는 key로 사용되는 값을 우리가 만들 객체에서는 id라는 이름의 프로퍼티로 사용할 수 있게 연결시켜주는 부분이 추가되어 있습니다.

name 프로퍼티에 해당하는 JSON키는 그대로 name에 연결시켰습니다.

 

이런 것을 매핑이라고 합니다.

 

만약 이 매핑이 필요하지 않고, 프로퍼티 이름과 JSON 키의 이름이 동일하다면 별도의 CodingKeys를 선언할 필요가 없습니다.

 

 

 

🩵 JSON → Model 변환하기

URLSession으로 받은 Data 타입을 JSONDecoder 클래스를 사용하여 데이터를 모델 객체로 디코딩합니다.

do {
    let user = try JSONDecoder().decode(User.self, from: data)
    print("디코딩된 사용자: \(user)")
} catch {
    print("디코딩 에러: \(error)")
}

 

  1. JSONDecoder().decode(User.self, from: data)
    • SONDecoder 인스턴스를 생성하고, 해당 인스턴스를 사용하여 User 타입으로 디코딩합니다.
    • decode 메서드는 디코딩이 성공하면 해당 타입의 객체를 반환합니다.
  2. let user = try JSONDecoder().decode(User.self, from: data)
    • 디코딩 작업은 예외가 발생할 수 있으므로 try 키워드를 사용합니다.
    • 디코딩이 성공하면 디코딩된 User 객체가 user 상수에 할당됩니다.
  3. print("디코딩된 사용자: \(user)")
    • 디코딩이 성공한 경우, 디코딩된 사용자 객체(2번의 user 상수)를 출력합니다.
  4. catch
    • 디코딩 과정에서 에러가 발생하면 catch 블록이 실행되어 해당 에러를 출력합니다.
    • 이를 통해 디코딩 실패 시 에러에 대한 디버깅이 가능합니다.

 

 

🖋️ URLSession 을 통해 REST API 통신하기

✔️ API 정보

[GET] https://dummyjson.com/products/{ID}

  • ID의 범위: 1 ~ 100
  • Response JSON
{
    "id": 1,
    "title": "iPhone 9",
    "description": "An apple mobile which is nothing like apple",
    "price": 549,
    "discountPercentage": 12.96,
    "rating": 4.69,
    "stock": 94,
    "brand": "Apple",
    "category": "smartphones",
    "thumbnail": "https://i.dummyjson.com/data/products/1/thumbnail.jpg",
    "images": [
        "https://i.dummyjson.com/data/products/1/1.jpg",
        "https://i.dummyjson.com/data/products/1/2.jpg",
        "https://i.dummyjson.com/data/products/1/3.jpg",
        "https://i.dummyjson.com/data/products/1/4.jpg",
        "https://i.dummyjson.com/data/products/1/thumbnail.jpg"
    ]
}

 

✔️ 스토리보드

 

 

✔️ 구현 화면

 

 

 

✔️ 실습 코드

Decodable 프로토콜

더보기

 

  1. CodingKeys 열거형
    • CodingKeys 열거형은 각 프로퍼티의 이름과 실제 JSON 데이터의 키를 매핑하는 역할을 합니다.
    • 이를 통해 Swift에서 다른 이름의 프로퍼티를 사용하고 JSON에서 원하는 이름으로 디코딩할 수 있습니다.
  2. init(from decoder:) 초기화 메소드
    • 이 메소드는 Decodable 프로토콜을 따르는 객체를 생성하기 위해 필요한 디코딩 로직이 포함되어 있습니다.
    • 각 프로퍼티를 디코딩하기 위해 container를 사용하고, CodingKeys를 통해 매핑된 키를 지정하여 디코딩합니다.
  3. container
    • Decoder에서 제공하는 container를 통해 디코딩(외부 데이터에서 내부 데이터로의 변환 작업)을 수행합니다.
    • container는 키-값 쌍을 가지고 있고, 이를 통해 JSON 데이터의 각 프로퍼티에 접근합니다.
    • 그리고 해당 값을 내부 데이터 모델의 프로퍼티에 매핑할 수 있습니다.

 

 

Int, String, Double, [String] 등 각 프로퍼티에 해당하는 데이터 타입으로 디코딩을 수행하고 있습니다.

프로퍼티의 타입과 JSON 데이터의 형식이 일치해야 올바르게 디코딩됩니다.

 

struct Product: Decodable {
    
    // MARK: 프로퍼티 정의
    
    let key: Int // 원래는 id 인데 이름을 key 로 바꿔봄 (1)
    let productTitle: String // 원래는 title 인데 이름을 productTitle 로 바꿔봄 (1)
    let description: String
    let price: Int
    let discountPercentage: Double
    let rating: Double
    let stock: Int
    let brand: String
    let category: String
    let thumbnail: String
    let images: [String]
    
    // MARK: CodingKeys 열거형
    //      프로퍼티의 이름과 실제 JSON 데이터의 키를 매핑하는 역할
    //      이를 통해 Swift에서 다른 이름의 프로퍼티를 사용하고 JSON에서 원하는 이름으로 디코딩할 수 있음
    
    public enum CodingKeys: String, CodingKey {
    	// "id" 키를 사용하여 key 프로퍼티 디코딩  
        case key = "id" // 원래는 id 인데 이름을 key 로 바꿔봄 (2) 
        // "title" 키를 사용하여 productTitle 프로퍼티 디코딩
        case productTitle = "title" // 원래는 title 인데 이름을 productTitle 로 바꿔봄 (2) 
        case description
        case price
        case discountPercentage
        case rating
        case stock
        case brand
        case category
        case thumbnail
        case images
    }
    
    // MARK: init(from decoder:) 초기화 메소드
    //      Decodable 프로토콜을 따르는 객체를 생성하기 위해 init(from:) 초기화 메소드 구현
    //      각 프로퍼티를 디코딩하기 위해 container를 사용하고, CodingKeys를 통해 매핑된 키를 지정하여 디코딩
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        
        key = try container.decode(Int.self, forKey: .key) // 원래는 id 인데 이름을 key 로 바꿔봄 (3)
        productTitle = try container.decode(String.self, forKey: .productTitle) // 원래는 title 인데 이름을 productTitle 로 바꿔봄 (3)
        description = try container.decode(String.self, forKey: .description)
        price = try container.decode(Int.self, forKey: .price)
        discountPercentage = try container.decode(Double.self, forKey: .discountPercentage)
        rating = try container.decode(Double.self, forKey: .rating)
        stock = try container.decode(Int.self, forKey: .stock)
        brand = try container.decode(String.self, forKey: .brand)
        category = try container.decode(String.self, forKey: .category)
        thumbnail = try container.decode(String.self, forKey: .thumbnail)
        images = try container.decode([String].self, forKey: .images)
    }
}

 

JSON → Model 변환하기

더보기

 

  1. URL 생성 및 URLSession 인스턴스 생성:
    • URL(string: "https://dummyjson.com/products/\(productID)")를 통해 지정된 productID를 이용하여 요청할 URL을 생성합니다.
    • URLSession.shared를 사용하여 앱 전체에서 공유되는 기본 URLSession 인스턴스를 가져옵니다.
  2. URLSessionDataTask를 사용하여 비동기적으로 데이터 요청
    • URLSession.shared.dataTask(with: url)를 호출하여 비동기적으로 데이터를 요청하는 URLSessionDataTask를 생성합니다.
    • 클로저 내에서 데이터 요청이나 다른 비동기 작업이 완료된 후에 UI를 업데이트하는 목적으로 DispatchQueue.main.async를 사용하여 UI 작업을 수행합니다.
  3. 데이터 처리 및 디코딩
    • data, response, error 파라미터를 사용하여 네트워크 요청 결과를 처리합니다.
    • 만약 에러가 발생하면 에러를 출력하고 함수 실행을 종료합니다.
    • 성공적으로 데이터를 받아오면 JSONDecoder를 사용하여 Product 모델로 디코딩합니다.

 

func decodeing() {
    if let url = URL(string: "https://dummyjson.com/products/\(productID)") {
        
        // 1. URLSession 인스턴스 생성
        // 2. URLSessionDataTask를 사용하여 비동기적으로 데이터 요청
        let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
            
            // 네트워크 요청이나 다른 비동기 작업이 완료된 후에 UI를 업데이트하는 목적
            DispatchQueue.main.async {
                self.hideSkeletonView()
            }
            
            if let error = error  {
                print("Error: \(error)")
            } else if let data = data {
                // 데이터를 받아오고 처리하는 로직 작성
                do {
                    // URLSession으로 받은 Data 타입을
                    // JSONDecoder 클래스를 사용하여 데이터를 모델 객체로 디코딩
                    let product = try JSONDecoder().decode(Product.self, from: data)
                    
                    // 네트워크 요청이 완료되고 상품 정보가 성공적으로 디코딩되면 호출되는 부분
                    // 실제 데이터로 UI를 업데이트
                    DispatchQueue.main.async {
                        self.updateUIWithProduct(product)
                    }
                    
                    print("Decoded Product:")
                    print("ID: \(product.key)") // 원래는 id 인데 이름을 key 로 바꿔봄 (4)
                    print("Title: \(product.productTitle)") // 원래는 title 인데 이름을 productTitle 로 바꿔봄 (4)
                    print("Description: \(product.description)")
                    print("Price: \(product.price)")
                    print("Discount Percentage: \(product.discountPercentage)")
                    print("Rating: \(product.rating)")
                    print("Stock: \(product.stock)")
                    print("Brand: \(product.brand)")
                    print("Category: \(product.category)")
                    print("Thumbnail: \(product.thumbnail) \(type(of: product.thumbnail))")
                    print("Images: \(product.images)")
                } catch {
                    print("Decode Error: \(error)")
                }
            }
        }
        
        task.resume()
    }
}