본문 바로가기

Swift

Codable

728x90

서버 통신을 하게 된다면 한번쯤 만나게 되는 Codable !!

관련해서 Encodable / Decodable도 알아보자.

 

Codable? Encodable? Decodable?

위의 개념을 왜 알아야할까?

(이렇게 안하면 오류가 나니까 라고 하면 틀린 말은 아니지만 .. 친절하지 못한 설명이니 .. pass)

 

클라가 서버에 요청을 하면,

서버에서는 요청을 바탕으로 응답 메시지를 전달한다.

 

위의 과정을 잘 이루어지기 위해서는 클라-서버는 서로 정해진 형태로 요청과 응답을 주고 받아야 한다.

 

이 때 주로 HTTP 프로토콜을 준수해서 요청을 하게 되고, (클라이언트 -> 서버)

HTTP 방식으로 요청할 때에는 여러가지 방법(= 메서드)이 존재한다.

- GET

- HEAD

- POST

- PUT

- DELETE

- OPTIONS

- TRACE

- PATCH 

그리고 서버는 요청한 메서드에 맞춰서 Response를 클라이언트에게 전달한다.

 

 

JSON

둘 사이의 메시지는 주로 JSON 포맷의 데이터로 주고 받는다.

 

JSON은

Java Script Object Notation의 약자로 

데이터를 저장하거나 전송할 때 사용하는 형식이다. (= 데이터를 표시하는 표현 방법)

  • key-value 형태로
  • 모두 "" 를 이용해 표기하고
  • 객체, 배열 등을 사용할 수 있으며
  • 중첩이 가능한 형태이다. 

 

그래서 클라-서버의 네트워크 방식은

> 클라이언트가 서버에게로 해당 URI를 통해 GET 요청을 하면

> JSON 형태로 데이터를 받는 것이다.

 

 

🟡 서버에서 클라이언트로 JSON 형태의 파일을 넘겨주게 된다면 이 데이터를 어떻게 처리할까?

🟡 반대로 서버에게 요청 메시지를 보낼 때 어떤 형태로 보낼까?

.. 여기서 나오는 개념이 Decode / Encode이다.

 

🟢 서버에서 받은 JSON 데이터를 Swift의 문법으로 사용할 수 있도록 하는 것을 Decode라고 하고

(= data를 원하는 모델로 변환)

🟢 Decode의 반대 과정을 Encode라고 한다. 

(= 모델을 원하는 data로 변환)

 

 

JSON을 Decode해서 데이터 모델로 만드려면 Decodable 이라는 프로토콜을 채택해야 한다.

데이터 모델을 Encode해서 JSON으로 넘기려면 Encodable 이라는 프로토콜을 채택해야 한다.

그리고 이 두가지가 모두 가능한 프로토콜이 바로 Codable이다.

 


서론이 엄청 길었네 .. ㅋㅋ

이제 어떻게 사용하는지 알아보자.

 

Codable

JSON과 같은 외부 표현과의 호환성을 위해 데이터 유형을 인코딩 및 디코딩 할 수 있는 프로토콜을 말한다.

 

 

JSON 뿐만 아니라 디스크에 저장, 네트워크 연결 등을 위한 API 통신 등의 작업에서는 데이터가 전송되는 동안 중간 형식으로 데이터를 인코딩/디코딩 해야 하는 경우가 많은데 Swift 표준 라이브러리에서는 Codable을 통해 데이터 인코딩 및 디코딩에 대한 표준화된 접근 방식을 제공하고 있다.

 

Codable은 Decodable 프로토콜과 Encodable 프로토콜에 대한 typealias이므로 양방향 인코딩/디코딩을 지원하지 않는 경우라면 Decodable 프로토콜이나 Encodable 프로토콜을 독립적으로 채택해서 사용할 수 있다.

 

JSONSerialization VS Codable

JSONSerailization은 Swift 4에서 Codable이 등장하기 전까지 사용하던 클래스이다.

 

SON Value 중에서 단일한 값만 가져오는 경우에는 JSONSerailization이 빠르지만

