
본 글은 Encoding and Decoding Custom Types (애플 공식 문서)를 한국어로 번역하여 옮긴 글입니다.
데이터 타입을 JSON과 같은 외부 표현과의 호환성을 위해 인코딩과 디코딩을 가능하게 만드세요.
많은 프로그래밍 작업은 네트워크 연결을 통해 데이터를 보내거나, 디스크에 데이터를 저장하거나, 아니면 API나 서비스에 데이터를 제출하는 일과 관련되어 있습니다. 이 작업은 데이터를 전송하는 동안 종종 중간 형식(format)으로 인코딩하거나 디코딩이 되도록 요구됩니다.
Swift 표준 라이브러리는 데이터 인코딩과 디코딩을 위한 표준 접근 방식을 명시합니다. 이 접근 방식을 채택하려면 커스텀 데이터 타입에 Encodable과 Decodable 프로토콜을 구현하면 됩니다. 이 프로토콜을 채택하면 Encoder와 Decoder 프로토콜의 구현부가 데이터를 보고 JSON이나 프로퍼티 리스트와 같은 외부 표현으로 인코딩하거나 (외부 표현을 모델로) 디코딩을 하게 합니다. 인코딩과 디코딩 모두 지원하려면, Encodable과 Decodable 프로토콜을 합친 Codable 프로토콜 준수를 선언하세요.
타입을 코더블(codable)하게 만드는 가장 쉬운 방법은 이미 Codable 프로토콜을 준수하는 타입을 사용하여 해당 타입의 프로퍼티를 선언하는 것입니다. 이 타입은 String, Int와 Double과 같은 표준 라이브러리와 Date, Data와 URL과 같은 파운데이션(foundation)에 포함되어 있습니다. 프로퍼티들이 코더블한 타입은 해당 준수를 선언하는 것만으로도 자동으로 Codable 프로토콜을 준수하게 됩니다.
랜드마크의 이름(name)과 설립연도(foundingYear)를 저장하는 Landmark 구조체를 살펴보죠.
struct Landmark {
var name: String
var foundingYear: Int
}
Landmark 구조체의 상속 목록으로 Codable을 추가하면 Encodable과 Decodable 프로토콜의 모든 요구사항을 만족하는 자동 준수성이 실행됩니다.
struct Landmark: Codable {
var name: String
var foundingYear: Int
// Landmark now supports the Codable methods init(from:) and encode(to:)
// even though they aren't written as part of its declaration
}
타입에 Codable 프로토콜을 채택하면 빌트-인 데이터 형식과 커스텀 인코더와 디코더가 제공하는 모든 형식으로 해당 타입을 직렬화(serialize)할 수 있습니다. 예를 들어, Landmark 구조체는 JSON과 프로퍼티 리스트를 다루게 하는 자체적인 코드가 없음에도 PropertyListEncoder와 JSONEncoder 클래스를 모두 사용하여 인코딩할 수 있습니다.
동일한 규칙을 코더블한 다른 커스텀 타입으로 만들어진 커스텀 타입에도 적용할 수 있습니다. 모든 프로퍼티가 Codable 프로토콜을 준수하기만 하면, 어느 커스텀 타입도 코더블이 될 수 있습니다.
아래 예제는 Landmark 구조체에 location 프로퍼티를 추가하면 어떻게 자동 준수성을 적용하는지 보여줍니다.
struct Coordinate: Codable {
var latitude: Double
var longitude: Double
}
struct Landmark: Codable {
// Double, String, and Int all conform to Codable.
var name: String
var foundingYear: Int
// Adding a property of a custom Codable type maintains overall Codable conformance.
var location: Coordinate
}
Array, Dictionary와 Optional과 같은 빌트-인 타입도 코더블한 타입을 가지면 Codable 프로토콜을 준수할 수 있습니다. Landmark 구조체에 Coordinate 인스턴스의 배열을 추가할 수 있으며, 전체 구조체는 여전히 Codable 프로토콜의 요구사항을 만족합니다.
아래 예제는 Landmark 구조체에 빌트-인 코더블 타입을 사용하여 여러 프로퍼티를 추가하면 어떻게 자동 준수성을 적용하는지 보여줍니다.
struct Landmark: Codable {
var name: String
var foundingYear: Int
var location: Coordinate
// Landmark is still codable after adding these properties.
var vantagePoints: [Coordinate]
var metadata: [String: String]
var website: URL?
}
일부 경우에는, 인코딩과 디코딩 양방향에 대한 Codable의 지원이 필요치 않을 수 있습니다. 예를 들어, 일부 앱은 원격 네트워크 API에 대한 호출만 필요하며 동일한 타입을 포함하는 응답은 디코딩할 필요가 없을 수 있습니다. 데이터 인코딩만 지원한다면 Encodable 프로토콜 준수를 선언하세요. 반대로 주어진 타입의 데이터를 읽기만 해야 한다면 Decodable 프로토콜 준수를 선언하세요.
아래 예제는 데이터를 인코딩과 디코딩만 하는 Landmark 구조체의 대체 선언을 보여줍니다.
struct Landmark: Encodable {
var name: String
var foundingYear: Int
}
struct Landmark: Decodable {
var name: String
var foundingYear: Int
}
코더블 타입은 CodingKey 프로토콜을 준수하는 특별한 중첩 열거형인 CodingKeys를 선언할 수 있습니다. 이 열거형을 작성하면, 열거형의 케이스는 코더블 타입의 인스턴스가 인코딩이나 디코딩이 될 때 반드시 포함되어야 하는 권한을 가진 프로퍼티 목록으로 역할을 합니다. 열거형 케이스의 이름은 타입에서 대응되는 프로퍼티 이름과 일치해야 합니다.
인스턴스를 디코딩할 때 보이기를 원치 않거나, 특정 프로퍼티가 인코딩된 표현에 포함되기를 원치 않으면 CodingKeys 열거형에서 프로퍼티를 생략합니다. CodingKeys에서 생략된 프로퍼티는 해당 프로퍼티를 포함하는 타입이 Decodable과 Encodable 자동 준수성을 받기 위해 기본 값이 필요로 합니다.
직렬화된 데이터 형식이 사용하는 키(key)가 데이터 타입의 프로퍼티 이름과 동일하지 않다면, CodingKeys 열거형에 String 타입의 원시 값(raw value)을 명시하여 대체 키를 제공하세요. 열거형 케이스에서 원시 값으로 사용하는 문자열은 인코딩과 디코딩 중에 사용되는 키 이름입니다. 케이스 이름과 그 원시 값의 연관성 덕분에, 직렬화 형식의 이름, 구두점과 대소문자를 일치시킬 필요없이 Swift API Design Guidelines에 따라 데이터 구조의 이름을 지정할 수 있습니다.
아래 예제는 인코딩과 디코딩을 할 때 Landmark 구조체의 name과 foundingYear 프로퍼티의 대체 키를 사용하는 방법을 보여줍니다.
struct Landmark: Codable {
var name: String
var foundingYearL Int
var location: Coordinate
var vantagePoints: [Coordinate]
enum CodingKeys: String, CodingKey {
case name = "title"
case foundingYear = "founding_date"
case location
case vantagePoints
}
}
Swift 타입의 구조체가 인코딩된 형식의 구조체와 다르면, Encodable과 Decodable 프로토콜의 커스텀 구현을 제공하여 직접 인코딩과 디코딩 로직을 정의할 수 있습니다.
아래 예제에서, Coordinate 구조체는 additionalInfo 컨테이너 안에 중첩된 elevation 프로퍼티를 지원하도록 확장합니다.
struct Coordinate {
var latitude: Double
var longitude: Double
var elevation: Double
enum CodingKeys: String, CodingKey {
case latitude
case longitude
case additionalInfo
}
enum AdditinalInfoKeys: String, CodingKey {
case elevation
}
}
Coordinate 타입의 인코딩된 형태가 두 번째 레벨의 중첩된 정보를 포함하기에, 이 타입의 Encodable과 Decodable 프로토콜의 채택은 특정 레벨에서 사용되는 코딩 키의 전체 집합을 나열하는 두 개의 열거형을 사용합니다.
⭐️ 김지지의 NOTE
- 아래는 두 가지 수준으로 중첩된 JSON 형식을 보여줍니다.
{ "latitude": 123.123, "longitude": 789.789, "additionalInfo": { "elevation": 456.456 } }
아래 예제에서, Coordinate 구조체는 init(from:)인 필수 이니셜라이저를 구현하여 Decodable 프로토콜을 준수하도록 확장되었습니다.
extension Coordinate: Decodable {
init(from decoder: any Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
latitude = try values.decode(Double.self, forKey: .latitude)
longitude = try values.decode(Double.self, forKey: .longitude)
let additionalInfo = try values.nestedContainer(keyedBy: AdditionalInfoKeys.self, forKey: .additionalInfo)
elevation = try additionalInfo.decode(Double.self, forKey: .elevation)
}
}
이 이니셜라이저는 매개변수로 전달받은 Decoder 인스턴스의 메서드를 사용하여 Coordinate 인스턴스를 채웁니다. Coordinate 인스턴스의 두 프로퍼티는 Swift 표준 라이브러리가 제공하는 키드(keyed) 컨테이너 API를 사용하여 초기화됩니다.
아래 예제에서, Coordinate 구조체는 encode(to:)인 필수 메서드를 구현하여 Encodable 프로토콜을 준수하도록 확장될 수 있습니다.
extension Coordinate: Encodable {
func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(latitude, forKey: .latitude)
try container.encode(longitude, forKey: .longitude)
var additionalInfo = container.nestedContainer(keyedBy: AdditionalInfoKeys.self, forKey: .additionalInfo)
try additionalInfo.encode(elevation, forKey: .elevation)
}
}
encode(to:) 메서드의 구현은 이전 예제의 디코딩 연산과 반대입니다.
인코딩과 디코딩 프로세스를 커스텀할 때 사용되는 컨테이너 타입에 대한 자세한 정보는 KeyedEncodingContainerProtocol과 UnkeyedEncodingContainer를 참조하세요.