티스토리 뷰

Programming/Swift

Swift Expert | Codable

글그리 2020. 12. 2. 17:15

Swift4 부터 JSON 데이터를 다룰 수 있는 Codable이라는 protocol이 추가되었다. 정확히 말하면 JSONEncoder, JSONDecoder 라는 JSON parsor가 요구하는 protocold로 JSON으로 작성된 데이터를 읽어서 저장할 수 있는 Decodable, Swift 데이터를 JSON으로 변환할 수 있는 Encodable, 그리고 이 두가지를 모두 가능하게 하는 Codable, 이렇게 세 종류가 있다.

Codable은 Foundation에 정의되어 있기 때문에 사용하기 위해서는 Foundation을 import해야 된다. 숫자나 문자열을 저장하려면 Int, Float, String을 사용할 수 있고, 또 다른 codable을 사용할 수도 있다.

 

 

 

General

일반적으로 변환하고자 하는 JSON의 구조를 모두 알고, 어떤 값의 집합을 가질 지 알고 있다면 간단하게 변환할 수 있다. 예를 들어 이름과 생년월일, 키 등 프로필 정보를 담고있는 JSON 데이터를 읽기 위해서는 해당 attribute와 같은 이름의 변수들을 1대1 대응하도록 선언한 후 Codable protocol을 채용하면 같은 포맷의 JSON 데이터를 읽거나 쓸 수 있게 된다.

 

let jsonData = """
{
    "name": "이지은",
    "nickName": "IU",
    "birth": "1993년 5월 16일",
    "height": 161.8
}
""".data(using: .utf8)!

struct Profile : Codable {
    let name: String
    let nickName: String
    let birth: String
    let height: Float
}

 

 

 

encode

이렇게 만든 구조체를 JSON으로 만들기 위해서 JSONEncoder를 사용한다. decode도 마찬가지 이지만 두 경우 모두 실패할 수 있기 때문에 try의 사용을 강제한다. 이 때 만들어지는 결과값은 Data라는 byte memory로 변환되기 때문에 따로 출력해보려면 String으로 변환해서 출력할 수 있다.

 

let custom = Data(name: "이동근", nickName: "eastroot", birth: "1994년 4월 26일", height: 178.5)
if let jsonResult = try? JSONEncoder().encode(custom) {
	print(jsonRsult)    // 86byte
	print(String(data: jsonResult, encoding: .utf8)!)    // json form
}

 

 

 

decode

JSON 데이터를 decodable 자료형에 저장하는 작업이다. 마찬가지로 try의 사용을 강제하고, 원본 데이터로 Data 자료형을 요구한다.

 

if let iu = try? JSONDecoder().decode(Profile.self, from: jsonData) {
	print(iu.birth)    // 1993년 5월 16일
}

 

이렇게 쉽게 모든 JSON이 변환될 수 있다면 좋겠지만 세상은 그렇게 호락호락하지 않다.

 

 

 

정보가 JSON에만 있는 경우

JSONDecoder에서 decode를 할 때, 전달받은 Codable 구조에서 검색해서 대입하기 때문에 Codable에 없는 attribute의 경우 그냥 무시되고 성공한다.

 

let jsonData = """
{
    "name": "이지은",
    "nickName": "IU",
    "birth": "1993년 5월 16일"
}
""".data(using: .utf8)!

struct Profile : Codable {
    let name: String
    let nickName: String
}

if let iu = try? JSONDecoder().decode(Profile.self, from: jsonData) {
	print(iu.name)    // 이지은
}

 

 

 

정보가 Codable에만 있는 경우

어떤 데이터가 들어올 거라고 예상하고 codable 구조체를 만들었는데 전달받은 JSON이 그 데이터를 안가지고 있을 수 있다. 생각해보면 JSONDecoder는 Codable 객체를 생성하는 일을 하는데, 구조체의 맴버변수를 모두 초기화 하지 못한다는 뜻이기 때문에 실패할 수 밖에 없다. 두가지 방법이 있을 수 있는데, 첫번째는 optional로 선언해서 초기화 하지 않아도 되게 만드는 것이고, 두번째는 강제로 초기값을 대입하는 방법이 있다.

let jsonData = """
{
    "name": "이지은",
    "nickName": "IU",
    "birth": "1993년 5월 16일"
}
""".data(using: .utf8)!

struct Profile : Codable {
	let name: String
	let nickName: String
	var nation: String = "대한민국"
	// or 
	// let nation: String?
	let birth: String
}

 

초기값을 줄 때는 let이 아니라 var로 선언한다. let은 상수라서 다음번에 nation 정보를 가진 JSON이 들어오더라도 "대한민국"으로 고정되기 때문이다.

 

 

 

Attribute의 이름이 다를 경우

위에서 사용한 JSON 데이터는 예시를 위해 만든 데이터고, 데이터의 모든 내용을 알고 있는 상태에서 codable 구조체를 만들었기 때문에 attribute이름과 동일한 변수 이름을 사용했다. 예를 들어 이름의 경우 JSON에서도 name이며, codable에서도 name이다. 대소문자 구분도 철저하기 때문에 "nickName"과 "nickname"은 다르게 처리되고, decode에 실패한다. JSONDecoder는 CodingKeys 라는 CodingKey enum을 가지고 JSON 데이터와 비교하면서 값을 대입하는데, 이 enum은 구조체에 들어있는 변수들의 이름으로 초기화 된다. 위에서는 이 enum을 따로 작성하지 않아도 잘 동작하는데, 컴파일 시점에 자동으로 만들어주는 것 같다.

 

// default CodingKeys enum for Profile
enum CodingKeys: String, CodingKey {
	case name
	case nickName
	case birth
	case height
}

 

그리고 각 case의 기본값은 같은 이름의 String으로 초기화 되기 때문에, JSON 데이터의 내용과 대조할 수 있었던 것이다.

 

CodingKeys에서 알맞은 case를 찾아서 대입한다.

 

실제로 nickName은 "nickName"으로 초기화 되고, 당연하게도 nickName에만 매치하기 때문에 이 기본값을 수정해주는 것으로 대응할 수 있다. 초기값을 수정하기 위해서 CodingKeys를 우리 구조체에 다시 정의한다. 이름은 꼭 CodingKeys라고 해야 동작한다.

 

struct Profile : Codable {
	enum CodingKeys : String, CodingKey {
		case name
		case nickName = "nickname"
		case birth
		case height
	}
	// ...
}

 

재정의 하고 있기 때문에 현재 필요한 nickName 외에 모든 case를 모두 정의해야 한다. 단, 이렇게 새로운 이름을 대입하면 이제 nickName으로 데이터가 들어오면 대응하지 못한다. Decoder에서 JSON attribute 이름을 바로 쓰지 않고, 특정 포맷으로 바꿔준다면 좋을 것 같다.

 

 

 

keyDecodingStrategy

그래서 자동으로 camelCase로 변환해주는 기능을 제공한다. custom에 closure로 원하는 decoding strategy를 만들 수도 있지만 convertFromSnakeCase 처럼 사전 정의된 규칙을 적용할 수도 있다. 이 규칙은 snake_case로 되어있는 이름을 camelCase로 바꿔준다.

 

let jsonData = """
{
    "name": "이지은",
    "nick_name": "IU",
}
""".data(using: .utf8)!

struct Profile : Codable {
	let name: String
	let nickName: String
}

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeKase
if let profile = try? decoder.decode(Profile.self, from: jsonData) {
	print(nickName)    // IU
}

 

이 방법은 snake_case, camelCase 모두 다 대응할 수 있다는 장점이 있다.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함