2022. 4. 24. 18:45ㆍSWIFT
블로그에 너무 소홀했는데, 오랜만에 정신을 차리고 다시 포스팅을 진행해보려고 합니다 🥲
오늘은 그동안 스터디를 진행하면서 이 부분은 꼭 따로 공부해서 써봐야지 했던 부분인 Codable에 관한 부분입니다.
보통은 JSON 모델을 요리 조리 볶을 때, Codable을 채택해서 편하게 Decoding, Encoding을 하고 있는데 여기서 Codable에 대해 조금 더 깊게 알아보려고 합니다.
서론
우선 Codable을 알아보기 전에 Serailization, Deserailization이라는 용어에 대해서 간단히 알아볼게요. 우선 Codable을 사용하는 이유라고 할 수 있는데요. Swift의 데이터 타입을 외부에서 사용할 수 있는 데이터(external representation) 타입으로 변환하기 위해서 사용하는데, 이런 기술을 Serailization이라고 합니다.
그렇다면 반대로 외부에서 사용하는 데이터(external representation)를 Swift의 데이터 타입으로 변환하는 기술은 Deserailization이겠죠? 저도 사용해보지는 않았지만 이전에는 NSCoding을 이용해서 이 Serailization 기술을 사용하고 있었는데, NSCoding에서는 너무 큰 추상화를 제공해서 많은 이슈들이 존재했던 것 같고 이를 보완하기 위해 Codable이라는 프로토콜이 제공되는 것 같네요 ㅎㅎ
Codable
우선 Codable은 프로토콜로서 아래의 합성어입니다. Decodable & Encodable을 모두 채택하고 있는 프로토콜이라는 뜻입니다.
즉, 채택한 모델은 Decoding, Encoding이 모두 가능하다는 뜻입니다.
typealias Codable = Decodable & Encodable
Decodable or Encodable은 Encoding, Decoding을 위한 추상화를 제공하는데, 이를 채택함으로서 JSONEncoder, JSONDecoder에서 어떻게 이 모델을 지지고 볶을지 방법을 제공하게 됩니다.
우리는 간단하게 사용하지만 내부적으로는 해당 프로토콜을 통해 방법을 제공하고 있기 때문에, 이를 이용하면 더 다양한 형식으로 Encoding, Decoding이 가능하게 됩니다 🥲
이제부터 지금까지 사용했던 간단한 방법 외에 좀 더 심화된 예제들을 통해 사용을 해볼게요!!
가당 간단한 케이스
struct Person: Codable {
let name: String
let twitter: String
let github: URL
let birthday: Date
}
// Decoding
let decoder = JSONDecoder()
let person = try? decoder.decode(Person.self, from: jsonData)
// Encoding
let encoder = JSONEncoder()
let jsonData = try? encoder.encode(jsonData)
요렇게 사용함으로 간단하게 Decoding, Encoding이 가능해집니다.
Decoding, Encoding을 하는 방법은 Codable을 채택함으로서 내부에서 알고있겠죠 ㅎㅎ
Key Decoding Strategies
[
{
"name": "Dongmin",
"age": 20,
"github_link": "https://github.com/dongminyoon"
}, ...
]
// JSON
struct Person: Codable {
let name: String
let age: Int
let githubLink: String
}
do {
let decoder = JSONDecoder()
let person = try decoder.decode([Person].self, from: jsonData)
} catch let error {
print(error)
}
만약 이런 JSON이 있고 Decoding을 하고 싶을 때, 위의 코드와 같이 실행하면 성공할까요..??
우선 name, age 키 값에서는 큰 문제는 보이지 않는데 나머지 github_link의 키 값이 JSON과 상이하죠 그렇기 때문에, 다음과 같은 에러가 떨어집니다 😭
keyNotFound(CodingKeys(stringValue: "githubLink", intValue: nil), Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 0", intValue: 0)], debugDescription: "No value associated with key CodingKeys(stringValue: \"githublink\", intValue: nil) (\"githublink\").", underlyingError: nil))
예로 지금 같은 경우는 서버에서는 키 값을 snake case를 활용하고 있고 클라이언트에서는 snake case를 활용하지 않고 받고싶은 경우라고 볼 수 있겠죠? 이 같은 경우는 다음과 같이 간단하게 decodingStarategy를 지정하는 것으로 사용이 가능합니다.
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let person = try decoder.decode([Person].self, from: jsonData)
} catch let error {
print(error)
}
Data Decoding Strategies
{
"name": "Clean Code",
"imageData": "iVBORw0KGgoAAAANSUhEUgAAAfAAAAHGC..." // base64 String
}
// JSON
struct Book: Codable {
let name: String
let imageData: Data
var image: UIImage? { UIImage(data: self.imageData) }
}
do {
let decoder = JSONDecoder()
decoder.dataDecodingStrategy = .base64
let book = try decoder.decode(Book.self, from: jsonString)
} catch let error {
print(error)
}
이번에는 예로 base64로 Encoding된 String을 받는 경우 사용할 수 있는 방법입니다.
String을 받는 경우 별도로 String을 받아서 Data(base64Encoded: base64String)와 같은 과정이 필요할 수 있지만 dataDecodingStrategy를 이용하면 이와 같은 과정을 생략할 수 있습니다 🙃
아!! 그리고 원래 JSONDecoder의 dataDecodingStrategy의 기본 값이 .base64 이므로 따로 설정하지 않아도 해당 예제처럼 받을 수 있습니다 ㅎㅎ
Custom Key Decoding Strategies
이번에는 Swift Standard Library에 정의되어 있지 않은 경우의 커스텀한 Key Decoding을 하는 경우입니다.
우선 상황을 가정하면 위에서 사용했던 snake case 이외에 '-'로 구분을하는 kebab case로 내려오면 어떻게 해야할까요 🤔
어쩔수없이 받기 위해서는 통일을 하던가 그게 싫다면 별도로 무언가를 구현해야겠죠?!
이 때 사용할 수 있는 것이 decodingStrategy = .custom 설정입니다.
{
"level-One": "Mushroom",
"level-Two": "Ant"
}
// JSON
struct Level: Codable {
let levelOne: String
let levelTwo: String
}
이런식으로 받아야하는데 이미 키 값이 다르잖아요...😭
자 우선 .custom 정의를 볼게요 case custom((_ codingPath: [CodingKey]) -> CodingKey) 연관 값으로 클로저를 받고 있죠. 아마 저게 custom 한 정의를 위해 사용하는 클로저이고 내부에서 불리면서 키들을 매칭하는 것 같아요. 이제 사용을 해볼게요.
struct AnyCodingkey: CodingKey {
let stringValue: String
let intValue: Int?
init?(stringValue: String) {
self.stringValue = stringValue
self.intValue = nil
}
init?(intValue: Int) {
self.intValue = intValue
self.stringValue = "\(intValue)"
}
}
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .custom { keys in
let codingKey = keys.last! // 1
let key = codingKey.stringValue // 2
guard key.contains("-") else {return codingKey } // 3
let words = key.components(separatedBy: "-") // 4
let camelCased = words[0] + words[1...].map(\.capitalized).joined()
return AnyCodingkey(stringValue: camelCased)! // 5
}
let someData = try decoder.decode(SomeData.self, from: someJSON)
print(someData)
} catch let error {
print(error)
}
- 우선 custom의 정의처럼 [CodingKey]가 파라메터로 들어오는데 이 값은 codingPath로 Decoding에서 이 값을 받아오는데 사용되는 Key의 경로라고 합니다. 가장 마지막 값만 JSON Key 값이 들어오는데, 그래서 last 값만 사용해요.
- Key 값의 stringValue를 가져와야겠죠 (ex. level-One, level-Two)
- kebab case인 경우는 '-'를 포함하기 때문에, 포함하지 않는 경우 그대로 리턴
- '-'을 기준으로 나누어줍니다
- 마지막으로 리턴값이 CodingKey 타입이잖아요. 이를 위해 지금 만들었던 키 값을 stringValue로 AnyCodingKey라는 인스턴스를 리턴해줍니다
이런식으로 사용하면 조금 다른 키 값들도 커스텀하게 Decoding이 가능해집니다. 반복적으로 이런 키값들이 들어오고 커스텀해야한다면 static으로 정의해서 사용하면 의미있을 것 같네요 🙃
CodingKeys
이번에는 CodingKey에 대해 알아보려고 합니다. 그렇다면 CodingKey는 무엇일까요?
Swift에 구현된 것을 보면 프로토콜의 형태를 가지고 있고 사용할 때에는 특정 키에 대해 커스텀하게 표현할 수 있는 키로 매칭이 가능했습니다. 특정 속성의 키가 어떻게 표현되는지를 설명하기 위해 도와주는 프로토콜로 이해하면 될 것 같네요?
protocol CodingKey {
var stringValue: String { get }
var intValue: Int? { get }
init(stringValue: String)
init?(intValue: Int)
}
사실 이전에 Decoding을 진행할 때에도 JSON의 값과 완벽하게 키 값이 일치했다면 별도의 과정이 필요없지만 일치하지 않는 경우에 사용했었잖아요? CodingKey도 이와 같은 경우에 사용할 수 있습니다.
앞에서 사용했던 에시를 가져와볼게요
[
{
"name": "Dongmin",
"age": 20,
"github_link": "https://github.com/dongminyoon"
}, ...
]
// JSON
struct Person: Codable {
let name: String
let age: Int
let githubLink: String
enum CodingKeys: String, CodingKey {
case name, age
case githubLink = "github_link'
}
}
이전에는 keyDecodingStrategy을 지정해서 snake case인 키 값을 매칭했었죠? CodingKey를 이용해서 커스텀하게 명시적으로 키 값을 지정할 수 있습니다.
또 만약 JSON으로부터 값을 받는데, JSON 값에는 있을 수도 있고 없을 수도 있는데 기본값으로 받고싶은 경우는 다음과 같이 CodingKey를 이용해 가능합니다.
extension Person {
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
self.name = try values.decode(String.self, forKey: .name)
self.age = try values.decode(Int.self, forKey: .age)
// GithubLink가 JSON에서 없지만 Default로 써야하는 경우
self.githubLink = try values.decodeIfPresent(String.self, forKey: .githubLink) ?? "https://github.com"
}
}
우선 오늘은 Codable에 대해 조금(?) 심화된 사용법들을 알아보았는데요.
다음 포스팅에서는 좀 더 자세한 내용의 Codable에 대해 이어서 알아보겠습니다 🙃
오늘 내용이 도움이 되었으면 좋겠네요~
혹시 잘못된 점이나 궁금한 점이 있으면 말씀해주세요
'SWIFT' 카테고리의 다른 글
[Swift] Macro (1) (1) | 2024.09.16 |
---|---|
[Swift] Codable (2) (0) | 2022.09.14 |
[SWIFT] ArraySlice (2) | 2022.02.25 |
[SWIFT] String Compare (앱 버전 비교) (0) | 2021.10.10 |
[SWIFT] KVO (Key - Value - Observing) (0) | 2021.09.26 |