Using JSON with Custom Types

YesCoach·2021년 7월 6일
0

단순 번역이 대부분이라 공식문서와 비교하면서 보세요.
공식문서 -> 플레이그라운드 샘플코드 순입니다.

Using JSON with Custom Types 공식문서 링크
playground 샘플코드

Using JSON With Custom Types

Swift의 JSON 지원을 사용하여 JSON 데이터의 구조에 관계없이 인코딩 및 디코딩할 수 있습니다.

Overview

다른 앱, 서비스 및 파일에서 보내거나 받는 JSON 데이터는 다양한 모양과 구조로 제공됩니다. 이 예시에 설명된 테크닉을 사용하여 외부 JSON 데이터와 앱 모델 타입 간의 차이를 처리합니다.

이 샘플은 간단한 데이터 타입인 Groudit Product를 정의하고, 여러 가지 다른 JSON 포맷에서 해당 유형의 인스턴스를 구성하는 방법을 보여줍니다.

struct GroceryProduct: Codable {
    var name: String
    var points: Int
    var description: String?
}

Read Data from Arrays

Swift의 표현형(expressive type) 시스템을 사용하여, 동일한 구조의 객체 그룹(collections)을 수동으로 루핑(looping)하지 않도록 합니다. 이 playground에서는 Array 타입을 값으로 사용하여 다음과 같이 구성된 JSON을 사용하는 방법을 확인합니다.

[
    {
        "name": "Banana",
        "points": 200,
        "description": "A banana grown in Ecuador."
    }
]

루핑(looping)이란 말이 이해되지 않았는데, loop 반복의 그 loop 인 듯 하다. 즉, 동일한 구조의 객체 그룹(collections)을 수동으로 읽어오지 않고, 자동으로 읽어오는 것을 뜻함. 그래서 [ {"name": "Banana", ...} ] 와 같이 배열 구조를 가지는 듯

Change Key Names

이름에 관계없이 JSON 키의 데이터를 사용자 지정 타입의 속성에 매핑하는 방법에 대해 알아 보세요. 예를 들어, 이 playground는 아래의 JSON에 있는 "product_name" 키를 Groudit Product의 이름 타입에 매핑하는 방법을 보여줍니다.

{
    "product_name": "Banana",
    "product_cost": 200,
    "description": "A banana grown in Ecuador."
}

JSON 키의 이름이 다른 경우 에도 사용자 지정 매핑을 통해 Swift 모델의 프로퍼티 이름에 Swift API Design Guidelines를 지킬 수 있습니다.

Access Nested Data

코드에 필요하지 않은 JSON의 구조 및 데이터를 무시하는 방법에 대해 배우세요. 이 playground는 중간 타입(intermediate type)을 사용하여 원하지 않는 데이터와 구조를 건너뛰기 위해 이와 같은 모양의 식료품 제품을 JSON에서 추출하는 방법을 확인합니다.

[
    {
        "name": "Home Town Market",
        "aisles": [
            {
                "name": "Produce",
                "shelves": [
                    {
                        "name": "Discount Produce",
                        "product": {
                            "name": "Banana",
                            "points": 200,
                            "description": "A banana that's perfectly ripe."
                        }
                    }
                ]
            }
        ]
    }
]

Merge Data at Different Depths

EncodableDecodable의 프로토콜 요구 사항에 대한 맞춤형 구현을 작성하여 JSON 구조의 서로 다른 깊이(depth)에서 데이터를 결합하거나 분리합니다. 이 playground에서는 다음과 같은 모양의 JSON에서 Groudit Product 인스턴스를 구성하는 방법을 보여 줍니다.

{
    "Banana": {
        "points": 200,
        "description": "A banana grown in Ecuador."
    }
}

See Also

JSON

  • class JSONEncoder
    데이터 타입(data type)의 인스턴스를 JSON 객체로 인코딩하는 객체.

  • class JSONDecoder
    JSON 객체에서 데이터 타입(data type)의 인스턴스를 디코딩하는 객체.

  • class JSONSerialization
    JSON 객체Foundation 객체간에 변환되는 객체입니다.


Playground Sample Code

Swift에서 데이터 타입의 구조와 인코딩 및 디코딩에 사용하는 JSON의 구조를 제어할 때는 JSONEncoderJSONDecoder 클래스에서 생성된 기본 형식(default format)을 사용하세요.
그러나 코드 모델이 변경해서는 안 되는 사양을 따르는 경우에도, Swift의 모범 사례 및 규칙(best practices and conventions)에 따라 데이터 타입을 작성해야 합니다. 이 샘플은 타입의 Codable 프로토콜 준수를 데이터의 JSON 표현스위프트 표현 사이의 변환 계층으로 사용하는 방법을 보여줍니다.

