[Swift] Codable (1)

2022. 4. 24. 18:45SWIFT

반응형

블로그에 너무 소홀했는데, 오랜만에 정신을 차리고 다시 포스팅을 진행해보려고 합니다 🥲

오늘은 그동안 스터디를 진행하면서 이 부분은 꼭 따로 공부해서 써봐야지 했던 부분인 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를 이용하면 이와 같은 과정을 생략할 수 있습니다 🙃

 

아!! 그리고 원래 JSONDecoderdataDecodingStrategy의 기본 값이 .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)
}
  1. 우선 custom의 정의처럼 [CodingKey]가 파라메터로 들어오는데 이 값은 codingPath로 Decoding에서 이 값을 받아오는데 사용되는 Key의 경로라고 합니다. 가장 마지막 값만 JSON Key 값이 들어오는데, 그래서 last 값만 사용해요.
  2. Key 값의 stringValue를 가져와야겠죠 (ex. level-One, level-Two)
  3. kebab case인 경우는 '-'를 포함하기 때문에, 포함하지 않는 경우 그대로 리턴
  4. '-'을 기준으로 나누어줍니다
  5. 마지막으로 리턴값이 CodingKey 타입이잖아요. 이를 위해 지금 만들었던 키 값을 stringValueAnyCodingKey라는 인스턴스를 리턴해줍니다

이런식으로 사용하면 조금 다른 키 값들도 커스텀하게 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] 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
[SWIFT] KVC (Key - Value - Coding)  (0) 2021.09.20