JSON Parser using JSONSerialization or JSONDecoder in Swift

ellyheetov·2021년 3월 27일
3

JSONParsing

목록 보기
1/2
post-thumbnail

목표

(1) API를 호출해서 (2) JSON 객체를 받아 온 후, (3) Swift data type으로 저장하기

swift에서 JSON객체를 받아와서 Parsing할 수 있는 방법은 2가지가 있다.

  • JSONSerialization
  • JSONDecoder

두 가지 방법을 각각 알아보자.

Example JSON

먼저 api를 요청하여 받아오는 JSON 객체는 다음과 같은 형태이다.

{
  "items": [
    {
      "id": 1,
      "name": "전시",
      "count": 10
    },
    {
      "id": 2,
      "name": "뮤지컬",
      "count": 10
    },
    {
      "id": 3,
      "name": "콘서트",
      "count": 16
    },
    {
      "id": 4,
      "name": "클래식",
      "count": 10
    },
    {
      "id": 5,
      "name": "연극",
      "count": 13
    }
  ]
}

Swift Object

items 배열을 가지고 있으며 그 안에는 id, name, count 값을 가지고 있다.
이렇게 3개의 값을 가지고 있는 구조체를 하나 선언해주자.

struct Item {
    let id : Int
    let name : String
    let count : Int
}

URL을 이용해서 Json 정보를 받아오기

URL을 이용해서 JSON 데이터를 받아오면 swift에서는 Data 자료형을 이용하여 저장한다.

let url = URL(string: "http://49.236.147.192:9090/api/categories")!
let data : Data = try Data.init(contentsOf: url)

1. JSONSerialization

JSONSerialization은 Swift에 Foundation 프레임워크에서 제공하는 객체이다.

let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: [Any]] 

JSONSerialization을 통해서 앞에서 받아온 data 객체에 대해서 parsing을 시작한다. Data[String:[Any]] 타입으로 캐스팅한다.여기서 사용하는 JSON이 "items": [] 구조를 가지고 있기 때문이다.

잘 가지고 왔는 지 확인 하기 위해서 print문을 출력해 보았다.

if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: [Any]] {
    if let items = json["items"] as? [Any] {
        print(items)
    }
}

Output

[{
    count = 10;
    id = 1;
    name = "\Uc804\Uc2dc";
}, {
    count = 10;
    id = 2;
    name = "\Ubba4\Uc9c0\Uceec";
}, {
    count = 16;
    id = 3;
    name = "\Ucf58\Uc11c\Ud2b8";
}, {
    count = 10;
    id = 4;
    name = "\Ud074\Ub798\Uc2dd";
}, {
    count = 13;
    id = 5;
    name = "\Uc5f0\Uadf9";
}]

다음과 같이 "items"가 가지고 있는 배열을 잘 받아 온 것을 확인할 수 있었다. utf8 설정이 되어있지 않아서 아직은 한글이 보이지 않는 것 같다.

이제 가지고 온 [String: [Any]][Item]배열로 바꾸어 저장해 보자.

let json = try JSONSerialization.jsonObject(with: data, options: []) as! [String: [Any]] 
let items = json["items"] as! [Any]  // "item" 안에 있는 값들만 가져옴

items.forEach{ item in 
    guard let object = item as? [String : Any] else { return }
    // 가지고 있는 string key값을 이용하여 값을 가져온다.
    let id = object["id"] as! Int
    let name = object["name"] as! String
    let count = object["count"] as! Int 
 
    products.append(Item(id: id, name: name, count: count))
}

여기까지가 JSONSerialization을 이용한 JSON parsing 과정이다.

products.forEach{
    print("id : \($0.id), name : \($0.name), count : \($0.count)")
}

output

id : 1, name : 전시, count : 10
id : 2, name : 뮤지컬, count : 10
id : 3, name : 콘서트, count : 16
id : 4, name : 클래식, count : 10
id : 5, name : 연극, count : 13

출력을 통해 products배열에 값이 제대로 할당 된 것을 확인할 수 있다.

전체 코드

struct Item {
    let id : Int
    let name : String
    let count : Int
}

var products = [Item]()

let url = URL(string: "http://49.236.147.192:9090/api/categories")!
let data : Data = try Data.init(contentsOf: url)

let json = try JSONSerialization.jsonObject(with: data, options: []) as! [String: [Any]] 
let items = json["items"] as! [Any] 

items.forEach{ item in
    guard let object = item as? [String : Any] else { return }
    let id = object["id"] as! Int
    let name = object["name"] as! String
    let count = object["count"] as! Int
    products.append(Item(id: id, name: name, count: count))
}

