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"]
이런식으로 저장이 될 것이다.
- 추출을 한 결과는 아마도
- 배열 안의 객체들을 하나씩 꺼내서, 그 안의 특정 값(name)을 추출하는 루프
- 간단한 결론
- 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에는
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
구조:- 배열 → 딕셔너리 → 딕셔너리
- 사용 흐름:
nestedUnkeyedContainer
→nestedContainer
→nestedContainer
🧩 요약 예시
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 구조에 맞게 계층적으로 사용해야 한다는 점을 기억하자.