포스트

Dex (3)

이전 글에서 모델 구조와 CodingKeys를 정리했으니, 이번에는 직접 init에서 decoding을 더 자세히 알아본다.

코드 분석

이제 Json 구조와 함께 코드를 보면서 알아보자

Json 구조의 경우나머지는 생략하고 필요한 부분만 가져온다.

코드 역시도 필요한 부분만 가져오도록 한다.

이때 둘다 //생략 이라는 주석은 빼고 적도록 하겠다.

1. id, name

Json 구조

1
2
3
4
{
  "id": 1,
  "name": "bulbasaur"
 }

Code

Json 구조상 간단하게 Decoding을 할 수 없기에 Container를 사용해야함. (JsonDecoder 사용❌)

1
2
3
4
5
6
7
8
9
enum CodingKeys: CodingKey {
        case id
        case name
}
init(from decoder: any Decoder) throws {
  let container = try decoder.container(keyedBy: CodingKeys.self)       
  id = try container.decode(Int16.self, forKey: .id)
  name = try container.decode(String.self, forKey: .name)
}

id, name 모두 CodingKey에 있는 id, name으로 해결 가능

2. types

Json 구조

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
  "types": [
          {
              "slot": 1,
              "type": {
                  "name": "grass",
                  "url": "https://pokeapi.co/api/v2/type/12/"
              }
          },
          {
              "slot": 2,
              "type": {
                  "name": "poison",
                  "url": "https://pokeapi.co/api/v2/type/4/"
              }
          }
      ]
}

Code

우리는 types안에 있는 type의 name이 필요한 상태

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
enum CodingKeys: CodingKey {
        case types
        
        enum TypeDictionaryKeys: CodingKey {
            case type
            
            enum TypeKeys: CodingKey {
                case name
            }
        }

init(from decoder: any Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        
        var decodedTypes: [String] = []
        var typesContainer = try container.nestedUnkeyedContainer(forKey: .types)
        
        while !typesContainer.isAtEnd {
            // Decode types
            let typesDictionaryContainer = try typesContainer.nestedContainer(keyedBy: CodingKeys.TypeDictionaryKeys.self)
            let typeContainer = try typesDictionaryContainer.nestedContainer(keyedBy: CodingKeys.TypeDictionaryKeys.TypeKeys.self, forKey: .type)
            
            let type = try typeContainer.decode(String.self, forKey: .name)
            decodedTypes.append(type)
        }
        types = decodedTypes
}

우선 json을 보면 types 라는 배열안에 딕셔너리가 존재하는 구조이다.

어차피 우리는 딕셔너리 전체를 가져와서 담는게 아니기 때문에 [String] 배열을 만들어 주었다.

var decodedTypes: [String] = []

Docs를 읽어보도록 하자.

  • while의 경우
    • 배열 안의 객체들을 하나씩 꺼내서, 그 안의 특정 값(name)을 추출하는 루프
      • 추출을 한 결과는 아마도 ["grass", "poison"] 이런식으로 저장이 될 것이다.
  • 간단한 결론
    • types: 배열 안의 딕셔너리 안의 딕셔너리에서 name만 뽑기

3. stats

Json 구조

1
2
3
4
5
6
7
8
9
10
11
12
13
{
  "stats": [
          {
              "base_stat": 45,
          },
          {
              "base_stat": 49,
          },
          {
              "base_stat": 49,
          }
      ]
}

Code

Docs는 여기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
enum CodingKeys: CodingKey {
        case stats
        