Read Data From Arrays

사용하는 JSON에 균일한 요소 배열이 포함되어 있으면, 개별 요소의 타입에 Codable 프로토콜를 채택시킵니다. 모든 배열을 디코딩하거나 인코딩하려면 [Element].self 구문을 사용합니다.

아래 예시에서 Grocery Product 구조는 Codable 프로토콜 채택이 선언문에 포함되기 때문에 자동으로 디코딩이 가능합니다. 예시에서 모든 배열은 decode method이 사용된 구문을 기반으로 디코딩할 수 있습니다.

import Foundation

let json = """
[
    {
        "name": "Banana",
        "points": 200,
        "description": "A banana grown in Ecuador."
    },
    {
        "name": "Orange",
        "points": 100
    }
]
""".data(using: .utf8)!

struct GroceryProduct: Codable {
    var name: String
    var points: Int
    var description: String?
}

let decoder = JSONDecoder()
let products = try decoder.decode([GroceryProduct].self, from: json)

print("The following products are available:")
for product in products {
    print("\t\(product.name) (\(product.points) points)")
    if let description = product.description {
        print("\t\t\(description)")
    }
}

JSON 배열에 GroceryProduct 인스턴스가 아닌 요소가 하나라도 포함되어 있으면 디코딩이 실패합니다. 이렇게 하면 오류나 JSON 배열 제공자의 보장(guarantees)에 대한 오해로 인해 데이터가 자동으로 손실되지 않습니다.

Change Key Names

Swift 코드에서 사용하는 이름이 JSON에서 동일한 값을 참조하는 이름과 항상 일치하는 것은 아닙니다. Swift에서 JSONEncoder 및 JSONDecoder 클래스로 작업할 때는, 다른 이름을 사용하는 JSON을 사용하더라도 기존 Swift 이름을 데이터 유형에 쉽게 적용할 수 있습니다.

Swift 이름과 JSON 이름 간의 연결(mapping)을 생성하려면 Codable, Encodable 또는 Decodable를 준수하는 동일한 타입 내에서 CodingKeys라는 중첩된 열거형(Nested enumeration)을 사용합니다.

아래 예시에서 프로퍼티가 인코딩 및 디코딩될 때, Swift 프로퍼티 이름이 "product_name" 이름과 매핑되는 방법을 확인하세요.

import Foundation

let json = """
[
    {
        "product_name": "Bananas",
        "product_cost": 200,
        "description": "A banana grown in Ecuador."
    },
    {
        "product_name": "Oranges",
        "product_cost": 100,
        "description": "A juicy orange."
    }
]
""".data(using: .utf8)!

struct GroceryProduct: Codable {
    var name: String
    var points: Int
    var description: String?
    
    private enum CodingKeys: String, CodingKey {
        case name = "product_name"
        case points = "product_cost"
        case description
    }
}

let decoder = JSONDecoder()
let products = try decoder.decode([GroceryProduct].self, from: json)

print("The following products are available:")
for product in products {
    print("\t\(product.name) (\(product.points) points)")
    if let description = product.description {
        print("\t\t\(description)")
    }
}

description은 두 표현 간에 일치하지만 GroceryProduct 구조에 필요한 값이므로 CodingKeys 열거에 계속 포함됩니다. description의 Enumeration Cases 이름은 해당 속성 이름과 동일하므로 명시적인 원시 값이 필요하지 않습니다.

Access Nested Data

외부 소스 또는 기존 로컬 형식의 JSON을 사용하는 앱을 작성할 수 있습니다. 어느 경우든, 앱에서 모델링 중인 개념 구조와 JSON 생산자가 모델링한 개념 간에 불일치가 발견될 수 있습니다. 때때로 Swift 프로그램을 위한 논리적 데이터 번들은 사용되는 JSON의 중첩된 여러 객체 또는 배열에 분산됩니다. 판독 중인 JSON의 구조와 일치하는 decodable type을 작성하여 구조적 차이(gap)을 메웁니다. decodable type은 디코딩하기에 안전한 중간타입(intermediate types) 역할을 합니다. decodable type은 나머지 앱에서 사용할 타입의 Initializer에서 데이터 원본 역할을 합니다.

중간타입(intermediate types)을 사용하면 다양한 형태의 외부 JSON과의 호환성을 유지하면서 자체 코드에서 가장 자연스러운 타입을 사용할 수 있습니다.

아래 예제는 GroceryStore를 나타내는 타입과 판매되는 제품의 목록을 소개합니다.

import Foundation

struct GroceryStore {
    var name: String
    var products: [Product]
    