products.forEach{
    print("id : \($0.id), name : \($0.name), count : \($0.count)")
}

2. JSONDecoder

JSONDecoder은 상대적으로 JSONSerialization 보다 간결 한 것 같다.

JSONDecoder을 사용하기 위해서는 swift 객체가 Decodable을 채택하고 있어야 한다.
Item 객체가 Decodable을 채택하게 해주자.

struct Item : Decodable{
    let id : Int
    let name : String
    let count : Int
}

아무 설정을 하지 않으면 JSON의 key값이 "id"이면 id, "name"이면 name, "count"이면 count 변수로 자동 할당 된다. 만약 JSON에 key값과 Swift 객체의 변수명을 다르게 하고 싶다면 CodingKey를 사용하면 된다. (여기서는 CodingKey 적용에 대한 설명은 생략한다.)

Decodable을 채택하였으니, JSONDecoder를 이용하여 parsing해보자.

products = try JSONDecoder().decode([String : [Item]].self, from: json)

이 한줄이 전부이다. 앞에서 key값을 이용해서 값을 가져오고 형변환을 해주는 모든 과정은 Decodable에서 일어나기 때문이다. 잘 들어 간것이 맞는지 확인해보자.

products.map{
    $0.value.forEach{ item in
        print("id : \(item.id), name : \(item.name), count : \(item.count)")
    }
}

output

id : 1, name : 전시, count : 10
id : 2, name : 뮤지컬, count : 10
id : 3, name : 콘서트, count : 16
id : 4, name : 클래식, count : 10
id : 5, name : 연극, count : 13

전체 코드

  • without try-catch
struct Item : Decodable{
    let id : Int
    let name : String
    let count : Int
}

var products = [String :[Item]]()
let url = URL(string: "http://49.236.147.192:9090/api/categories")!
let json : Data = try Data.init(contentsOf: url)

products = try JSONDecoder().decode([String : [Item]].self, from: json)
products.map{
    $0.value.forEach{ item in
        print("id : \(item.id), name : \(item.name), count : \(item.count)")
    }
}
  • with try-catch

import Foundation

struct Item : Decodable{
    let id : Int
    let name : String
    let count : Int
}

var products = [String :[Item]]()
var json : Data!
do {
    let url = URL(string: "http://49.236.147.192:9090/api/categories")!
    json = try Data.init(contentsOf: url)
} catch let error as NSError {
    print("Failed to load: \(error.localizedDescription)")
}

do {
    products = try JSONDecoder().decode([String : [Item]].self, from: json)
} catch let error as NSError {
    print("Faild to parser : \(error.localizedDescription)")
}
products.map{
    $0.value.forEach{ item in
        print("id : \(item.id), name : \(item.name), count : \(item.count)")
    }
}

3. SwiftyJSON

JSON을 파싱하는데 사용된 방법은 JSONSerialization이다. 그런데 앞에서 보았듯이, 굉장히 코드가 길어지는 것을 볼 수 있다. 이를 조금더 나은 방향을 개선하기 위해서 나온 것이 SwiftyJSON 방식이다. JSONDecoder는 Swift4에서 부터 나온 방식이다.

Performance

모두 똑같이 JSON을 파싱하는 기능을 하는데, 왜 3가지나 있는걸까? 성능상에 차이가 있을까?

JSONDecoder와 JSONSerialization, SwiftJSON의 성능을 비교한 그래프이다.

parsing해야하는 데이터의 개수가 1000개 미만인 경우 성능의 차이가 없지만, 데이터의 개수가 늘어날 수록 확연하게 차이가 나는 것을 확인 할 수 있다. 사용하기에는 JSONSerialization이 가장 복잡하더라도, 성능상으로는 가장 뛰어나다.

이런 차이가 나는 이유는 SwiftyJSON은 내부적으로 JSONSerialization을 사용하는데, 배열 내에 있는 모든 객체에 대하여 반복하고 JSON객체에 매핑하기 때문이라고 한다. 중첩 배열을 처리할 때 시간복잡도와 공간복잡도가 기하 급수적으로 증가한다. (아직 여기까지는 잘 모르겠다)

참고
https://www.avanderlee.com/swift/json-parsing-decoding/
https://medium.com/@shaileshaher07/json-parsing-with-decodable-1b9d349ea8d5
https://betterprogramming.pub/time-to-abandon-swiftyjson-switch-jsondecoder-codable-407f9988daec

profile
 iOS Developer 좋아하는 것만 해도 부족한 시간

0개의 댓글