티스토리 뷰
아래와 같은 JSON data가 있다고 했을때 어떻게 decoding을 하나요?
{
"id": 1,
"name": "John Doe",
"details": {
"address": {
"city": "Seoul",
"postalCode": "12345"
},
"contacts": {
"email": "john.doe@example.com",
"phone": "010-1234-5678"
}
}
}
최상위로 id, name, details가 있고
details 내부에 address, contacts 가 있고,
address 내부에 city, postalCode,
contacts 내부에 email, phone이 있는 구조입니다.
보통 저는 decoding을 하게되면 아래와 같이 JSO 중괄호 ( { } )를 기점으로 struct를 하나씩 만들어서 관리를 했었습니다.
struct User: Decodable {
let id: Int
let name: String
let details: DetailsInfo
struct DetailsInfo: Decodable {
let address: AddressInfo
let contacts: ContactsInfo
}
struct AddressInfo: Decodable {
let city: String
let postalCode: String
}
struct ContactsInfo: Decodable {
let email: String
let phone: String
}
}
오늘은 이 방법 이외에 nestedContainer라는 인스턴스 메서드를 사용한 decode 방법을 알아보려고 합니다.
예를들어 아래와 같은 UserDTO(Data Transfer Object)가 있다고 가정해봅시다.
struct User: Decodable {
let id: Int
let name: String
enum UserKeys: String, CodingKey {
case id
case name
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: UserKeys.self)
self.id = try container.decode(Int.self, forKey: .id)
self.name = try container.decode(String.self, forKey: .name)
}
}
User struct에 Decoadable을 채택해준 후 init()을 해보면 아래와 같이 자동완성이 뜹니다.
init에 파라미터인 decoder는 Decoder 프로토콜을 채택하고 있으며,
아래 프로퍼티 및 메서드를 정의하고 있습니다.
이중 container(keyedBy:) 를 아래 로직로 예를들어 해석을 해보면
container(keyedBy: CodingKeys.self)를 호출하면 *CodingKeys를 채택하는 UserKeys가 갖고 있는 id, name의 key값을 반환합니다.
*CodingKey 란?
encoding과 decoding을 위해 'key'로 사용할 수 있게한 타입
let container = try decoder.container(keyedBy: UserKeys.self)
이후 container의 key들을 디버깅해보면 name과 id가 반환된걸 볼 수 있습니다.
그 다음줄을 봅시다 (decode)
self.id = try container.decode(Int.self, forKey: .id)
self.name = try container.decode(String.self, forKey: .name)
name과, id의 key를 가지고 있는 container (UserKeys container)에 decode(type:, forKey)를 호출하는데,
decode 메서드 선언부는 아래와 같습니다.
type:
파라미터의 type에는 다양한 데이터 타입이 (String, Bool, Int, Double 등) 들어가고,
여기엔 변환하고자 하는 데이터 타입을 넣어줍니다.
key:
container에 저장되어있는 key에 해당하는 값을 decoding해 return합니다.
에러처리는 아래와 같이 되어있습니다.
1. typeMismatch: 타입이 맞지 않는경우
2. keyNotFound: key를 찾을 수 없는 경우
3. valueNotFound: 값을 찾을 수 없는 경우
아래 예제 코드를 분석해보자면
self.id = try container.decode(Int.self, forKey: .id)
self.name = try container.decode(String.self, forKey: .name)
id라는 key의 데이터를 Int 타입으로 decoding후 self.id에 할당
name key의 데이터를 String 타입으로 decoding 후 self.name에 할당 하는게 되겠네요.
이렇게 계층이 하나만있는 경우에는 decode하는 흐름을 파악하기 쉽죠
container에서 decode까지의 흐름은 알았으니,
그럼 좀더 심화해서 아래의 경우처럼 계층이 여러개 있다면?
그리고 아래 JSON에서 city라는 데이터를 가져오고 싶다면?
{
"id": 1,
"name": "John Doe",
"details": {
"address": {
"city": "Seoul",
"postalCode": "12345"
},
"contacts": {
"email": "john.doe@example.com",
"phone": "010-1234-5678"
}
}
}
이런 경우에 사용하는게 nestedContainer 메서드 입니다.
nestedContainer 란?
주어진 key에 대한 key encoding container를 저장하고 반환합니다.
예시를 들면서 하나씩 뜯어봅시다.
CodingKey를 채택하는 Key(enum)가 갖고 있는 case들의 key에 접근합니다
만약 city 값을 decoding해서 가져오고 싶다고 가정하면
city는 아래와 같이 접근해서 가져와야 합니다.
details.address.city
위 예제에서 UserKeys를 통해 container를 만들어 id, name key에 접근할 수 있었듯이
이번엔 details에 접근하기 위해 CodingKey를 채택하고 있는 DetailsKeys 열거형을 하나 만들어 줍시다.
struct User: Decodable {
let id: Int
let name: String
let city: String
let postalCode: String
enum UserKeys: String, CodingKey {
case id
case name
case details
}
// 추가
enum DetailsKeys: String, CodingKey {
case address
case contacts
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: UserKeys.self)
self.id = try container.decode(Int.self, forKey: .id)
self.name = try container.decode(String.self, forKey: .name)
}
}
UserKeys의 key 중 details에 대한 container를 만들어주기 위해 사용하는게 nestedContainer 입니다.
let detailsContainer = try container.nestedContainer(keyedBy: DetailsKeys.self, forKey: .details)
코드를 해석하자면 아래와 같겠네요.
UserKeys로된 container내부에 container를 만들건데, DetailsKeys의 key 중 details에 해당하는 container를 만들거야
detailsContainer의 key들을 디버깅해보면 아래와 같이 생성된걸 볼수 있습니다.
아직 city에 접근하려면 한단계 더 가야합니다.
city는 address 내부에 있으므로 위에서 진행한 방법과 같이 nestedContainer를 하나 더 만들어줍니다.
struct User: Decodable {
let id: Int
let name: String
let city: String
let postalCode: String
enum UserKeys: String, CodingKey {
case id
case name
case details
}
enum DetailsKeys: String, CodingKey {
case address
case contacts
}
// AddressKeys 추가
enum AddressKeys: String, CodingKey {
case city
case postalCode
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: UserKeys.self)
self.id = try container.decode(Int.self, forKey: .id)
self.name = try container.decode(String.self, forKey: .name)
let detailsContainer = try container.nestedContainer(keyedBy: DetailsKeys.self, forKey: .details)
// addressContainer 추가
let addressContainer = try detailsContainer.nestedContainer(keyedBy: AddressKeys.self, forKey: .address)
// decode한 값 할당 (self.city, self.postalCode)
self.city = try addressContainer.decode(String.self, forKey: .city)
self.postalCode = try addressContainer.decode(String.self, forKey: .postalCode)
}
}
위와 같이 생성 후 디버깅을 해보면 addressContainer가 잘 생성되었고,
decoding 된 값이 self.city에 할당된걸 볼 수 있습니다.
최종 코드는 아래와 같습니다.
struct User: Decodable {
let id: Int
let name: String
let city: String
let postalCode: String
let email: String
let phone: String
enum UserKeys: String, CodingKey {
case id
case name
case details
}
enum DetailsKeys: String, CodingKey {
case address
case contacts
}
enum AddressKeys: String, CodingKey {
case city
case postalCode
}
enum ContactsKeys: String, CodingKey {
case email
case phone
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: UserKeys.self)
self.id = try container.decode(Int.self, forKey: .id)
self.name = try container.decode(String.self, forKey: .name)
let detailsContainer = try container.nestedContainer(keyedBy: DetailsKeys.self, forKey: .details)
let addressContainer = try detailsContainer.nestedContainer(keyedBy: AddressKeys.self, forKey: .address)
self.city = try addressContainer.decode(String.self, forKey: .city)
self.postalCode = try addressContainer.decode(String.self, forKey: .postalCode)
let contactsContainer = try detailsContainer.nestedContainer(keyedBy: ContactsKeys.self, forKey: .contacts)
self.email = try contactsContainer.decode(String.self, forKey: .email)
self.phone = try contactsContainer.decode(String.self, forKey: .phone)
}
}
느낀점
개인적으로 nestedContainer를 써야겠다고 생각이 들었던 점은
key에 해당하는 value값을 flat하게 접근하기 위한 용도로 사용하면 좋겠다는 생각이 들었습니다.
struct를 사용할경우 User.Details.Address.city 이렇게 타고타고 들어가야 되는점과 비교가 됐던것 같네요.
오늘은 decoder부터 container, nestedContainer까지 decode하는 또 다른 방법과 흐름을 훑어봤습니다.
처음엔 이 코드를 보고 코드양이 많아 '이게 뭐지..' 하면서 한참동안 들여다 봤던 기억이 있는데
막상 하나씩 분석해보니 가독성도 늘고 어떤 흐름으로 decode를 할 수 있는지 배울 수 있었던것 같습니다.
아는만큼 보이는것 같네요.
그동안 사용했던 메서드등도 한번씩 다시봐야겠다는 생각이 들었습니다 :)
참고
1. container
https://developer.apple.com/documentation/swift/decoder/container(keyedby:)
2. decode
https://developer.apple.com/documentation/swift/keyeddecodingcontainer/decode(_:forkey:)-7vj8e
'iOS' 카테고리의 다른 글
[iOS] UITableView cell LifeCycle (0) | 2024.12.22 |
---|---|
[iOS] URLSession image upload (0) | 2024.11.24 |
[iOS] Tuist App Extension (feat. WidgetExtension) (0) | 2024.10.25 |
[iOS] 고해상도 이미지 다운시 성능 개선 (Image DownSampling) (2) | 2024.10.11 |
[iOS] Tuist (feat. xcproj 충돌) (5) | 2024.09.02 |
- Total
- Today
- Yesterday
- 2023년 회고
- Swift Error Handling
- swift 고차함수
- swift protocol
- 원티드 프리온보딩
- RTCCameraVideoCapturer
- ios
- removeLast()
- Swift 알고리즘
- iOS error
- Swift Leetcode
- Swift 프로퍼티
- RIBs tutorial
- Combine: Asynchronous Programming with Swift
- swift programmers
- swift (programmers)
- Swift joined()
- Swift RIBs
- Swift init
- CS 네트워크
- Swift 내림차순
- Swift ModernRIBs
- Swift
- swift property
- Swift joined
- swift reduce
- Class
- Swift 프로그래머스
- Swift final
- Swift inout
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 | 28 | 29 | 30 | 31 |