    struct Product: Codable {
        var name: String
        var points: Int
        var description: String?
    }
}

API는 다음과 같이 구성된 JSON을 사용하여 식료품점에 대한 정보를 제공할 수 있습니다.

let json = """
[
    {
        "name": "Home Town Market",
        "aisles": [
            {
                "name": "Produce",
                "shelves": [
                    {
                        "name": "Discount Produce",
                        "product": {
                            "name": "Banana",
                            "points": 200,
                            "description": "A banana that's perfectly ripe."
                        }
                    }
                ]
            }
        ]
    },
    {
        "name": "Big City Market",
        "aisles": [
            {
                "name": "Sale Aisle",
                "shelves": [
                    {
                        "name": "Seasonal Sale",
                        "product": {
                            "name": "Chestnuts",
                            "points": 700,
                            "description": "Chestnuts that were roasted over an open fire."
                        }
                    },
                    {
                        "name": "Last Season's Clearance",
                        "product": {
                            "name": "Pumpkin Seeds",
                            "points": 400,
                            "description": "Seeds harvested from a pumpkin."
                        }
                    }
                ]
            }
        ]
    }
]
""".data(using: .utf8)!

API에서 반환하는 JSON에는 해당 Swift 타입을 채우는 데 필요한 것보다 더 많은 정보가 포함되어 있습니다. 특히 앞서 정의한 GroceryStore 구조와 구조적으로 호환되지 않습니다. product가 복도(aisles)와 선반(shelves) 내부에 중첩되어 있습니다. JSON 제공자에 추가 정보가 필요할 수 있지만, JSON 제공자에 종속된 모든 앱에서 유용하지 않을 수 있습니다.

외부 컨테이너에서 필요한 데이터를 추출하려면 원본 JSON의 모양을 미러링하는 타입을 작성하여 Decodable하다고 표시합니다. 그런 다음 JSON을 미러링하는 타입의 인스턴스를 사용하는 남은 앱에 이니셜라이저를 작성합니다.

아래의 예에서 GroceryStoreService 구조는 앱에서 의도된 사용에 이상적인 GroceryStore JSON과 GroceryStore 구조 사이의 중개자 역할을 한다.


struct GroceryStoreService: Decodable {
    let name: String
    let aisles: [Aisle]
    
    struct Aisle: Decodable {
        let name: String
        let shelves: [Shelf]
        
        struct Shelf: Decodable {
            let name: String
            let product: GroceryStore.Product
        }
    }
}

GroceryStoreService 구조는 JSON의 구조(Aisle과 Shelf포함)와 일치하므로 프로토콜이 구조의 상속 타입 목록에 포함될 때 Decodable 프로토콜에 대한 준수는 자동으로 이루어집니다. 데이터가 동일한 이름 및 타입을 사용하기 때문에 GroceryStore 구조의 중첩된 Product 구조가 Shelf 구조에서 재사용됩니다.

GroceryStoreService 구조의 역할을 중간 타입으로 완료하려면 GroceryStore 구조의 extension을 사용하십시오. 확장 기능에는 GroceryStoreService 인스턴스를 사용하고 Aisle 및 Shelf를 루프 처리하여 불필요한 중첩을 제거하는 이니셜라이저가 추가되었습니다.

extension GroceryStore {
    init(from service: GroceryStoreService) {
        name = service.name
        products = []
        
        for aisle in service.aisles {
            for shelf in aisle.shelves {
                products.append(shelf.product)
            }
        }
    }
}

위의 예에서 타입 간의 관계를 사용하여 JSON에서 안전하고 간결하게 읽고, GroceryStore Service 중간 타입을 전달하며, 앱에서 결과로 생성된 GroceryStore 인스턴스를 사용할 수 있습니다.

let decoder = JSONDecoder()
let serviceStores = try decoder.decode([GroceryStoreService].self, from: json)

let stores = serviceStores.map { GroceryStore(from: $0) }

for store in stores {
    print("\(store.name) is selling:")
    for product in store.products {
        print("\t\(product.name) (\(product.points) points)")
        if let description = product.description {
            print("\t\t\(description)")
        }
    }
}

Merge Data from Different Depths

JSON 파일 또는 API에서 사용하는 데이터 모델이 앱에서 사용하는 모델과 일치하지 않는 경우가 있습니다. 이 경우 인코딩 및 디코딩 시 JSON에서 객체를 병합하거나 분리해야 할 수 있습니다. 따라서 단일 인스턴스의 인코딩 또는 디코딩은 JSON 객체의 계층 구조에서 위쪽 또는 아래쪽으로 도달합니다.