중첩된 JSON 구조 및 여러번 반복해서 데이터를 갖고 와야 하는 경우는 Codable이 빠르다.

 

*Alarmofire와 함께 자주 사용하는 SwiftJSON 라이브러리는 JSONSerailization을 기반으로 만들어졌고 속도가 가장 오래 걸리지만 JSON 객체를 딕셔너리 형태로 접근할 수 있다는 측면에서 장점이 있다.

 


Decodable

Decodable은 원하는 모델로 데이터를 디코딩 할 수 있는 프로토콜

 

✔️ 구조체, 열거형, 클래스에서 모두 채택할 수 있다.

✔️ Key의 일부를 디코딩하지 않는 것은 문제가 없다.

     ㄴ 하지만, Key 값이 동일하지 않거나 호환되지 않는 형식이 있다면 디코딩에 실패하기 때문에 별도의 처리를 해야한다. 

 

여기서 말하는 실패 상황은 아래와 같다.

  • 스펠링 오류 (눈 크게 떠라)
  • 서버의 키 값과 다른 키를 사용할 경우 
    • ex) author_name을 모델에서 authorName으로 선언한 경우
    • ex) API 업데이트로 서버의 키 값이 변경된 경우 
  • 옵셔널 타입으로 선언하지 않은 키에 nil 값이 올 경우

 

✅ Decodable을 채택하고 위 상황 등에 대해서 별도의 처리가 필요한 경우 init을 모델 내에서 생성/구현한다. 

 


Decodable을 채택해서 JSON 데이터를 원하는 모델로 변환하는 과정은 아래와 같다.

 

서버에서 아래와 같이 데이터를 준다고 할 때 (-> String의 형태로 줄 것)

let json = """
{
"quote": "Count your age by friends, not years. Count your life by smiles, not tears.",
"author": "John Lennon"
}
"""

 

JSON Key와 동일하게 모델을 만든다.

struct Quote: Decodable {
    let quote: String
    let author: String
}

 

이제 두 데이터를 변환하기 위해 디코딩 과정을 거친다.

먼저 String 타입을 데이터 타입으로 변환한 뒤에

guard let result = json.data(using: .utf8) else { fatalError("Error") }

JSONDecoder 객체를 통해 Quote(클라에서 원하는 모델) 타입으로 값을 반환한다.

do {
    let value = try JSONDecoder().decode(Quote.self, from: result)
    print(value)
    print(value.quote)
    print(value.author)
} catch {
    print(error)
}

 

여기까지 잘 마무리가 되었다면,

데이터가 잘 나오는 것을 확인할 수 있다.

 


그러나 만약 키 값이 같지 않을 경우에는 디코딩에 실패하게 되는데 이를 해결하기 위해서 Decoding Strategy가 있다.

 

Decoding Strategy : Optional

만약 서버에서 위와 같이 데이터를 넘겨주는 것이 아니라,

let json = """
{
"quote_content": "Count your age by friends, not years. Count your life by smiles, not tears.",
"author_name": "John Lennon"
}
"""

이렇게 키 값을 설정해서 데이터를 넘겨준다고 할 때 그대로 Quote 구조체를 사용할 경우

키를 찾을 수 없다는 오류가 난다.

 

오류 자체는 구조체에서 모델을 옵셔널 타입으로 지정을 해서 (아래와 같이)

struct Quote: Decodable {
    let quoteContent: String?
    let authorName: String?
}

해결할 수 있지만 원하는 데이터로 변환하지 못하는 것은 마찬가지이다.

 

Decoding Strategy : SnakeCase

이럴 때, Snake Case에 대응할 수 있게 설정하면 된다.

let decoder = JSONDecoder() ✅ 
decoder.keyDecodingStrategy = .convertFromSnakeCase ✅ 

// Data -> Quote
do {
    let value = try decoder.decode(Quote.self, from: result)
    print(value)
    print(value.quoteContent)
    print(value.authorName)
} catch {
    print(error)
}

