[ToTheMoon] 트러블슈팅: Bithumb API 디코딩 트러블슈팅

황석범·2025년 2월 24일

iOS

목록 보기
76/76

Bithumb API 디코딩 트러블슈팅

Bithumb API JSON을 Swift 모델로 변환하는 과정에서 발생한 문제와 해결 과정 정리


문제 상황

Bithumb API에서 제공하는 코인 시세 데이터를 Swift의 Decodable 모델로 변환하려고 했는데, 일반적인 딕셔너리 매핑 방식이 실패함

처음 시도했던 코드

struct BithumbTickersResponse: Decodable {
    let data: [String: CoinData] 

    struct CoinData: Decodable {
        let openingPrice: Double
        let closingPrice: Double
        let minPrice: Double
        let maxPrice: Double
    }
}

Bithumb API 응답 예시 (JSON)

{
    "data": {
        "BTC": {
            "opening_price": "50000000",
            "closing_price": "51000000",
            "min_price": "49500000",
            "max_price": "51500000",
            "units_traded": "100.5",
            "acc_trade_value": "5050000000",
            "prev_closing_price": "50500000",
            "units_traded_24H": "200.2",
            "acc_trade_value_24H": "10100000000",
            "fluctate_24H": "1000000",
            "fluctate_rate_24H": "2.0"
        },
        "ETH": {
            "opening_price": "3500000",
            "closing_price": "3550000",
            "min_price": "3450000",
            "max_price": "3600000",
            "units_traded": "200.3",
            "acc_trade_value": "710000000",
            "prev_closing_price": "3500000",
            "units_traded_24H": "400.6",
            "acc_trade_value_24H": "1420000000",
            "fluctate_24H": "50000",
            "fluctate_rate_24H": "1.5"
        },
        
    ......
    
    	"date": "1706700000000"
    }
}

문제 원인

  • JSON의 data 내부를 보면 date와 BTC, ETH 같은 코인 심볼이 같은 레벨에 존재
  • date는 단순 문자열이지만, BTC, ETH는 각각 CoinData라는 별도 구조체로 변환해야 함
  • 일반적인 방식으로 Dictionary<String, CoinData>로 매핑하면 date도 CoinData로 변환하려고 시도해서 디코딩 오류 발생

해결 과정

해결 방법

  • DynamicCodingKeys를 이용한 수동 디코딩

date 값은 고정된 키이므로 별도로 처리.
나머지 코인 데이터(BTC, ETH 등)는 동적 키이므로 DynamicCodingKeys를 활용하여 디코딩

해결 코드

struct BithumbTickersResponse: Decodable {
    let data: CoinDataWrapper
    
    struct CoinDataWrapper: Codable {
        let date: String
        let coins: [String: CoinData]
        
        enum CodingKeys: String, CodingKey {
            case date
        }

        init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            self.date = try container.decode(String.self, forKey: .date) // ✅ date는 별도로 디코딩

            let dynamicContainer = try decoder.container(keyedBy: DynamicCodingKeys.self)
            var tempCoins = [String: CoinData]()

            for key in dynamicContainer.allKeys {
                if key.stringValue != "date" { // "date"는 제외하고 나머지 코인 데이터만 저장
                    let coinData = try dynamicContainer.decode(CoinData.self, forKey: key)
                    tempCoins[key.stringValue] = coinData
                }
            }
            self.coins = tempCoins
        }
    }
}

코인 데이터 구조체

struct CoinData: Decodable {
    let openingPrice: Double
    let closingPrice: Double
    let minPrice: Double
    let maxPrice: Double
    let unitsTraded: Double
    let accTradeValue: Double
    let prevClosingPrice: Double
    let unitsTraded24H: Double
    let accTradeValue24H: Double
    let fluctate24H: Double
    let fluctateRate24H: Double
    
    enum CodingKeys: String, CodingKey {
        case openingPrice = "opening_price"
        case closingPrice = "closing_price"
        case minPrice = "min_price"
        case maxPrice = "max_price"
        case unitsTraded = "units_traded"
        case accTradeValue = "acc_trade_value"
        case prevClosingPrice = "prev_closing_price"
        case unitsTraded24H = "units_traded_24H"
        case accTradeValue24H = "acc_trade_value_24H"
        case fluctate24H = "fluctate_24H"
        case fluctateRate24H = "fluctate_rate_24H"
    }
    
    // API에서 모든 값을 문자열로 제공하므로 Double 변환 시도
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        self.openingPrice = Double(try container.decode(String.self, forKey: .openingPrice)) ?? 0.0
        self.closingPrice = Double(try container.decode(String.self, forKey: .closingPrice)) ?? 0.0
        self.minPrice = Double(try container.decode(String.self, forKey: .minPrice)) ?? 0.0
        self.maxPrice = Double(try container.decode(String.self, forKey: .maxPrice)) ?? 0.0
        self.unitsTraded = Double(try container.decode(String.self, forKey: .unitsTraded)) ?? 0.0
        self.accTradeValue = Double(try container.decode(String.self, forKey: .accTradeValue)) ?? 0.0
        self.prevClosingPrice = Double(try container.decode(String.self, forKey: .prevClosingPrice)) ?? 0.0
        self.unitsTraded24H = Double(try container.decode(String.self, forKey: .unitsTraded24H)) ?? 0.0
        self.accTradeValue24H = Double(try container.decode(String.self, forKey: .accTradeValue24H)) ?? 0.0
        self.fluctate24H = Double(try container.decode(String.self, forKey: .fluctate24H)) ?? 0.0
        self.fluctateRate24H = Double(try container.decode(String.self, forKey: .fluctateRate24H)) ?? 0.0
    }
}

