Dex (2)

 

CoreData로 부터 Fetch

그전에 Controller에서

마지막 부분에

container.viewContext.mergePolicy = NSMergePolicy.mergeByPropertyStoreTrump MergePolicy를 추가해주자.

Docs 참고

Merge Policy 설명 우선순위 사용 예시
NSMergeByPropertyObjectTrumpMergePolicy 메모리(Context)의 변경값이 저장소(Store) 값을 덮어씀 메모리 우선 사용자가 입력한 최신 값을 유지하고 싶을 때
NSMergeByPropertyStoreTrumpMergePolicy 저장소(Store)의 기존 값을 메모리(Context) 변경보다 유지 저장소 우선 서버 동기화 상태를 신뢰하고 충돌을 피하고 싶을 때
struct ContentView: View {
    @Environment(\.managedObjectContext) private var viewContext

    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \Pokemon.id, ascending: true)],
        animation: .default)
    private var pokedex: FetchedResults<Pokemon>

    var body: some View {
        NavigationView {
            List {
                ForEach(pokedex) { pokemon in
                    NavigationLink {
                        Text(pokemon.name ?? "no name")
                    } label: {
                        Text(pokemon.name ?? "no name")
                    }
                }
                
            }
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    EditButton()
                }
                ToolbarItem {
                    Button("Add Item", systemImage: "plus") {
                        
                    }
                }
            }
        }
    }
}

이렇게 기본틀을 다듬어 준다.

Image

에러가 모두 해결되고 Controller에서 만든 초기값이 현재 Preview에 보이게 된다.

CodingKeys

뭐 CodingKeys 라고 적었지만 이미 Modeling 하면서 사용했던것이기에 크게 뭐 언급할만한건 없어보인다. (이때는 몰랐다…..)

이전글에서 비슷하게 한적이 있긴한데.

struct FetchedPokemon: Decodable {
    let id: Int16
    let name: String
    let types: [String]
    let hp: Int16
    let attack: Int16
    let defense: Int16
    let specialAttack: Int16
    let specialDefense: Int16
    let speed: Int16
    let sprite: URL
    let shiny: URL
    
    enum CodingKeys: CodingKey {
        case id
        case name
        case types
        case stats
        case sprites
        
        enum TypeDictionaryKeys: CodingKey {
            case type
            
            enum TypeKeys: CodingKey {
                case name
            }
        }
        
        enum StatDictionaryKeys: CodingKey {
            case baseStat
        }
        
        enum SpriteKeys: String, CodingKey {
            case sprite = "frontDefault"
            case shiny = "frontShiny"
        }
    }
    init(from decoder: any Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    
    self.id = try container.decode(Int16.self, forKey: .id)
    self.name = try container.decode(String.self, forKey: .name)
    self.types = try container.decode([String].self, forKey: .types)
    self.hp = try container.decode(Int16.self, forKey: .hp)
    self.attack = try container.decode(Int16.self, forKey: .attack)
    self.defense = try container.decode(Int16.self, forKey: .defense)
    self.specialAttack = try container.decode(Int16.self, forKey: .specialAttack)
    self.specialDefense = try container.decode(Int16.self, forKey: .specialDefense)
    self.speed = try container.decode(Int16.self, forKey: .speed)
    self.sprite = try container.decode(URL.self, forKey: .sprite)
    self.shiny = try container.decode(URL.self, forKey: .shiny)
    }
}

여기서 눈여겨 봐야 할 것은

CondingKeys 라는 enum 안에 여러 enum을 또 세분화 한건데 이유는 json에 있다.

Image

해당 사진을 보면 이렇게 세부적으로 나뉘어져있는데

이것을 api를 통해 가져올것이므로 위와 같이 코드를 작성한 것.

Image

여기서 init구문 내 self를 모두 지워주자 이때, 한번에 지우는 방법이 있는데 Shift+Control을 누른 상태에서 이렇게 내리면 다중 선택 처럼 되어서 동시에 삭제가 가능하다.

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

        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]
        
        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)
    }

이렇게 코드를 바꿔주었따.

이부분은 생소한 부분이기에 조금 더 자세히 짚어보도록 한다.


🧬 FetchedPokemon 구조 요약

1️⃣ 왜 CodingKeys를 계층적으로 나누었는가?

중첩 JSON 구조에 정확히 접근하기 위해 각 계층마다 별도의 CodingKey enum이 필요하다.