        enum StatDictionaryKeys: CodingKey {
            case baseStat
        }
      }

init(from decoder: any Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        
        var decodedStats: [Int16] = []
        var statsContainer = try container.nestedUnkeyedContainer(forKey: .stats)
        while !statsContainer.isAtEnd {
            let statsDictionaryContainer = try statsContainer.nestedContainer(keyedBy: CodingKeys.StatDictionaryKeys.self)
            let stat = try statsDictionaryContainer.decode(Int16.self, forKey: .baseStat)
            decodedStats.append(stat)
        }

        hp = decodedStats[0]
        attack =  decodedStats[1]
        defense =  decodedStats[2]
        specialAttack =  decodedStats[3]
        specialDefense =  decodedStats[4]
        speed =  decodedStats[5]
}

types와 비슷한 구조

대신 stats에는

Image

0~5 까지 총 6개의 배열이 존재한다.

그 배열은 각각의 능력치 수치가 존재한다.

하지만 굳이 name을 가져올 필요없이 우리가 이미

1
2
3
4
5
6
let hp: Int16
let attack: Int16
let defense: Int16
let specialAttack: Int16
let specialDefense: Int16
let speed: Int16

이런식으로 변수들을 만들어 두었기에

각 배열에 해다하는 값만 넣어주면 된다. 그래서 baseStat만 디코딩해서 가져오는것.

그리고 stats라는 큰 배열안에 baseStats 라는 딕셔너리가 있으므로

  • nestedUnkeyedContainer → nestedContainer -> decode 작업 이렇게 진행된다.
    • 이때 nestedContainer -> decode 작업은 while 안에서 값이 없을때까지 순환
  • 간단한 결론
    • stats: 배열 안의 baseStat 값만 순서대로 뽑기 (key 없이 순서 고정)
  • ⚠️ 참고: stats 디코딩 시 주의점
    • decodedStats[0] ~ [5]는 PokeAPI에서 항상 6개 항목(hp, attack, …, speed)을 제공한다는 전제하에 사용된다.
    • 하지만 만약 decodedStats[6] 등 존재하지 않는 인덱스를 접근하려 하면 index out of range 에러 발생.
    • 또한 JSON 구조 자체가 변경되거나 누락되면 디코딩 도중 DecodingError가 발생할 수 있음.

4. sprites

Json 구조

1
2
3
4
5
6
7
8
{
  "sprites": {
          "front_default": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/1.png",
          "front_female": null,
          "front_shiny": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/shiny/1.png",
        
          }
}

Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
enum CodingKeys: CodingKey {
        case sprites
        
        enum SpriteKeys: String, CodingKey {
            case sprite = "frontDefault"
            case shiny = "frontShiny"
        }
    }

init(from decoder: any Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    
    let spriteContainer = try container.nestedContainer(keyedBy: CodingKeys.SpriteKeys.self, forKey: .sprites)
    
    sprite = try spriteContainer.decode(URL.self, forKey: .sprite)
    shiny = try spriteContainer.decode(URL.self, forKey: .shiny)
}

Sprite는 Swift 변수명(frontDefault)과 JSON key(front_default)가 다르기 때문에, CodingKeys.SpriteKeys 안에서 rawValue를 명시하여 정확하게 매핑해야 한다.

그리고 내부가 딕셔너리기에 nestedContainer를 사용해주었다.

5. 📦 nestedContainer vs nestedUnkeyedContainer

메서드 이름용도JSON 구조 예시
nestedContainer딕셔너리(JSON 객체) 접근용{ "type": { "name": "..." } }
nestedUnkeyedContainer배열(JSON 배열) 접근용[ { "type": ... }, { "type": ... } ]

📌 핵심 개념 정리

  • 딕셔너리 → nestedContainer
  • 배열 → nestedUnkeyedContainer
  • 중첩 구조에 따라 컨테이너를 계층적으로 중첩해서 사용해야 함

예시 흐름:

  • types 구조:
    • 배열 → 딕셔너리 → 딕셔너리
  • 사용 흐름:
    • nestedUnkeyedContainernestedContainernestedContainer

🧩 요약 예시

1
2
3
let typesContainer = try container.nestedUnkeyedContainer(forKey: .types)
let typeDictContainer = try typesContainer.nestedContainer(keyedBy: ..., ...)
let typeContainer = try typeDictContainer.nestedContainer(keyedBy: ..., forKey: .type)
  • 배열 → 딕셔너리 → 딕셔너리 구조 파싱

중첩된 JSON 구조는 배열인지 딕셔너리인지에 따라 접근 방식이 달라진다. container, nestedUnkeyedContainer, nestedContainer를 JSON 구조에 맞게 계층적으로 사용해야 한다는 점을 기억하자.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.