JSON 데이터를 디코딩할 때, 일반적으로 키(key)는 미리 정의된 CodingKeys를 통해 매핑됩니다. 하지만, JSON의 키가 동적으로 변하는 경우에는 미리 정의할 수 없기 때문에 일반적인 방식으로 디코딩이 어렵습니다.


container의 개념과 동작 방식

  • Swift의 Decoder는 JSON 데이터를 디코딩할 때 내부적으로 container를 사용합니다.
  • container는 키-값 구조를 해석하는 역할을 하며, keyed, unkeyed, singleValue 세 가지 타입이 있습니다.

1. Keyed Container (키가 있는 딕셔너리 형태)

  • keyedBy: CodingKeys.self를 사용하여 키-값 쌍을 기반으로 데이터를 가져옴.
  • JSON이 딕셔너리 구조일 때 사용.

일반적인 KeyedDecodingContainer 사용

struct ExampleData: Decodable {
    let id: Int
    let name: String
    
    enum CodingKeys: String, CodingKey {
        case id, name
    }
}

let jsonData = """
{
    "id": 123,
    "name": "Alice"
}
""".data(using: .utf8)!

let decodedData = try JSONDecoder().decode(ExampleData.self, from: jsonData)
print(decodedData.id)   // 123
print(decodedData.name) // Alice

KeyedDecodingContainer 내부 동작

  1. container = try decoder.container(keyedBy: CodingKeys.self) 를 생성하면 JSON 내부에서 "id"와 "name"이라는 키를 찾음.
  2. id 키가 존재하면, container.decode(Int.self, forKey: .id)를 호출하여 값을 Int로 변환.
  3. name 키가 존재하면, container.decode(String.self, forKey: .name)를 호출하여 값을 String으로 변환.

2. Unkeyed Container (배열 형태)

  • JSON이 배열일 때 사용.
  • decode(T.self)를 호출할 때마다 하나씩 값을 가져옴.

배열을 처리하는 UnkeyedDecodingContainer

struct Item: Decodable {
    let name: String
}

struct ResponseData: Decodable {
    let items: [Item]
}

let jsonData = """
{
    "items": [
        { "name": "Apple" },
        { "name": "Banana" }
    ]
}
""".data(using: .utf8)!

let decodedData = try JSONDecoder().decode(ResponseData.self, from: jsonData)
print(decodedData.items[0].name) // Apple

3. Single Value Container (단일 값 처리)

  • JSON이 단일 값(숫자, 문자열, 불리언 등) 일 때 사용.
  • decode(T.self)를 호출하면 단일 값을 가져옴.

SingleValueDecodingContainer

let jsonData = """
"Hello, world!"
""".data(using: .utf8)!

let decodedString = try JSONDecoder().decode(String.self, from: jsonData)
print(decodedString) // Hello, world!

JSON의 동적 키 처리 방법

1. DynamicCodingKeys를 이용한 해결 방법

동적 키를 처리하려면, 키 값을 미리 정의할 수 없기 때문에 DynamicCodingKeys를 사용하여 키를 동적으로 가져와야 합니다.

DynamicCodingKeys 구현

struct DynamicCodingKeys: CodingKey {
    var stringValue: String
    init?(stringValue: String) { self.stringValue = stringValue }
    var intValue: Int? { return nil }
    init?(intValue: Int) { return nil }
}

DynamicCodingKeys는 CodingKey 프로토콜을 준수하여 동적으로 키를 생성할 수 있도록 함. stringValue를 통해 키 값을 가져오고, intValue는 사용하지 않으므로 nil 반환


2. 해결과정의 해결코드 동작 방식

  1. 고정된 키(date)를 먼저 처리
let container = try decoder.container(keyedBy: CodingKeys.self)
self.date = try container.decode(String.self, forKey: .date)

container를 생성하여 date 값을 가져옴

  1. 나머지 코인 데이터(BTC, ETH .....)를 동적으로 처리
let dynamicContainer = try decoder.container(keyedBy: DynamicCodingKeys.self)

dynamicContainer.allKeys를 호출하여 모든 키를 가져옴

  1. 동적 키를 활용하여 코인 데이터를 저장
for key in dynamicContainer.allKeys {
    if key.stringValue != "date" {
        let coinData = try dynamicContainer.decode(CoinData.self, forKey: key)
        tempCoins[key.stringValue] = coinData
    }
}

BTC, ETH 같은 키들은 DynamicCodingKeys를 통해 동적으로 디코딩됨, date는 이미 처리했으므로 제외

profile
iOS 공부중...

0개의 댓글