Do.

Swift - JSON Encoding과 Decoding - Part2 본문

iOS

Swift - JSON Encoding과 Decoding - Part2

Hey_Hen 2022. 2. 9. 15:57

 

이미지 썸네일 삭제
Swift - JSON Encoding과 Decoding - Part1

만드는 어플에 JSON 데이터를 파싱해야 하는데 이게 눈으로만 보고 할려고 하니까 너무 어려워서 정리...

blog.naver.com

 

길어서 파트를 나눈다기 보다는 이제부터 작성하는 내용이 이해가 잘 안되어서 진짜로 공부할려는 부분이다.

레퍼런스 : https://www.raywenderlich.com/3418439-encoding-and-decoding-in-swift

이미지 썸네일 삭제
Encoding and Decoding in Swift

In this tutorial, you’ll learn all about encoding and decoding in Swift, exploring the basics and advanced topics like custom dates and custom encoding.

www.raywenderlich.com

Custom JSON Key

custom json key는 인코딩 할때 지정되있던 프로퍼티 이름이 아닌 별도의 이름으로 내보낼 수 있는 것이다.

무슨 내용인고 하니

import Foundation

struct Person: Codable {
  var name: String
  var age: String
  var gender: String
  var majorJob: Job
}

struct Job: Codable {
  var name: String
}

let henrysJob = Job(name: "developer")
let henry = Person(name: "henry", age: "28", gender: "male", majorJob: henrysJob)

let encoder = JSONEncoder()
encoder.outputFormatting = [.sortedKeys, .prettyPrinted]

let encodingData = try encoder.encode(henry)

print(String(data: encodingData, encoding: .utf8)!)
 
{
  "age" : "28",
  "gender" : "male",
  "majorJob" : {
    "name" : "developer"
  },
  "name" : "henry"
}
 

위 코드가 이해가 안되면 Part1을 보고오자

Person 프로퍼티로 majorJob이 job 객체를 또 나타내는데 이를 인코딩 했을 때는 그냥 job으로 바꾸고 싶다.

그럴때 쓰는 방법이다.

import Foundation

struct Person: Codable {
  var name: String
  var age: String
  var gender: String
  var majorJob: Job
  
  enum CodingKeys: String, CodingKey {
    case name, age, gender, majorJob = "job"
  }
}

struct Job: Codable {
  var name: String
}

let henrysJob = Job(name: "developer")
let henry = Person(name: "henry", age: "28", gender: "male", majorJob: henrysJob)

let encoder = JSONEncoder()
encoder.outputFormatting = [.sortedKeys, .prettyPrinted]

let encodingData = try encoder.encode(henry)

print(String(data: encodingData, encoding: .utf8)!)
 

변경점은 Person 구조체 내부에 열거형으로 CodingKey 프로토콜이 들어왔다는 것이다.

majorJob을 job으로 표시하게 된다.

{
  "age" : "28",
  "gender" : "male",
  "Job" : {
    "name" : "developer"
  },
  "name" : "henry"
}
 

주의할 점은 CodingKey를 사용하게 되면 majorJob 이름 하나 바꿔줄려고 name, age, gender 등 속성 이름들을 일일이 다 적었다는 것이다. 만약에 JSON의 데이터가 아주 방대한 내용이라면 고작 이름하나 바꾸자고 하기에는 비용이 너무 크다.

 

JSON 계층을 평탄화 하기

왜 그러는지 아직 이해 못하겠는데 위와 같은 JSON을 평탄화 하기로 마음먹었다고 치자

{
  "name" : "henry",
  "age" : "28",
  "gender" : "male",
  "job" : "developer"
}
 

최초 인코딩 하기 전 데이터는 Person과 Job객체의 NestedType 상태인데 이를 계층을 없애고 평탄화 해서 JSON 데이터를 내보내려고 한다. 위와 같이 말이다.

Dictionary로 보자면 더이상 [String: Job] 이 아니라. [String: String]으로 바뀐 것이다.

 

이를 인코딩 해서 내보낸다고 했을 때 아래오 같이 코딩할 수 있다.

import Foundation

struct Person: Encodable {
  var name: String
  var age: String
  var gender: String
  var majorJob: Job
  
  enum CodingKeys: CodingKey {
    case name, age, gender, job
  }
  
  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(name, forKey: .name)
    try container.encode(age, forKey: .age)
    try container.encode(gender, forKey: .gender)
    try container.encode(majorJob.name, forKey: .job)
  }
}

struct Job: Codable {
  var name: String
}

let henrysJob = Job(name: "developer")
let henry = Person(name: "henry", age: "28", gender: "male", majorJob: henrysJob)

let encoder = JSONEncoder()
encoder.outputFormatting = [.sortedKeys, .prettyPrinted]
let encodingData = try encoder.encode(henry)

print(String(data: encodingData, encoding: .utf8)!)
 

바뀐 부분은 Person 객체 내부의 CodingKey와 encode 메소드만 보면 된다.