항목 JSON 구조 예시 설명 필요한 키 enum
types [ { "type": { "name": "..." } } ] 배열 → 딕셔너리 → 내부 딕셔너리 TypeDictionaryKeys.TypeKeys
stats [ { "base_stat": ..., "stat": { "name": "..." } } ] 배열 → 딕셔너리 (기본값) → 내부 딕셔너리 (이름 등 추가 정보 포함) StatDictionaryKeys
sprites { "sprites": { "front_default": "...", "front_shiny": "...", ... } } 딕셔너리 → 여러 key → URL (Swift 속성과 JSON key 이름이 다름) SpriteKeys (with rawValue)

JSON 내부에 중첩된 배열이나 딕셔너리를 파싱하려면 Swift의 nested(Un)keyedContainer와 계층적인 CodingKey 설계가 필수적이다.


2️⃣ 왜 init(from:)을 직접 구현했는가?

자동 생성이 JSON 구조에 대응하지 못하기 때문.

  • 중첩 구조 (types, stats) → 배열 안에 딕셔너리 구조
  • 키 이름 불일치 (sprites) → "front_default" 등 JSON key가 Swift 변수명과 다름

3️⃣ 주요 흐름 정리

항목 처리 방식
id, name 상위 container에서 바로 디코딩
types 배열 → 딕셔너리 → 내부 딕셔너리 → namewhile 루프
stats 배열 → 딕셔너리 → base_stat만 추출 → while 루프
sprites 1단계 nested container (sprites) + key 이름 매핑

4️⃣ 컨테이너 사용 요약

구조 메서드 예시
딕셔너리 nestedContainer sprites, type, stat
배열 nestedUnkeyedContainer types, stats
루프 while !container.isAtEnd 반복 디코딩
누적 append() 배열에 값 추가 (types, stats)

🧩 핵심 요약

  • 중첩 JSON 구조 → CodingKeys를 계층화하여 정확히 매핑
  • 자동 생성 init은 구조 대응 불가 → 수동 구현 필수
  • types, stats, sprites는 각기 다른 방식으로 접근 및 파싱 필요
  • nested(Un)keyedContainer, while 루프, rawValue 사용 이해 필수

📦 JSONDecoder.decode vs Decoder.container - 예제 기반 비교

✅ JSONDecoder.decode 방식

예제 JSON (단순 구조):

{
  "name": "Bulbasaur",
  "hp": 45,
  "attack": 49
}

적용 코드:

struct Pokemon: Decodable {
  let name: String
  let hp: Int
  let attack: Int
}

let result = try JSONDecoder().decode(Pokemon.self, from: data)

특징:

  • JSON 구조가 평면적(flat)일 때 매우 간단하게 사용 가능
  • 모델 구조와 JSON 구조가 1:1로 대응됨

✅ Decoder.container 방식

예제 JSON (중첩 구조):

{
  "pokemon": {
    "name": "Bulbasaur",
    "stats": {
      "hp": 45,
      "attack": 49
    }
  }
}

적용 코드:

struct PokemonStats: Decodable {
  let name: String
  let hp: Int

  enum RootKeys: String, CodingKey {
    case pokemon
  }

  enum PokemonKeys: String, CodingKey {
    case name
    case stats
  }

  enum StatsKeys: String, CodingKey {
    case hp
  }

  init(from decoder: Decoder) throws {
    let root = try decoder.container(keyedBy: RootKeys.self)
    let pokemon = try root.nestedContainer(keyedBy: PokemonKeys.self, forKey: .pokemon)
    name = try pokemon.decode(String.self, forKey: .name)

    let stats = try pokemon.nestedContainer(keyedBy: StatsKeys.self, forKey: .stats)
    hp = try stats.decode(Int.self, forKey: .hp)
  }
}

특징:

  • 중첩된 구조에서 필요한 값만 파싱 가능
  • 전체 구조를 모델링하지 않아도 됨
  • JSON이 복잡하거나 일부 값만 추출할 때 유리

📌 요약 비교표

항목 JSONDecoder.decode Decoder.container
예제 JSON 구조 평면적 중첩 구조
처리 대상 전체 구조 필요한 값만 선택적으로 추출
사용 난이도 간단하고 직관적 정교하고 유연한 제어 가능
적합한 상황 전체 구조가 단순하고 그대로 쓸 수 있을 때 일부 값만 파싱하거나 구조가 복잡할 때

다음편에서는 실제 JSON 구조를 바탕으로 init 디코딩을 어떻게 하는지 자세히 본다.