이렇게 서버의 키 값과 모델의 키 값이 스네이크 케이스로 해결이 되는 경우라면 디코딩 전략으로 런타임 오류를 해결할 수 있고 원하는 값도 얻을 수 있다.

 

⚪️ Decoding Strategy : CodingKey

✔️ 원하는 키로 모델을 생성하고 싶거나,

✔️ 기본적으로 제공해주는 디코딩 전략으로 원하는 형태를 얻기가 어려운 경우

-> 커스텀 키를 생성하면 된다.

 

CodingKey 프로토콜로 인코딩과 디코딩을 할 수 있는 키를 생성/사용할 수 있다.

 

열거형 CodingKeys는 항상 내부적으로 생성이 되어 있다

따라서, 커스텀 키가 필요할 때만 CodingKeys를 작성하면 된다.

🟠 주의할 점은

커스텀 키로 사용하지 않을 키도 모두 열거형 CodingKeys에 포함해야 한다. 

(= 즉 모든 속성이 포함되어 있어야 한다. 이 때 커스텀 키로 사용하지 않을 키는 case에 작성한 이름이 그대로 rawValue로 들어가게된다.)

 

let json = """
{
"quote_content": "Count your age by friends, not years. Count your life by smiles, not tears.",
"author_name": "John Lennon"
}
"""

struct Quote: Decodable {
    let ment: String
    let author: String
    
    enum CodingKeys: String, CodingKey {
        case ment = "quote_content"
        case author = "author_name"
    }
}

 

⚪️ Decoding Strategy : init(from decoder: Decoder) & decodeIfPresent

이거 처음에 듣고 와싀; 했음요 ㅋㅋ

 

서버에서 받은 값을 그대로 사용하지 않고 일부 제약 조건을 추가하거나 값에 대한 변형을 하고 싶을 수 있다.

또는 nil 값일 경우 대체할 문자열을 추가하고 싶을 수 있다.

 

디코딩을 한 후에 로직적으로 구현해도 되지만 디코딩을 하면서 원하는 결과를 바로 얻고 싶을 때 용이하다.

(또는 디코딩 결과를 모든 화면에서 같은 로직으로 변경해서 사용하는 경우 등 .. )

 

let json = """
{
"quote_content": "Count your age by friends, not years. Count your life by smiles, not tears.",
"author_name": null,
"likeCount": 12345
}
"""

서버에서 위와 같이 명언, 이름, 좋아요 수를 넘겨줄 때

✔️ null 값이 넘겨서 오는 경우 클라이언트에서 공통적으로 설정할 수 있고

✔️ 좋아요 수에 대해 분기처리를 디코딩 과정에서 할 수 있다.

 

struct Quote: Decodable {
    let ment: String
    let author: String
    let like: Int
    let isInfluencer: Bool // 10000개 이상 좋아요 받은 경우
    
    enum CodingKeys: String, CodingKey { // 내부적으로 선언되어 있는 열거형
        case ment = "quote_content"
        case author = "author_name"
        case like = "likeCount"
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        
        ment = try container.decode(String.self, forKey: .ment)
        author = try container.decodeIfPresent(String.self, forKey: .author) ?? "unknown"
        like = try container.decode(Int.self, forKey: .like)
        isInfluencer = (10000...).contains(like) ? true : false
    }
}

위와 같이 init 과정을 하게 된다면 null 값인 경우 unknown으로 일괄 처리할 수 있고

좋아요 수가 10000개 이상인 경우 인플루언서로 지정할 수 있다. 

 


Encodable

원하는 데이터 모델을 외부로 전달하기 위해 JSON 데이터 등으로 변환할 때 사용하는 프로토콜이다.

 

✔️ 구조체, 열거형, 클래스에서 모두 채택할 수 있다.

✔️ Key의 일부를 인코딩 하지 않는 것은 문제 없다. 

(.. Decodable과 동일)

 


가지고 있는 객체를 JSON 객체로 인코딩 하는 과정은 아래와 같다.

 

struct User: Encodable {
    let name: String
    let age: Int
    let signDate: Date
}

