Swift

[Swift] 프로토콜 뽀개볼까 (3) | Codable (feat. Encodable & Decodable), JSON이란?

하이D:) 2024. 4. 28. 12:45

 

그동안 Codable은 Encodable과 Decodable을 합쳐 놓은 거다 이렇게 이해하고 쓰곤 했었는데

이해하는 거랑 정리해 놓는 거랑은 또 다르니까 한 번 정리해보려 합니다!

 

 

📍공식문서

일단 공식문서를 보면

프로토콜도, 클래스도, 구조체도 아닌 Type Alias 라고적혀 있네요?

 

 

 

그럼 typealias 란 무엇인가?

-> 직역하면 '타입 별명'입니다.

 


예시를 좀 보자면

이렇게 함수에 클로저를 전달할 때, 매개변수에 이런 식으로 타입을 나타내죠

func functionName(completionHandler : () -> Void ) {

}

 

근데 (() -> Void) 타입에 대한 별칭을 주고 그 별칭으로 치환해서 사용할 수도 있는 것입니다.

typealias Handler = () -> Void
func functionName(completionHandler : Handler) {

}

 

 

 

또 다른 예시로는 딕셔너리에서 키의 타입을 String으로 정해놓고 밸류 타입만 제네릭으로 타입을 지정해 준다면

이런 식으로도 사용할 수 있습니다.

typealias StringKeyDictionary<ValueType> = Dictionary<String, ValueType>
let dictionary : StringKeyDictionary<Int> = ["1" : 1, "2" : 2]

 


 

 

 

이러한 typealias를 사용해서 프로토콜을 여러 개 채택하는 것에 대한 typealias도 만들어줄 수 있는데요.

이렇게 작성된 코드를 

extension ViewController : UITableViewDataSource, UITableViewDelegate {
}

 

 

이렇게 바꾸어줄 수 있죠!

typealias TableViewDelegate = UITableViewDataSource & UITableViewDelegate
extension ViewController : TableViewDelegate {
}

 

 

이것도 하나의 예시가 될 수 있어요.

typealias Protocols = Equatable & Hashable
struct Struct: Protocols{
}

 

 

 

 

 

자 어쨌든 공식문서로 돌아와서

Codable이라는 것도 보니 Decodable이라는 프로토콜과 Encodable이라는 프로토콜을 결합시킨 타입별칭(typealias)라고 나와있습니다.

typealias Codable = Decodable & Encodable

 

 

 

 

정의

공식문서의 정의는  "A type that can convert itself into and out of an external representation." 인데, 번역해 보면 자신을 외부 표현으로 변환하거나 외부 표현으로 변환할 수 있는 형식입니다.???? (번역은 한계갸 있죠..?ㅎㅎㅎ)

 

 

이게 무슨 소리인가 하면 

  • Codable을 준수하는 어떤 것을 외부 표현으로 변환할 수도, (인코딩과 관련 있음)
  • 외부 표현을 Codable을 준수하는 어떤 것으로 변환할 수도 있다는 뜻! (디코딩과 관련 있음)

 

이해하기 더 어렵죠..?

=> 어쨌든 Decodable와 Encodable을 결합해 놓은 개념이기 때문에 이 두 개념을 합쳐놓은 거라고 보면 되는데, 일단 인코딩 디코딩과 관련이 있는 정의이구나라고 알아두고 넘어가면 될 것 같아요!

 

아마 이 글을 끝까지 읽으면 이해가 될 거예요!

 

 

 

 

 

 

📍JSON

이 Codable이라는 것은 우리가 서버와 통신할 때 주고받는 JSON데이터 형식과 연관이 깊은데요

정의에서 외부표현으로 변환할 수 있다는 것에서 “외부 표현”이라는 것도 일단은 이 JSON 데이터 형식이라고 이해하면 될 것 같습니다.

 

 

그래서 JSON이 뭔데

라는 의문을 가질 사람들을 위해 간단히 정리하자면

  • JavaScript Object Notation의 축약어
  • Swift는 자바스크립트가 아닙니다.. 만!! Json이 객체 속성을 표현하기에 좋은 방법이었으므로 다른 언어들도 사용해서 HTTP 통신에 필수 요소가 되어버린 것임
  • 데이터를 저장하거나 전송할 때 많이 사용되는 경량의 Data교환방식
  • 사람과 기계 모두 이해하기 쉬운 형태고 용량이 작기 때문에 데이터 전송에 많이 쓰이는 것이라고! 합니다아
  • JSON이라는 하나의 거창한 용어로 불리니 프로그래밍 상의 문법이나 특정 통신 방법이라고 생각할 수도 있는데 그게 아니라!! 그냥 데이터 표시 방식일 뿐 그 이상도 그 이하도 아니고요!

 

