iOS/Swift

공공 API를 통해 데이터를 가져오는 중에 발생한 오류 해결

밤새는 탐험가89 2024. 9. 21. 06:31

오류 내용

The data couldn’t be read because it isn’t in the correct format.

공공 API를 통해 데이터를 불러오는 과정에서 데이터 모델 형식이 맞지 않아 오류가 발생했습니다.

 

 

내가 설계한 데이터 모델

import Foundation

// MARK: - Main Response Model
struct ImageResponse: Codable {
    let response: ImageResponseBody
}

struct ImageResponseBody: Codable {
    let header: ResponseHeader
    let body: ImageResponseBodyContent
}

struct ResponseHeader: Codable {
    let resultCode: String
    let resultMsg: String
}

struct ImageResponseBodyContent: Codable {
    let items: ImageItems?
    let numOfRows: Int
    let pageNo: Int
    let totalCount: Int
}


struct ImageItems: Codable {
    let item: [ImageItem]?
}

// MARK: - Image Item Model
struct ImageItem: Codable {
    let contentid: String
    let originimgurl: String
    let imgname: String
    let smallimageurl: String
    let cpyrhtDivCd: String
    let serialnum: String
}

좌: 실패 / 우: 성공

 

 문제의 원인은 API로부터 받은 items 값이 빈 문자열 ""로 반환되는 반면, 사용자 데이터 모델에서는 items가 ImageItems? 구조체로 정의되어 있기 때문에 발생하는 것입니다.

 

즉, JSON 데이터의 items가 실제로 빈 문자열일 때, 이를 ImageItems?로 디코딩하려고 하면 형식이 맞지 않기 때문에 The data couldn’t be read because it isn’t in the correct format. 오류가 발생하는 것입니다.

 

이 문제를 해결하려면, items 필드가 빈 문자열일 경우 이를 적절히 처리하도록 디코딩 로직을 수정해야 합니다. 이를 위해 ImageItems?를 빈 문자열 또는 null로 올 수 있는 경우를 처리하도록 custom decode를 작성할 수 있습니다.

 

 

struct ImageResponseBodyContent: Codable {
    let items: ImageItems?
    let numOfRows: Int
    let pageNo: Int
    let totalCount: Int
    
    // 커스텀 디코딩 로직 추가
    enum CodingKeys: String, CodingKey {
        case items, numOfRows, pageNo, totalCount
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        numOfRows = try container.decode(Int.self, forKey: .numOfRows)
        pageNo = try container.decode(Int.self, forKey: .pageNo)
        totalCount = try container.decode(Int.self, forKey: .totalCount)
        
        // items를 빈 문자열인 경우 nil로 처리
        if let itemsString = try? container.decode(String.self, forKey: .items), itemsString.isEmpty {
            items = nil
        } else {
            items = try? container.decode(ImageItems.self, forKey: .items)
        }
    }
}

 

 이 방식은 items가 빈 문자열일 때 이를 nil로 처리하므로 오류가 발생하지 않도록 합니다. items가 구조체로 오지 않고 빈 문자열로 올 경우 이를 안전하게 무시하고 넘어가도록 하는 것입니다.

 

1. CodingKeys 열거형

enum CodingKeys: String, CodingKey {
    case items, numOfRows, pageNo, totalCount
}


 이 부분은 Codable 프로토콜을 따르는 구조체에서 JSON의 키와 구조체의 프로퍼티를 연결하기 위해 사용됩니다. 여기서는 items, numOfRows, pageNo, totalCount라는 네 가지 키를 설정했는데, 이는 JSON에서 이 필드들을 찾기 위한 역할을 합니다.

 

2. 커스텀 이니셜라이저 init(from:)

init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)

 

 여기서 container는 JSON 데이터의 키-값 쌍을 가져오기 위한 컨테이너입니다. 이 컨테이너를 사용해 각 키에 해당하는 값을 가져오고, JSON 데이터가 구조체와 어떻게 매핑되는지 제어할 수 있습니다.

 

 

3. 일반적인 값 디코딩

