Programing Langauge/swift

Dynamic Key decoding

Hamp 2024. 12. 21. 12:25
반응형

👋 들어가기 전

또 다시 오랜만에 포스팅을 하게 됐다.

 

지금은 익명의 프로젝트에 급작스럽게 합류하여 앱 개발을 하던 중 신선한 경험을 하게되어
그 경험을 적어보려한다. 

 

이번 시간의 주제는 동적 키 형태를 디코딩 하는 방법을 간단하게 정리해보자.


✊ 문제발생

만약 서버에서 다음과 같은 형태로 데이터를 보낸다면 우리는 어떻게 처리해야할까 ??

// Case1
{
  "data": {
    "a":100 
  }
}

// Case2
{
  "data": {
    "b": nil
  }
}

// Case3
{
  "data": {
    "c": -100
  }
}

 

어디가 불편할까 ??  data안의 key값이 계속 변하는 상황이다..


☝️서버 개발자님 "해줘"

서버 개발자님 key값 통일해주세요 ~~ 

단 칼에 거절 백엔드 상황을 들어보니 충분히 납득할만한 이유가 있었다! 
그러면 내가 해결을 해야한다는건데 애플도 이런 경우가 있겠지!


✌️ Decodable

우리는 서버로부터 데이터를 받은 후 디코딩을 진행하는데 그 때 DTO들은 맹목적으로
"Decodable" 프로토콜을 채택하는데 오늘 이 프로토콜의 진정한 힘을 써본 날인 것 같다.

public protocol Decodable {
    init(from decoder: any Decoder) throws
}

 

Decodable은 위와 같은 생성자가 있는데 이 기능을 사용하면 dynamic key 형태가 무섭지 않다.
물론 많을수록 작업량이 늘어나긴하지만 불가능하지는 않다.

 

필요한 과정을 스탭별로 알아보자.

1. CodingKeys

먼저 코딩키를 설정해 각 원소들을 가져올 준비를 한다.

struct SomeDTO: Decodable {
    var data: Int?
    var otherData: Int?

    enum CodingKeys: String, CodingKey {
        case data = "data"
    }
}

 

위 코딩키는 너무 간단하지만 실제 내가 받은 형태는 복잡하기때문에 코딩키가 필요했다.

2. Decodable init

struct SomeDTO: Decodable {
    var data: Int?
    
    enum CodingKeys: String, CodingKey {
        case data = "data"
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self) // 정의한 키로 내용 가져오기
        let dict = try container.decode([String: Int]?.self, forKey: .data) // 특정키를 해당 타입으로 디코딩
  		 self.data = dict?.values.first // 값 가져오기
	}
}

 

형태가 조금 이상하지만 우리가 지금 서버로부터 받는 형태는 data키에 [String: Int?] 형태로 데이터가
1개만 온다. 

 

만약 조금 더 복잡한 형태는 다음과 같이 진행할 수 있다.

 

😭 complex Form

아래 주식 DTO를 가정한다.

{
   "AAPL":{"quote":{},"chart":[]},
   "MSFT":{"quote":{},"chart":[]}
}

단일 DTO 정의

key는 디코딩을 위한 변수이므로 추후 사용은 하지 앟는다.

struct StockModel : Codable {
    let quote:          String
    let latestPrice:    Double
    let key:            String
    
    private enum CodingKeys: CodingKey {
        case quote
        case latestPrice
    }
    //  Init
    
    init(quote: String, latestPrice: Double, key: String) {
        self.quote = quote
        self.latestPrice = latestPrice
        self.key = key
    }
    //  Decode
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        
        guard let key = container.codingPath.first?.stringValue else {
            throw NSError(domain: "Key not found", code: 0, userInfo: nil)
        }
        
        self.quote = try container.decode(String.self, forKey: .quote)
        self.latestPrice = try container.decode(Double.self, forKey: .latestPrice)
        self.key = key
    }

Root DTO 정의

struct StockModels : Codable {
    let models: [StockModel]
    
    private struct StockKey : CodingKey {
        var stringValue: String
        init?(stringValue: String) {
            self.stringValue = stringValue
        }
        var intValue: Int?
        init?(intValue: Int) { return nil }
    }
    
    //  Init
    init(_ models: [StockModel]) {
        self.models = models
    }
    
    //  Decode
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: StockKey.self)
        
        var models: [StockModel] = []
        for key in container.allKeys {
            if let key = StockKey(stringValue: key.stringValue) {
                let model = try container.decode(StockModel.self, forKey: key)
                models.append(model)
            }
        }
        self.models = models
    }
}

👍 Nested Decode 

Decodable 프로토콜을 이용하면 복잡한 중첩형태도 쉽게 풀어낼 수 있다.

{
  "user": {
    "id": 123,
    "details": {
      "name": "John",
      "age": 30
    }
  }
}

 

struct User: Decodable {
    let id: Int
    let name: String
    let age: Int

    enum CodingKeys: String, CodingKey {
        case user
        case id
        case details
        case name
        case age
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        
        // 현재 "user" 키로 이동
        let userContainer = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: .user)
        id = try userContainer.decode(Int.self, forKey: .id)

        // "user.details" 키로 이동
        let detailsContainer = try userContainer.nestedContainer(keyedBy: CodingKeys.self, forKey: .details)
        name = try detailsContainer.decode(String.self, forKey: .name)
        age = try detailsContainer.decode(Int.self, forKey: .age)

        // 디코딩 경로 추적
        print("Current coding path: \(detailsContainer.codingPath)")
    }
}

😀 소감 및 마무리

이제 서버 개발자분들에게 부담을 덜어줄 수 있는 스킬을 배운 것 같아서 좋다.

 

Encodable을 이런식으로 사용해본적이 없지만 그 상황이 오면 반대로 하면되니 크게 문제 없겠지 ??


출처

https://gist.github.com/stammy/0872636a4c740e8a2011e57eaf09bbff

 

swift json codable with dynamic key

swift json codable with dynamic key. GitHub Gist: instantly share code, notes, and snippets.

gist.github.com

 

반응형