JSON의 특징은

  • 자바스크립트에서 사용하는 객체 표기 방식과 매우 유사한데! (중괄호 안에 key-value가 쌍을 지어 있는 형태)
  • key-value라고 하면 우리가 사용하는 swift에서는 딕셔너리를 생각해 볼 수도 있을 것 같은데요
  • but, JSON은 전체 String 형태입니다. {key-value}이런 형태가 쌍 따옴표로 둘러싸여 있는 거라고 보면 돼요.
  • JSON은 key와 value에 형식의 제한이 있습니다.
    • key는 오로지 문자열만!
    • value는 문자열, 숫자, 논리값, 배열, Json객체, null로 제한됩니다.
  • 이렇게 만든 객체 형태를 배열로 쓰고 싶을 때 배열 안에 JSON객체 요소로 넣는 것 가능

 

 

JSON 어떻게 만드는데?

자바스크립트 아니어도 만들 수 있어?

  • Yes → JS가 아닌 다른 언어에서도 JSON 포맷의 데이터 핸들링할 수 있는 도구들을 제공하는데
  • Swift에서는JSONEncoder(), JSONDecoder()라는 게 있죠.

 

 

 

 

 

📍Encoding, Decoding

JSON을 알아봤으니 이제 우리가 만드는 객체형태 (구조체나 클래스의 인스턴스)를 어떻게 JSON화 하고 JSON 형태로 받은 데이터를 어떻게 우리가 사용하는 객체형태화하는지 알아보자

 

 

 

우리가 사용하는 객체형태 (구조체나 클래스의 인스턴스)를 JSON화 하는 과정

이 경우는 서버에 POST 하거나 할 때 데이터를 JSON 형태로 보낼 때 필요한 과정이라고 볼 수 있습니다!

 

 

struct1에 Struct타입의 인스턴스를 만들어서 할당해 줬고이제 이 인스턴스를 아까 말했던 JSONEncoder()를 가지고 JSON으로 만들어주었습니다. 

struct Struct{
    var string : String
    var int : Int
}

let struct1 = Struct(string : "", int : 0)

// MARK: - encoding
//인스턴스 -> JSON
let jsonData = try? JSONEncoder().encode(struct1)
print("JSON 데이터 형식으로 encoding -> ",jsonData)

 

 

 

그럼 잘 될까요??

택도 없지 ㅎㅎㅎㅎ 에러가 나죠?

🚨Instance method 'encode' requires that 'Struct' conform to 'Encodable’

인코딩해줄 건데 왜 인스턴스의 타입인 Struct가 ‘Encodable’을 준수하지 않냐!는겁니다

 

 

 

이렇게 Encodable 채택해 주면 에러 해결!

즉, "너가 인코딩해서 JSON형태로 만들 타입은 Encodable이라는 프로토콜을 준수해야 해"라는 것을 여기서 알 수 있죠

struct Struct : Encodable{
    var string : String
    var int : Int
}

 

 

 

 

 

JSON 형태로 받은 데이터를 우리가 사용하는 객체형태로 만드는 과정

이경우는 서버의 응답으로 JSON 형태의 데이터가 왔을 때 우리가 활용 가능한 인스턴스로 만들기 위해 필요한 과정입니다!

 

 

그럼 이 경우에는 어떨까요?

아까 만들어준 JSON 형태를 서버에서 받는 데이터라고 생각하고 우리가 쓸 수 있도록 다시 객체화해 주는 겁니다! 이때 아까 말한 JSONDecoder()를 쓰구요.

 

 

 

그래서 해주는데..? 에러

🚨Instance method 'decode(_:from:)' requires that 'Struct' conform to 'Decodable’

아까 인코딩에서 에러 났던 것처럼 디코딩해 줄 건데 왜 인스턴스의 타입인 Struct가 ‘Decodable’을 준수하지 않냐!는겁니다

 

 

 

 

그래요.. 다 개발자가 잘못이죠

그들이 원하는 대로 Decodable 채택해 주니 에러가 없어집니다~

struct Struct : Encodable, Decodable{
    var string : String
    var int : Int
}

 

 

 

 

 

 

📍Codable

그럼 결과적으로 인코딩하고 디코딩할 때 쓰이는 타입은 Encodable, Decodable를 모두 준수하는 형태가 되는데요

 

 

Encodable, Decodable를 모두 준수한다.. Encodable, Decodable가 결합된 상태이다..

흠.. 아까 뭐 봤던 게 생각나죠? 바로 Codable!!!!

 

 

Encodable, Decodable채택한 자리에다가 Codable 써주면 더 간단하고 명료하죠

struct Struct : Codable{
    var string : String
    var int : Int
}

 