아래 예는 이러한 데이터 병합 스타일의 일반적인 발생을 보여 줍니다. 이 회사는 판매하는 각 제품의 이름, 가격 및 기타 세부 정보를 추적하는 식품점을 모델로 합니다.

import Foundation

let json = """
{
    "Banana": {
        "points": 200,
        "description": "A banana grown in Ecuador."
    },
    "Orange": {
        "points": 100
    }
}
""".data(using: .utf8)!

제품 이름은 제품의 나머지 상세 내역을 정의하는 키의 이름이기도 합니다. 이 경우 "Banana" 제품에 대한 정보가 제품 이름 아래에 중첩된 개체에 저장됩니다. 그러나 관례상 제품 이름객체의 식별 키에서 온 것이 분명합니다.

반면, JSON 구조의 다른 공식에는 각 제품에 대한 "제품" 키와 각 개별 제품 이름을 저장하는 "이름" 키가 있을 수 있습니다. 이 대체 공식은 아래 예와 같이 Swift에서 데이터를 모델링하는 방법과 일치합니다.

struct GroceryStore {
    struct Product {
        let name: String
        let points: Int
        let description: String?
    }

    var products: [Product]

    init(products: [Product] = []) {
        self.products = products
    }
}

GrouditStore 구조에 대한 다음의 확장은 구조가 궁극적으로 Codable 프로토콜에 부합하도록 하는 Encodable 프로토콜을 준수하도록 합니다. 특히 CodingKey 프로토콜과 동일한 종류의 규정을 준수하는 보다 일반적인 열거형인 ProductKey를 사용합니다. 제품 구조의 인스턴스에 대한 이름으로 사용될 수 있는 코딩 키 수를 무제한으로 고려하려면 구조가 필요합니다.

extension GroceryStore: Encodable {
    struct ProductKey: CodingKey {
        var stringValue: String
        init?(stringValue: String) {
            self.stringValue = stringValue
        }

        var intValue: Int? { return nil }
        init?(intValue: Int) { return nil }

        static let points = ProductKey(stringValue: "points")!
        static let description = ProductKey(stringValue: "description")!
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: ProductKey.self)
        
        for product in products {
            // Any product's `name` can be used as a key name.
            let nameKey = ProductKey(stringValue: product.name)!
            var productContainer = container.nestedContainer(keyedBy: ProductKey.self, forKey: nameKey)
            
            // The rest of the keys use static names defined in `ProductKey`.
            try productContainer.encode(product.points, forKey: .points)
            try productContainer.encode(product.description, forKey: .description)
        }
    }
}

위의 예에서 Encodable 프로토콜에 따라 GrouditStore 인스턴스는 JSONEncoder 인스턴스를 사용하여 JSON으로 인코딩될 수 있습니다.

var encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted

let store = GroceryStore(products: [
    .init(name: "Grapes", points: 230, description: "A mixture of red and green grapes."),
    .init(name: "Lemons", points: 2300, description: "An extra sour lemon.")
])

print("The result of encoding a GroceryStore:")
let encodedStore = try encoder.encode(store)
print(String(data: encodedStore, encoding: .utf8)!)
print()

Codable 프로토콜의 준수를 구현하는 후반부는 디코딩입니다. 다음 확장으로 Groudit Store 구조에 대한 준수가 완료됩니다. 들어오는 JSON 개체를 디코딩하는 과정의 일부로 Initializer는 개체에 있는 첫 번째 중첩 수준의 모든 키에 루프를 적용합니다.

extension GroceryStore: Decodable {
    public init(from decoder: Decoder) throws {
        var products = [Product]()
        let container = try decoder.container(keyedBy: ProductKey.self)
        for key in container.allKeys {
            // Note how the `key` in the loop above is used immediately to access a nested container.
            let productContainer = try container.nestedContainer(keyedBy: ProductKey.self, forKey: key)
            let points = try productContainer.decode(Int.self, forKey: .points)
            let description = try productContainer.decodeIfPresent(String.self, forKey: .description)

            // The key is used again here and completes the collapse of the nesting that existed in the JSON representation.
            let product = Product(name: key.stringValue, points: points, description: description)
            products.append(product)
        }

        self.init(products: products)
    }
}

let decoder = JSONDecoder()
let decodedStore = try decoder.decode(GroceryStore.self, from: json)

print("The store is selling the following products:")
for product in decodedStore.products {
    print("\t\(product.name) (\(product.points) points)")
    if let description = product.description {
        print("\t\t\(description)")
    }
}

🙇‍ 오역 및 오개념 지적댓글 부탁드립니다 🙇‍

profile
iOS dev / Japanese with Computer Science

0개의 댓글