Swift에서 JSON 인코딩/디코딩하기

2023년 1월 6일 수정

이 글은 Swift에서 네이티브 콜렉션 값(인스턴스, 오브젝트)과 JSON 사이의 변환 방법을 간단하게 정리한다. 단순하게 이야기하자면 JSONEncoderJSONDecoder 를 정리하는 글이다.

JSONEncoder

복잡한 콜렉션 값 데이터를 JSON 포맷으로 표현하기 위해서 빌트인 객체인 JSONEncoder 를 사용할 수 있다.

아래는 샘플로 사용할 간단한 사전형 값 예제다.

let inputDict: [String : Int] = [
  "james": 10,
  "conrad": 20
]

이 값을 JSON 형식으로 변환하려면 JSONEncoder 를 이용해 아래와 같이 할 수 있다.

do {
  let jsonData = try JSONEncoder().encode(inputDict)
} catch {
  // 인코딩 실패 시 예외처리
}

이렇게 하면 Data 형식의 JSON 데이터를 얻을 수 있다.

JSONDecoder

JSON으로 인코딩 된 형식의 DataJSONDecoder 를 이용해 다시 Swift 값 타입으로 변환할 수 있다.

do {
  let decodedObject = try JSONDecoder().decode([String : Int].self, from: jsonData)
} catch {
  // 디코딩 실패 시 예외처리
}

인코딩 때와는 다르게 디코딩 때는 명확한 타입을 알려주고 있다는 점에 주목하자.

문제

위의 예제는 고정된 타입을 사용하는 단순한 예제였는데 실제 세계는 그렇게 단순하지 않다. JSON으로 데이터를 표현할 때도 여러 타입의 값을 사용하는게 일반적이다. 예를 들어 아래와 같이 말이다.

let inputObject: [[String : Any]] = [
  ["name": "james", "age": 10, "classType": "beginner"],
  ["name": "conrad", "age": 20, "classType": "expert"],
  ["name": "michael", "age": 30, "classType": "pro"],
]

각 아이템의 사전형 값에는 문자열과 숫자 등 복합 타입이 쓰인다. 따라서 이런 형태는 값의 타입을 Any 로 정의할 수밖에 없다.

그런데 문제는 불행히도 JSONEncoder 에서는 Any 타입이 사용되면 퇴짜를 놓는다 라는 점이다. 인코딩이든 디코딩이든 명확한 타입이 요구된다.

다만 아예 불가능한 것은 아니고, 만약 복합 타입을 사용하고 싶다면 아래 항목과 같이 Codable 을 이용해 구조화 시켜야 한다.

Codable

Codable 인터페이스는 Swift에서 값과 이를 표현하는 데이터 사이를 변환하는데 사용되는 인코딩(Encodable)과 디코딩(Decodable)을 위한 규약을 합친 형태다. 말이 좀 어려운데, 그냥 구조체나 클래스를 JSON 형식과 서로 바꾸기 쉽게 만들어 준다.

위의 복합 타입 예제의 입력 콜렉션 값을 구조체로 구조화 시켜보자.

struct Student: Codable {
    let name: String
    let age: Int
    let classType: ClassType
}

enum ClassType: String, Codable {
    case beginner = "beginner"
    case expert = "expert"
    case pro = "pro"
}

위와 같이 Codable 을 만족하는 구조체와 열겨형을 선언했다. 열거형(enum)으로 선언한 예에는 타입과 이름을 명확하게 선언하고 있는데 꼭 문자열 형태가 아니어도 된다면 굳이 타입과 값 자체를 명시하지 않아도 된다는 점을 참고하자.

이렇게 Codable 을 명시하면 이제 필드 이름과 값을 알아서 인코딩 및 디코딩 할 수 있게 만들어준다. 위의 구조체를 이용해 위의 데이터를 표현해서 인코딩 해보자.

let students = [
  Student(name: "James", age: 10, classType: .beginner),
  Student(name: "Conrad", age: 20, classType: .expert),
  Student(name: "Michael", age: 30, classType: .pro)
]

do {
  let jsonData = try JSONEncoder().encode(students)
} catch {
  // 인코딩 실패 시 예외처리
}

이렇게 하면 원하는 JSON 형식의 데이터를 얻을 수 있다. 실제로 위 코드가 실행되면 jsonData 에는 아래와 같은 데이터가 들어가게 된다.

[{"name": "James", "age":10, "classType": "beginner"},
 {"name": "Conrad", "age":20, "classType": "expert"},
 {"name": "Michael", "age":30, "classType": "pro"}]

참고로 위 내용은 읽기 편하게 약간 가공했지만 실제 내용은 공백과 개행문자를 제외하면 동일하다.

물론 반대로 디코딩도 아래와 같이 할 수 있다.

do {
  let decodedObject = try JSONDecoder().decode([Student].self, from: jsonData)
} catch {
  // 디코딩 실패 시 예외처리
}

이렇게 하면 처음 입력으로 사용된 형식의 값과 동일한 콜렉션 값을 얻을 수 있다.

관련된 글들