그니까 사실상 개발자들은 디코딩하거나 인코딩에 사용할 타입의 경우에 Codable을 채택해놔 주면 JSON에서 디코딩을 하건 JSON형태로 인코딩을 하건 신경 써줄 필요 없게 되는 거죠

 

 

 

 

 

📍 Codable과 CodingKey

근데 우리가 사용하는 객체 형태와 받는 데이터 형식의 키값이 다를 수도 있는 것 아니겠어요??

예를 들어 우리는 ParsingStruct 타입의 인스턴스를 사용할 건데

//우리는 이런 프로퍼티를 가진 타입의 인스턴스로 파싱하고싶은데
struct ParsingStruct {
    var key1 : Int
    var key2 : Int
    var key3 : Int
}

 

 

서버에서 오는JSON 형태가 serverJson이런 형태라고 하면

JSON의 키값과 ParsingStruct의 프로퍼티가 다르기 때문에 우리가 원하는 인스턴스로 파싱이 안 되겠죠?

//만약 서버에서 오는 JSON이 이럴 때
let serverJson = """
{
"key_1" : 1,
"key_2" : 2,
"key_3" : 3,
}
""".data(using: .utf8)!

 

 

 

 

그럼 우리가 서버에서 결정한 키값에 맞추어서 우리 프로퍼티를 정해주어야만 하는가?
우리도 우리만의 코드를 적는 규칙이 다 있는데 말이죠?

그렇다고 서버 측에 언더바 빼고 줘봐 봐.. 할 수도 없는 일!
백엔드도 백엔드만의 사정이 다 있숨다

 

 

그니까 백엔드로부터 키값에 대해 공유를 받았다면 우리는 알아서 받은 데이터를 파싱 할 때 우리가 사용하려는 형태에 맞게 바꾸어서 사용해줘야 하는 것입니다~~

 

 

그래서 이럴 때 사용할 수 있는 게 바로 CodingKey 입니다.

CodingKey는 프로토콜이며, “인코딩 및 디코딩을 위한 키로 사용할 수 있다”고 나와있습니다.

 

 

 

 

 

사용은 이렇게 해주면 되는데 CodingKeys라는 enum에 rawValue를 String으로 지정해 주었고 앞에서 말한 프로토콜인 CodingKey를 채택해 주었네요!

프로토콜인 CodingKey열거형 CodingKeys를 헷갈리면 안 됩니다!

 

이렇게 하면 JSON 데이터를 디코딩할 때 "key_1", "key_2", "key_3" 로 오는 키 값을 key1, key2, key3 에 매칭시켜서 디코딩 해줄거야! 라고 명령해 주는 겁니다!

struct ParsingStruct : Codable {
    var key1 : Int
    var key2 : Int
    var key3 : Int
    
    enum CodingKeys: String, CodingKey {
        case key1 = "key_1"
        case key2 = "key_2"
        case key3 = "key_3"
    }
}

let instance = try? JSONDecoder().decode(ParsingStruct.self, from: serverJson)
print("객체 형식으로 decoding -> ", instance)

 

 

 

 

 

 

 

근데 여기서 궁금증이 하나 있었는데 "CodingKeys라는 열거형 이름은 바꾸면 안되는건가?" 였습니다.

그래서 다른 이름으로 바꿔봤는데 디코딩되지 않습니다! ㅠㅠ..

 

⭐️왜 그럴까? 그 해답은 여기에 있습니다. ⭐️

 

 

간단하게 번역해 보자면

  • Codable 유형은 CodingKey 프로토콜을 준수하는 CodingKeys라는 특수 중첩 열거형을 선언할 수 있습니다.
  • 이 열거형이 존재하는 경우 해당 케이스는 코딩 가능한 유형의 인스턴스가 인코딩 되거나 디코딩될 때 포함되어야 하는 신뢰할 수 있는 속성 목록 역할을 합니다.
  • 열거형 케이스의 이름은 유형의 해당 속성에 지정한 이름과 일치해야 합니다.

 

 

 

자 보니까 명쾌하죠??

 

CodingKeys는 Swift에서 “CodingKeys라는 특수 중첩 열거형” 으로 지정해 놓은 거였어요! 바꾸면 안 됩니다! ㅎㅎ

그리고 여기서의 케이스 이름은 해당 속성에 지정한 이름과 일치해야 한다고 합니다! 즉. 프로퍼티명과 케이스 이름 일치시키기!!도 중요한 것이죠

 

 

 

 

 

 

 

 

이렇게 Codable, Encodable, Decodable 에 대해서 정리해 보았는데요.

역시 당연하게 쓰던 것도 정리를 하다 보면 막히는 게 있어서 새로운 궁금증을 정리하는 재미가 있네요! :)

 

틀린 내용이나 덧붙일 내용 있다면 댓글로 달아주세요!