let users: [User] = [
    User(name: "takki", age: 26, signDate: Date()),
    User(name: "huree", age: 25, signDate: Date()),
    User(name: "sokyte", age: 25, signDate: Date())
]

User이라는 구조체가 있고 User의 정보 3개가 배열 형태로 들어있다.

 

위에서 인코딩 과정은 디코딩 과정의 반대라고 말했으므로 순서도 반대로 흘러가게 된다.

디코딩 과정이 String -> Data -> Model로 변환하는 과정이었다면,

인코딩 과정은 Model -> Data -> String으로 변환하면 된다.

 

users를 JSONEncoder의 encode 메서드를 활용해서 Data 타입으로 반환한다. 

그리고 반환된 Data 타입을 String 타입으로 변환한다. 

 

do {
    let result = try JSONEncoder().encode(users)
    print(result)
    
    guard let jsonString = String(data: result, encoding: .utf8) else {
        fatalError("ERROR")
    }
    
    print(jsonString)
} catch {
    print(error)
}

 

이렇게 변환되는 것을 볼 수 있다.

 


Encoding Strategy : OutputFormatting

디코딩 전략이 있는 것처럼 인코딩 전략도 존재한다.

 

JSON 구조에 맞게 결과를 확인하고 싶을 때는 prettryPrinted를 

Key를 정렬하고자 할 때는 sortedKeys라는 outputFormatting을 사용한다. 

*이 두가지를 함께 사용하면 원하는 결과를 보기 어려울 수도 있다.

*이외에도 다른 종류의 outputFormatting이 존재한다.

 

let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted

do {
    let result = try encoder.encode(users)
    print(result)
    
    guard let jsonString = String(data: result, encoding: .utf8) else {
        fatalError("ERROR")
    }
    
    print(jsonString)
} catch {
    print(error)
}

이렇게 변환하면

 

JSON 구조에 맞게 결과가 출력된다.

 

Encoding Strategy : DateFormat

Date의 경우 인코딩/디코딩을 할 때 별도의 전략을 사용하지 않는다면 Double 타입으로 인코딩/디코딩을 하게 된다.

Double 타입은 1970년 1월 1일부터 몇 초가 경과 했는지를 나타낸다.

 

let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
encoder.dateEncodingStrategy = .iso8601

do {
    let result = try encoder.encode(users)
    print(result)
    
    guard let jsonString = String(data: result, encoding: .utf8) else {
        fatalError("ERROR")
    }
    
    print(jsonString)
} catch {
    print(error)
}

*iso8601 : 국제표준화기구(ISO)에서 정한 날짜와 시간에 대해 데이터 교환을 다루는 국제 표준 

 

 

우리가 자주 사용하는 dateFormat을 적용하고 싶다면 dateEncodingStrategy를 formatted()로 설정하면 된다.

let format = DateFormatter()
format.locale = Locale(identifier: "ko_KR")
format.dateFormat = "MM월 dd일 hh:mm:ss EEEE"

encoder.dateEncodingStrategy = .formatted(format)

이렇게 추가적으로 가공을 하면

원하는 형태로 날짜/시간을 인코딩 할 수 있다.

 

Encoding Strategy : encode(to encoder: Encoder)

인코딩된 데이터는 내부 컨테이너로 저장이 되어 있기 때문에 컨테이너로부터 인코딩된 데이터를 가져오기 위해 컨테이너에 먼저 접근을 해야 한다. 그리고 개별 속성을 인코딩하기 위해 컨테이너에서 제공하는 메서드를 사용해 원하는 값으로 리턴받을 수 있다.

 

옵셔널인 경우에은 encodeIfPresent를 아닌 경우에는 encode를 사용한다.

 

 

'Swift' 카테고리의 다른 글

[Swift] class func VS static func  (0) 2022.10.12
URLSession (개념)  (0) 2022.09.01
Type Casting Up? Down?  (2) 2022.08.19
Framework (Dynamic VS Static)  (1) 2022.08.19
Access Control (feat. Framework)  (1) 2022.08.16