encoder container를 정의하고 사용할 키가 CodingKeys로 정의되어 있다고 보면 된다.

container encode는 각각 객체의 속성과 연결할 키를 작성한다.

 

majorJob.name은 job으로 평탄화 해서 표시할 것이기 때문에 아래와 같이 작성한다.

try container.encode(majorJob.name, forKey: .job)
 

헷갈릴 수 있으니 이렇게 쓰는 거랑 같다.

try container.encode(majorJob.name, forKey: CodingKeys.job)
 
{
  "name" : "henry",  
  "age" : "28",
  "gender" : "male",
  "job" : "developer",
}
 

계층으로 보여지지 않고 평탄화 해서 나타났다.

 

이를 이제 디코딩 할때는 어떻게 해야할까?

디코딩 하는 사람 입장에서는 name, age, gender, job이 그냥 Person의 속성이고 Person에 죄다 저장해 버려도 상관 없을 것이다.

struct DPerson: Decodable {
  var name: String
  var age: String
  var gender: String
  var job: String
}

let decoder = JSONDecoder()
let decodingData = try decoder.decode(DPerson.self, from: encodingData)
 

이런식으로...원래 하던 방법 그대로 디코딩이 가능하다

 

job을 구조체 또는 클래스로 받을려고 하면 코드가 좀 많이 달라지는데

 

import Foundation

struct Person: Encodable {
  var name: String
  var age: String
  var gender: String
  var majorJob: Job
  
  enum CodingKeys: CodingKey {
    case name, age, gender, job
  }

  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(name, forKey: CodingKeys.name)
    try container.encode(age, forKey: .age)
    try container.encode(gender, forKey: .gender)
    try container.encode(majorJob.name, forKey: .job)
  }
}

extension Person: Decodable {
  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    name = try container.decode(String.self, forKey: .name)
    age = try container.decode(String.self, forKey: .age)
    gender = try container.decode(String.self, forKey: .gender)
    let job = try container.decode(String.self, forKey: .job)
    majorJob = Job(name: job)
  }
}

struct Job: Codable {
  var name: String
}

let henrysJob = Job(name: "developer")
let henry = Person(name: "henry", age: "28", gender: "male", majorJob: henrysJob)

let encoder = JSONEncoder()
encoder.outputFormatting = [.sortedKeys, .prettyPrinted]
let encodingData = try encoder.encode(henry)
print(String(data: encodingData, encoding: .utf8)!)

let decoder = JSONDecoder()
let decodingData = try decoder.decode(Person.self, from: encodingData)
 

 

눈여겨 볼 부분은

extension Person: Decodable {
  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    name = try container.decode(String.self, forKey: .name)
    age = try container.decode(String.self, forKey: .age)
    gender = try container.decode(String.self, forKey: .gender)
    let job = try container.decode(String.self, forKey: .job)
    majorJob = Job(name: job)
  }
}
 

이 부분으로

encoder와 마찬가지로 컨테이너를 생성하고

Person 구조체 속성들에게 각각 decode에서 저장하는 방법이다.

Job도 구조체 이기 때문에 문자열 값을 job에 저장한 다음에 majorJob = Job(name: job)으로 생성한다.

 

REST API를 통해서 json을 받을 때는 디코딩 상황만 신경 쓰면 되니까 바로 위 디코딩 extension만 눈여겨 보면 되겠다.

 

이번에는2중 3중 계층이 되어있을 때 방법이다.

이거 중요

Part1 에서 만들었던 것을 가져왔다

struct Person: Encodable {
  var name: String
  var age: String
  var gender: String
  var majorJob: Job
}

struct Job {
  var name: String
  var categories: [JobCategory]
}

struct JobCategory {
  var name: String
}

let henrysJobCategories = [JobCategory(name: "Computer"),JobCategory(name: "IT"),JobCategory(name: "SoftwareDeveloper"),JobCategory(name: "iOSDeveloper")]
let henrysJob = Job(name: "developer", categories: henrysJobCategories)
let henry = Person(name: "henry", age: "28", gender: "male", majorJob: henrysJob)
 

Person은 name, age, gender, majorJob(Job)을 가지고 있고

Job은 name과 categories(JobCategory)를 가지고 있다.

JobCategory는 category를 가지고 있다.

위와 같은 상황에서 아래와 같이 JSON 데이터를 인코딩 해서 내보내고자 한다.

{
  "name" : "henry",
  "age" : "28",
  "gender" : "male",
  "job" : {
    "categories" : [
      {
        "name" : "Computer"
      },
      {
        "name" : "IT"
      },
      {
        "name" : "SoftwareDeveloper"
      },
      {
        "name" : "iOSDeveloper"
      }
    ],
    "name" : "developer"
  }
}
 

인코딩 코드

import Foundation

struct Person: Encodable {
  var name: String
  var age: String
  var gender: String
  var majorJob: Job
  
