Dynamic Key decoding

2024. 12. 21. 12:25·Programing Langauge/swift
반응형

👋 들어가기 전

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

 

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

 

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


✊ 문제발생

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

// 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

 

반응형

'Programing Langauge > swift' 카테고리의 다른 글

@frozen  (0) 2025.03.18
lazy  (1) 2025.02.04
GC vs ARC  (1) 2024.12.13
클로저와 self  (0) 2024.10.23
swift 기본 타입  (0) 2024.10.18
'Programing Langauge/swift' 카테고리의 다른 글
  • @frozen
  • lazy
  • GC vs ARC
  • 클로저와 self
Hamp
Hamp
남들에게 보여주기 부끄러운 잡다한 글을 적어 나가는 자칭 기술 블로그입니다.
  • Hamp
    Hamp의 분리수거함
    Hamp
  • 전체
    오늘
    어제
    • 분류 전체보기 (325)
      • CS (30)
        • 객체지향 (2)
        • Network (7)
        • OS (6)
        • 자료구조 (1)
        • LiveStreaming (3)
        • 이미지 (1)
        • 잡다한 질문 정리 (0)
        • Hardware (2)
        • 이론 (6)
        • 컴퓨터 그래픽스 (0)
      • Firebase (3)
      • Programing Langauge (41)
        • swift (34)
        • python (6)
        • Kotlin (1)
      • iOS (133)
        • UIKit (37)
        • Combine (1)
        • SwiftUI (33)
        • Framework (7)
        • Swift Concurrency (22)
        • Tuist (6)
        • Setting (11)
        • Modularization (1)
        • Instruments (6)
      • PS (59)
        • 프로그래머스 (24)
        • 백준 (13)
        • LeetCode (19)
        • 알고리즘 (3)
      • Git (18)
        • 명령어 (4)
        • 이론 (2)
        • hooks (1)
        • config (2)
        • action (7)
      • Shell Script (2)
      • Linux (6)
        • 명령어 (5)
      • Spring (20)
        • 어노테이션 (6)
        • 튜토리얼 (13)
      • CI-CD (4)
      • Android (0)
        • Jetpack Compose (0)
      • AI (9)
        • 이론 (9)
        • MCP (0)
  • 블로그 메뉴

    • 홈
    • 태그
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    boostcamp
    Swift
    CS
    Spring
    concurrency
    protocol
    프로그래머스
    dispatch
    lifecycle
    property
    UIKit
    dfs
    SwiftUI
    IOS
    dp
    GIT
    Tuist
    백준
    AVFoundation
    투포인터
  • 최근 댓글

  • 최근 글

  • 반응형
  • hELLO· Designed By정상우.v4.10.0
Hamp
Dynamic Key decoding
상단으로

티스토리툴바