numOfRows = try container.decode(Int.self, forKey: .numOfRows)
pageNo = try container.decode(Int.self, forKey: .pageNo)
totalCount = try container.decode(Int.self, forKey: .totalCount)

 

 numOfRows, pageNo, totalCount는 Int 타입이므로 JSON에서 이 값들을 직접 가져와서 구조체의 프로퍼티에 할당합니다. 이 부분은 기본적인 디코딩 방식입니다.

 

 

4. items의 처리

if let itemsString = try? container.decode(String.self, forKey: .items), itemsString.isEmpty {
    items = nil
} else {
    items = try? container.decode(ImageItems.self, forKey: .items)
}

 

이 부분이 핵심입니다. items가 빈 문자열로 올 경우를 처리하기 위해 두 가지 방법을 사용했습니다:

  • 빈 문자열인 경우: try? container.decode(String.self, forKey: .items)을 사용하여 items 필드가 String으로 디코딩 가능한지 시도합니다. 만약 성공하고, 그 값이 빈 문자열일 경우 items = nil로 설정합니다. 즉, 빈 문자열은 안전하게 nil로 처리합니다.
  • 구조체로 올 경우: items가 실제로 구조체(ImageItems)로 올 경우, 이를 디코딩할 수 있도록 try? container.decode(ImageItems.self, forKey: .items)를 사용합니다. 이때 try?는 디코딩이 실패해도 오류를 발생시키지 않고 nil을 반환하도록 하여 안전하게 디코딩을 시도합니다.

 

🟧 CodingKeys는 기본적으로 JSON 데이터의 키 이름과 Swift 구조체의 프로퍼티 이름이 다를 때 매핑을 도와주는 역할을 합니다.

🟧 이름이 동일할 경우에도 명시적으로 사용할 수 있으며, 그럴 때는 Swift에서 자동으로 키를 매칭해줍니다.

 

 

 지금 코드에서는 JSON 데이터의 키와 구조체의 프로퍼티 이름이 동일하므로, 사실 CodingKeys는 없어도 됩니다. Swift는 기본적으로 JSON에서 "numOfRows", "pageNo", "totalCount" 같은 키들을 찾아 구조체의 동일한 이름의 프로퍼티에 자동으로 할당합니다.

그러나 CodingKeys를 명시적으로 추가한 것은:

  • 코드가 더 명확하고 읽기 쉬운 구조를 제공할 수 있기 때문입니다.
  • 나중에 JSON 키 이름을 변경해야 할 때 더 쉽게 수정할 수 있기 때문입니다.

 

예시: 이름이 다른 경우

만약 JSON에서 키 이름이 "num_of_rows"이고, Swift 구조체의 프로퍼티 이름이 numOfRows라면 CodingKeys를 사용해 이렇게 매핑해야 합니다:

enum CodingKeys: String, CodingKey {
    case numOfRows = "num_of_rows" // JSON의 "num_of_rows"를 Swift의 "numOfRows"에 매핑
    case pageNo = "page_no"
    case totalCount = "total_count"
}

현재 코드에서의 역할

지금 코드에서는 JSON과 Swift의 키 이름이 동일하므로 CodingKeys는 사실 생략 가능하지만, 명시적으로 작성한 상태입니다.

따라서:

  • 2. 열거형 CodingKeys 부분에서 외부 JSON 키 이름과 구조체의 프로퍼티 이름이 동일한 경우에는 큰 변화를 주지 않고 그대로 사용합니다.
  • 3. 값 디코딩에서는 forKey: .numOfRows를 통해 CodingKeys의 열거형 값을 이용해 JSON의 "numOfRows" 키로부터 값을 디코딩하고, Swift 구조체의 numOfRows 프로퍼티에 할당합니다.

결론적으로, 지금 코드에서는 키 이름을 바꾸지 않고 동일하게 사용하고 있습니다. 하지만 CodingKeys를 사용함으로써 나중에 JSON 키가 변경되었을 때 쉽게 대응할 수 있는 유연성을 갖추게 됩니다.