  //1.
  enum CodingKeys: CodingKey {
    case name, age, gender, job
  }
  
  //2.
  enum JobKeys: CodingKey {
    case name, categories
  }

  //3.
  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(name, forKey: CodingKeys.name)
    try container.encode(age, forKey: .age)
    try container.encode(gender, forKey: .gender)
  //4.
    var jobContainer = container.nestedContainer(keyedBy: JobKeys.self, forKey: .job)
    try jobContainer.encode(majorJob.name, forKey: .name)
    try jobContainer.encode(majorJob.categories, forKey: .categories)
  }
}

struct Job: Codable {
  var name: String
  var categories: [JobCategory]
}

struct JobCategory: Codable {
  var name: String
}
 

헷갈리니까 찬찬히 보자.

1. Person의 키들, 가장 바깥 데이터로 JSON으로 보면

{
  "name" : "henry",
  "age" : "28",
  "gender" : "male",
  "job" : {job의 내용}
}
 

이렇게 표시되길 원한다.

 

2. 그다음 JobKeys는 job의 내용이

//job
{
  "name" : "developer"
  "categories" : {categories의 내용}
}
 

이렇게 표시되길 원한다.

 

3. 컨테이너를 생성하고 연결 해 준다.

  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(name, forKey: CodingKeys.name)
    try container.encode(age, forKey: .age)
    try container.encode(gender, forKey: .gender)
    ...
  }
 

여기까지는 원래 했던대로 CodingKeys 이름 그대로 연결 해준다.

 

4. nestedContainer 생성

   func encode(to encoder: Encoder) throws {
    ...

    var jobContainer = container.nestedContainer(keyedBy: JobKeys.self, forKey: .job)
    try jobContainer.encode(majorJob.name, forKey: .name)
    try jobContainer.encode(majorJob.categories, forKey: .categories)
  }
 

job의 내용이 계층적으로 표시되길 원하기 때문에

jobContainer를 생성하고 container.nestedContainer 생성한다. 만약 이렇게 하지 않고 바로 아래 코드들을 쓰면 flat하게 써진다.

이 경우 keyedBy는 Job 내용을 표시할 JobKeys 이고 forKey는 CodingKeys에서 가져온 .job이다,

고쳐 쓰자면

var jobContainer = container.nestedContainer(keyedBy: JobKeys.self, forKey: CodingKeys.job)
 

위와 같다.

 

디코딩의 경우 의외로 너무 심플했는데

위 encode 코드에서는 전혀 건드릴 것 없고 extension 으로 Person 에 Decodable 프로토콜만 채용한다.

extension Person: Decodable {
  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    name = try container.decode(String.self, forKey: .name)
    age = try container.decode(String.self, forKey: .age)
    gender = try container.decode(String.self, forKey: .gender)
    majorJob = try container.decode(Job.self, forKey: .job)
  }
}
 

nested이지만 따로 nested 를 해줄 것도 없이 그냥 저렇게만 해도

print(decodingData.age)
print(decodingData.name)
print(decodingData.gender)
print(decodingData.majorJob.name)
let henrysJobCategoriesPrint = decodingData.majorJob.categories

henrysJobCategoriesPrint.forEach {
  print($0.name)
}
 
28
henry
male
developer
Computer
IT
SoftwareDeveloper
iOSDeveloper
 

데이터는 정상적으로 접근할 수 있었다.

 

만약에 job의 name을 커스텀 키로 name이 아니고 jobTitle로 쓴다면 위와 같이 작성하면 키가 일치하지 않기 때문에 오류가 발생한다.

 

그런 경우는 Person decodable은 그대로 두고 Job에서 또 custom codingkey를 정의해주면 된다.

struct Job: Encodable {
  var name: String
  var categories: [JobCategory]
  
  enum CodingKeys: CodingKey {
    case jobTitle, categories
  }
  
  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(name, forKey: .jobTitle)
    try container.encode(categories, forKey: .categories)
  }
}

extension Job: Decodable {
  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    name = try container.decode(String.self, forKey: .jobTitle)
    categories = try container.decode([JobCategory].self, forKey: .categories)
  }
}
 

 

Part2 정리

1.custom coding key를 사용할 수 있다.

2.JSON계층을 평탄화 하여 인코딩 할 수 있다.

3.평탄화 된 JSON 데이터를 내 입맛대로 맞게 구조적으로 가져올 수 있다(평탄화 된 구조를 다시 계층 구조로)

- 이부분이 어려웠는데 그래도 JSON에서 제공하는 데이터를 그대로 써서 전체 코드의 일관성이나 가독성을 해칠 경우 입맛대로 오브젝트화 할 수 있기 때문에 장기적으로 보자면 중요한 내용인 것 같다. 가장 이해가 안되었던 부분인데 잘 정리 된 것 같다.

 

Part3 내용도 경우에 따라서 중요할 것 같은데 개인적으로는 Part2에서 다룬 내용에 비하면 쉬운 것 같다.

 

 

Comments