iOS/πŸ€– App

[iOS] - Cocoa Touch μ—μ„œ JSON 닀루기

woozzang 2021. 5. 7. 23:27

1μ°¨ μˆ˜μ •: 2021.05.25

JSON = JavaScript Object Notation

 

πŸ€” 사전 지식 - JSON μ΄λž€?

λ‹¨μˆœν•˜κ²Œ 데이터λ₯Ό ν‘œν˜„ν•˜λŠ” 방법 쀑 ν•˜λ‚˜μ΄λ‹€. (톡신 방법도 μ•„λ‹ˆκ³ , ν”„λ‘œκ·Έλž˜λ° 문법도 μ•„λ‹ˆλ‹€. 포맷이닀 !!!!)

 

μ„œλ²„μ™€ ν΄λΌμ΄μ–ΈνŠΈ κ°„μ˜ 데이터 κ΅ν™˜μ—μ„œ 일반적으둜 많이 μ‚¬μš©λœλ‹€.

ν΄λΌμ΄μ–ΈνŠΈκ°€ API Request λ₯Ό 보내면 μ„œλ²„κ°€ μ‘λ‹΅μœΌλ‘œ JSON 데이터λ₯Ό 보내쀀닀.

 

JSON의 포맷은 μžλ°”μŠ€ν¬λ¦½νŠΈ 객체 ν‘œκΈ°λ²•μ„ λ”°λ₯Έλ‹€

  1. key - value μŒμ„ 이루어 ν‘œν˜„ν•˜λ©°, keyλŠ” λ¬Έμžμ—΄μ΄λ‹€. λ¬Έμžμ—΄μ€ " " μŒλ”°μ˜΄ν‘œλ₯Ό μ‚¬μš©ν•˜μ—¬ ν‘œκΈ°ν•œλ‹€.
  2. { } 와 내뢀에 key-value 둜 κ΅¬μ„±λœλ‹€. 즉 Swift λ”•μ…”λ„ˆλ¦¬μ™€ μœ μ‚¬ν•œ ꡬ쑰이닀.
  3. JSONν˜•μ‹μ—μ„œλŠ” null, number, string, array, object, boolean을 μ‚¬μš©ν•  수 μžˆλ‹€.

λ‹€μ‹œ ν•œλ²ˆ λ§ν•˜μ§€λ§Œ JSON은 데이터 ν‘œν˜„ 방식일 뿐이며, ν…μŠ€νŠΈν˜•μ‹μ˜ 데이터 이닀.

 

κ·Έλ ‡κΈ° λ•Œλ¬Έμ— 가독성이 μ’‹μœΌλ©°, λ‹¨μˆœν•œ ν…μŠ€νŠΈμ΄κΈ° λ•Œλ¬Έμ— νŠΉμ • 언어에 μ’…μ†λ˜μ§€ μ•ŠλŠ”λ‹€.

 

λŒ€λΆ€λΆ„μ˜ ν”„λ‘œκ·Έλž˜λ° μ–Έμ–΄μ—μ„œ JSON 포맷 데이터λ₯Ό 핸듀링 ν•  수 μžˆλŠ” 라이브러리λ₯Ό μ œκ³΅ν•œλ‹€


 

πŸ€”μ‚¬μ „ 지식 - JSON 이전에 μ‚¬μš©ν•˜λ˜ XML은?

νŠΉμ§• 데이터 κ°’ μ–‘μͺ½μœΌλ‘œ νƒœκ·Έκ°€ μžˆλ‹€. νƒœκ·Έλ‘œ ν‘œν˜„λ˜λŠ” 데이터이닀.

 

이것은 HTML을 기반으둜 κ³ μ•ˆλ˜μ—ˆκΈ° λ•Œλ¬ΈμΈλ°, νƒœκ·Έλ₯Ό 쀄인닀 해도 μ΅œμ†Œν•œ ν‘œν˜„ν•˜λ €λ©΄ μ–‘μͺ½μ— λͺ‡ κΈ€μžμ”©μ΄ μžˆμ–΄μ•Ό ν•˜κΈ° λ•Œλ¬Έμ— λ‚΄κ°€ 봣을 λ•Œ μž‘μ„±λ„ μ–΄λ ΅κ³  가독성도 μ’‹μ§€ μ•Šλ‹€.

 

 

참고자료 : https://velog.io/@surim014/JSON


κ°œμš”

λ„€νŠΈμ›Œν¬ 연결을 ν†΅ν•œ 데이터 μ „μ†‘μ΄λ‚˜, λ””μŠ€ν¬μ— 데이터λ₯Ό μ €μž₯ν•˜κ±°λ‚˜, API μ„œλ²„λ‚˜ μ„œλΉ„μŠ€λ‘œ 데이터λ₯Ό μ œμΆœν•˜λŠ” 것과 κ΄€λ ¨λœ ν”„λ‘œκ·Έλž˜λ° μž‘μ—…λ“€μ΄ λ§ŽμŠ΅λ‹ˆλ‹€.

 

이런 μž‘μ—…λ“€μ€ μ€‘κ°„μž 역할을 ν•  수 μžˆλŠ” ν˜•μ‹μœΌλ‘œμ˜ λ°μ΄ν„°μ˜ 인코딩 κ³Ό 디코딩을 ν•„μš”λ‘œ ν•©λ‹ˆλ‹€.

인코딩과 λ””μ½”λ”©?

 

μ™œλƒλ©΄ μŠ€μœ„ν”„νŠΈλ‘œ ν‘œν˜„λœ 데이터λ₯Ό λ‹€λ₯Έ ν™˜κ²½μ—μ„œλŠ” ν•΄μ„ν•˜κΈ° μ–΄λ ΅κΈ° λ•Œλ¬Έμž…λ‹ˆλ‹€.

 

Swift standard library λŠ” 데이터 인코딩 κ³Ό λ””μ½”λ”©μ˜ ν‘œμ€€ν™”λœ 방법을 이미 μ •μ˜ν•΄λ†“μ•˜μŠ΅λ‹ˆλ‹€.

 

μš°λ¦¬λŠ” ν•„μš”ν•œ μ»€μŠ€ν…€ νƒ€μž…μ— 이 방법을 μ μš©ν•  것인데,  μ΄κ²ƒμ€ Encodable κ³Ό Decodable ν”„λ‘œν† μ½œμ˜ κ΅¬ν˜„μ„ ν†΅ν•΄μ„œ κ°€λŠ₯ν•©λ‹ˆλ‹€.

 

일단 μœ„ ν”„λ‘œν† μ½œμ„ κ΅¬ν˜„ ν•˜λ©΄ Encoder 와 Decoder κ°€ 데이터λ₯Ό κ°€μ§€κ³  JSON μ΄λ‚˜ property list 와 같은 external representation* 으둜 인코딩 & 디코딩을 ν•΄μ€λ‹ˆλ‹€.

πŸ’‘μŠ€μœ„ν”„νŠΈ μ™ΈλΆ€μ—μ„œ μ‚¬μš©λ˜λŠ” 데이터 ν˜•μ‹μ„ external representation 이라고 ν‘œν˜„ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

 

Encodable κ³Ό Decodable 을 λ™μ‹œμ— μ±„νƒν•œλ‹€λ©΄ 인코딩과 λ””μ½”λ”© μž‘μ—… λͺ¨λ‘λ₯Ό ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

 

그리고 그것은 protocol composition(&) 을 ν†΅ν•˜μ—¬ Codable 둜 μ •μ˜λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€.

typealias Codable = Decodable & Encodable // μ‹€μ œλ‘œ μ΄λ ‡κ²Œ κ΅¬ν˜„λ˜μ–΄ 있음 πŸ˜‰

 

 

본둠으둜 λŒμ•„μ™€μ„œ...

JSON Encoding κ³Ό Decoding 을 ν•˜λŠ” μ£Όμ²΄λŠ” JSONEncoder 와 JSONDecoder μ˜€λΈŒμ νŠΈμž…λ‹ˆλ‹€.

 

Encoding κ³Ό Decoding 이 κ°€λŠ₯ν•˜λ €λ©΄ λŒ€μƒ struct λŠ”Encodable,Decodable ν”„λ‘œν† μ½œμ„ μ±„νƒν•˜κ³  μžˆμ–΄μ•Ό ν•©λ‹ˆλ‹€.

 

λ§Œμ•½ Encodable κ³Ό Decodable 을 λͺ¨λ‘ μ±„νƒν•œλ‹€λ©΄, Codable ν”„λ‘œν† μ½œμ„ μ±„νƒν•œλ‹€κ³  λͺ…μ‹œν•˜λ©΄ λ©λ‹ˆλ‹€.

 

μ–΄λ–€ μ»€μŠ€ν…€ νƒ€μž…μ„ codable ν•˜κ²Œ λ§Œλ“œλŠ” κ°€μž₯ μ‰¬μš΄ 방법은, κ·Έ νƒ€μž…μ΄ κ°€μ§„ 속성듀을 λͺ¨λ‘ Codable νƒ€μž…μœΌλ‘œ μ •μ˜ν•˜λŠ” κ²ƒμž…λ‹ˆλ‹€.

 

Codable ν•œ νƒ€μž…μ€ standart library νƒ€μž…μΈ  String, Int, Double ν˜Ήμ€ Foundation νƒ€μž…μΈ Date, Data, URL 등이 μžˆμŠ΅λ‹ˆλ‹€.

 

이런 νƒ€μž…λ“€μ˜ μ†μ„±μœΌλ‘œλ§Œ κ΅¬μ„±λœ νƒ€μž…μ€ Codable ν”„λ‘œν† μ½œμ„ λ”°λ₯Έλ‹€κ³  λͺ…μ‹œλ§Œν•˜λ©΄ λ©λ‹ˆλ‹€.

"automatically conforms to Codable just declaring that conformance."

struct Landmark: Codable {
  var name: String
  var foundingYear: Int
  
  // 속성듀이 이미 Codable ν•˜λ―€λ‘œ, ν”„λ‘œν† μ½œ κ΅¬ν˜„μ΄ ν•„μš” μ—†μŠ΅λ‹ˆλ‹€.
  
  var location: Coordinate
  
  // λ§Œμ•½ μ»€μŠ€ν…€ νƒ€μž…(Coordinate)이 μΆ”κ°€λœλ‹€κ³  ν•˜λ”λΌλ„, κ·Έ νƒ€μž…μ„ Codableν•˜κ²Œ μ •μ˜ν•œλ‹€λ©΄ 문제 μ—†μŠ΅λ‹ˆλ‹€.
  
  var metadata: [String: String]
  
  // Array, Dictionay, Optional 같은 Built-in type은 κ·Έ λ‚΄λΆ€ νƒ€μž…μ΄ Codable 일 경우 Codable이 λ©λ‹ˆλ‹€.
}

 

 


let encoder = JSONEncoder()
encoder.outFormatting = .prettyPrinted
encoder.keyEncodingStrategy = .converToSnakeCase

do {
// 1
let jsonData = try encoer.encode(someStruct)

// 2
  if let str = String(data: jsonData, encoding: .utf8) {
    print( str ) 
  }
} catch {
  print(error)
}

1 : encode() λŠ” Throwing Method μ΄λ―€λ‘œ try 둜 μ²˜λ¦¬ν•΄μ£Όμ—ˆμŠ΅λ‹ˆλ‹€.

그리고 try λŠ” do catch 둜 μ²˜λ¦¬ν•©λ‹ˆλ‹€.

λ°˜ν™˜ 값은 λ°”μ΄λ„ˆλ¦¬ 데이터 ν˜•νƒœμ˜ JSON μž…λ‹ˆλ‹€.

 

2 :

λ°”μ΄λ„ˆλ¦¬ λ°μ΄ν„°λŠ” 인간이 읽을 수 μ—†κΈ° λ•Œλ¬Έμ— 인코딩이 ν•„μš”ν•©λ‹ˆλ‹€.

눈으둜 ν™•μΈν•˜λ €λ©΄ String(data:encoding:) μƒμ„±μžλ‘œ λ¬Έμžμ—΄ μΈμŠ€ν„΄μŠ€λ‘œ 생성해 좜λ ₯ν•΄λ³Ό 수 μžˆμŠ΅λ‹ˆλ‹€.

λ˜ν•œ outFormatting 속성을 .prettyPrinted 둜 λ³€κ²½ν•΄ key-value κ°„ κ°œν–‰μ„ μΆ”κ°€ν•˜μ—¬ 가독성을 높일 수 μžˆλ‹€.


 

JSON Encoding

Encoding 은 Struct μΈμŠ€ν„΄μŠ€μ—μ„œ JSON λ°μ΄ν„°λ‘œ λ³€ν™˜ν•˜λŠ” κ³Όμ •μž…λ‹ˆλ‹€.

 


 

속성 이름과 key 이름 λ§€μΉ­

JSON μ—μ„œ ν‚€ 넀이밍 μ»¨λ²€μ…˜μ€ snake_case 이닀.

 

ν•˜μ§€λ§Œ Swift μ—μ„œλŠ” lowerCamelCase λ₯Ό μ‚¬μš©ν•˜κΈ° λ•Œλ¬Έμ— μˆ˜μ •μ—†μ΄ Encoding 을 μ§„ν–‰ν•œλ‹€λ©΄ key name 이 lowerCamelCase 둜 λ“€μ–΄κ°€κΈ° λ•Œλ¬Έμ— ν˜Έν™˜μ„±μ΄ λ–¨μ–΄μ§ˆ 것이닀.

 

μ΄λ•ŒλŠ” keyEncodingStrategy 속성을 λ³€κ²½ν•˜μ—¬ λŒ€μ‘ν•  수 μžˆλ‹€.

encoder.keyEncodignStrategy = .convertToSnakeCase

 

λ‚ μ§œ ν˜•μ‹ λ§€μΉ­

Swift λŠ” Date νƒ€μž…μ˜ 속성을 JSON 으둜 인코딩 ν• λ•Œ, Double κ°’μœΌλ‘œ μΈμ½”λ”©ν•œλ‹€.

 

이 값은 2001λ…„ 1μ›” 1일을 μ‹œμž‘μ μœΌλ‘œ λ–¨μ–΄μ§„ 만큼의 μ‹œκ°„μ„ ν‘œν˜„ν•˜λŠ”λ°, 초 λ‹¨μœ„μ΄λ‹€.

 

가독성이 맀우 λ–¨μ–΄μ§€λ―€λ‘œ 많이 μ‚¬μš©ν•˜λŠ” iso8601 ν˜•μ‹μ„ λ”°λ₯Έ λ¬Έμžμ—΄λ‘œ μΈμ½”λ”©ν•˜κ²Œ 바꿔쀄 수 μžˆλ‹€.

 

인코더 였브젝트의 dateEncodingStrategy 속성을 λ³€κ²½ν•˜λ©΄ λœλ‹€.

encoder.dateEncodingStrategy = .iso8601

그런데 이 ν˜•μ‹μ„ κ·ΈλŒ€λ‘œ λ”°λ₯΄μ§€ μ•ŠλŠ” API 듀이 μžˆλ‹€. (예: Kakao REST API)

 

이 κ²½μš°μ— ν•΄λ‹Ή Key ν˜Ήμ€ 속성은 Encoding ν˜Ήμ€ Decoding 에 μ‹€νŒ¨ν•˜κ²Œ λœλ‹€.

 

μ΄λ•ŒλŠ” DateFormatter μΈμŠ€ν„΄μŠ€λ₯Ό 직접 λ§Œλ“€μ–΄μ„œ encoder 에 μ „λ‹¬ν•΄μ•Όν•œλ‹€.

// 1. Custom DateFormatter λ₯Ό μƒμ„±ν•œλ‹€.
var dateFormatter: DateFormatter = {
    var formatter = DateFormatter()
    formatter.dateFormat = "MMMyyyy" // dateFormat ν˜•μ‹λ¬Έμžμ—΄
}()

// 2. encoder μ˜€λΈŒμ νŠΈμ— .formmated case의 μ—°κ΄€κ°’μœΌλ‘œ μ „λ‹¬ν•œλ‹€.
encoder.dateEncodingStrategy = .formmatted(dateFormatter)

🍯 그런데 μ›ν•˜λŠ” DateFormat ν˜•μ‹λ¬Έμžμ—΄μ„ 직접 μž‘μ„±ν•˜λŠ” 것이 μ–΄λ ΅κ²Œ 느껴질수 μžˆλ‹€.

 

이 λ•ŒλŠ” https://nsdateformatter.com μ—μ„œ μ›ν•˜λŠ” ν˜•μ‹λ¬Έμžμ—΄μ„ κ°„νŽΈν•˜κ²Œ 생성할 수 μžˆλ‹€.


 

JSON Decoding

Decoding은 μ„œλ²„λ‘œλΆ€ν„° 전달받은 JSON 데이터λ₯Ό Struct μΈμŠ€ν„΄μŠ€λ‘œ λ³€ν™˜ν•˜λŠ” 과정이닀.

guard let jsonData = jsonStr.data(using: .utf8) else {
   fatalError()
}

//
let decoder = JSONDecoder()
let result = try? decoder.decode(Person.self, from: jsonData)

Struct μ†μ„±μ˜ νƒ€μž…

일반 νƒ€μž…μ΄λΌλ©΄ decoding μ‹€νŒ¨ μ‹œ λŸ°νƒ€μž„ 였λ₯˜κ°€ λ°œμƒν•œλ‹€

μ˜΅μ…”λ„ νƒ€μž…μ΄λΌλ©΄ decoding μ‹€νŒ¨ μ‹œ nil 둜 μ΄ˆκΈ°ν™”λœλ‹€.


 

JSON key 와 struct 속성이 λ‹€λ₯Έ 경우

λ§Œμ•½ ν‚€ λ„€μž„κ³Ό 속성 μ΄λ¦„μ˜ Naming convention 만 λ‹€λ₯΄λ‹€λ©΄ encoding λ•Œμ™€ 같은 λ°©μ‹μœΌλ‘œ ν•΄κ²°ν•΄μ£Όλ©΄ λœλ‹€.

decoder.keyDecodingStrategy = .convertFromSnakeCase

 

ν•˜μ§€λ§Œ 이름이 μ•„μ˜ˆ λ‹€λ₯Έκ²½μš°λŠ” custom key mappping 을 ν•΄μ£Όμ–΄μ•Ό ν•œλ‹€.

 

CodingKey 에 λŒ€ν•œ λ‚΄μš©μ€ λ”°λ‘œ κΈ€λ‘œ μ •λ¦¬ν•˜μ˜€μŠ΅λ‹ˆλ‹€

 

struct iPhone: Codable {
   var SerialNumber: String
   var Storage: Int
   var generation: Int
   var ramSize: Int

  enum CodingKeys: String, CodingKey {
    case SerialNumber
    case Storage
    case generation = "model_number"
    case ramSize
  }
}

예λ₯Ό λ“€μ–΄ JSON λ°μ΄ν„°μ—μ„œ model_number 킀에 μ €μž₯된 값을 generation 속성에 λ°›μ•„μ˜€κ³  싢을 λ•Œ, custom Key mapping 을 ν•˜μ§€ μ•ŠμœΌλ©΄ decoding error κ°€ λ°œμƒν•  것이닀. Int νƒ€μž…μΈ generation 에 λ§€ν•‘λœ 값이 μ—†κΈ° λ•Œλ¬Έμ΄λ‹€.

 

μœ„μ˜ μ½”λ“œμ™€ 같이 ν˜•μ‹ 내에 enum 을 μ„ μ–Έν•˜κ³ , raw Value 둜 String 둜 μ„ μ–Έν•˜κ³ , CodingKey ν”„λ‘œν† μ½œμ„ μ±„νƒν•œλ‹€μŒ λ§€ν•‘ν•˜κ³  싢은 case 만 rawValue 섀정을 ν•΄μ£Όλ©΄ ν•΄κ²°λœλ‹€. rawValue 에 λ“€μ–΄κ°€λŠ” 값은 λ§€ν•‘ν•˜κ³  싢은 JSON 의 key name 이닀.


 

🍯 Custom Encoding & Custom Decoding

보톡 인코딩과 디코딩에 μ œμ•½μ„ λ„£κ³  μ‹Άμ„λ•Œ μ»€μŠ€ν…€μœΌλ‘œ κ΅¬ν˜„ν•œλ‹€.

 

Encodable & Decodable ν”„λ‘œν† μ½œμ˜ λ©”μ„œλ“œλ₯Ό 직접 κ΅¬ν˜„ν•˜λ©΄ λœλ‹€.

 

" πŸ₯³ 원리λ₯Ό μ œλŒ€λ‘œ μ΄ν•΄ν–ˆμœΌλ‹ˆ, μ»€μŠ€ν…€μ΄ ν•„μš”ν•  λ•Œ λ‚΄μš©μ„ 더 μΆ”κ°€ν•˜μž! "


 

πŸ’‘ κΈ°λ³Έ 원리: protocol method λ₯Ό 직접 κ΅¬ν˜„ν•˜κ³ , container 의 unkeyed-data λ₯Ό μ μ ˆν•˜κ²Œ encode/decode ν•˜μž!!!!

Encodable 의 경우 encode(to encoder:) Decodable 의 경우 init(from decoder:) λ©”μ„œλ“œλ₯Ό 직접 κ΅¬ν˜„ν•˜λ©΄ λœλ‹€.

  func encode(to encoder: Encoder) throws {
    <#code#>
  }

  init(from decoder: Decoder) throws {
    <#code#>
  }

μš°μ„  μ»€μŠ€ν…€ 인코딩을 ν•˜λ €λ©΄ CodingKey ν”„λ‘œν† μ½œμ„ μ±„νƒν•œ CodingKeys μ—΄κ±°ν˜•μ„ 생성해야 ν•œλ‹€.

struct Person: Codable {
   var firstName: String
   var lastName: String
   var age: Int
   var address: String?

  enum CodingKeys: String, CodingKey {
    case firstName
    case lastName
    case age
    case address
  }

 

μ™œ? container(keyedBy:) 의 νŒŒλΌλ―Έν„°λ‘œ CodingKey 의 메타 νƒ€μž…μ„ λ°›λŠ”λ‹€.

 

  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)

    try container.encode(firstName, forKey: .firstName)

    try container.encode(lastName, forKey: .lastName)

    guard (30...60).contains(age) else { // μ œμ•½μ„ μΆ”κ°€ν•˜κ³  쑰건에 λ§žμ§€ μ•ŠλŠ”λ‹€λ©΄ μ—λŸ¬λ₯Ό λ˜μ§„λ‹€.
      throw DecodingError.invalidRange
    }

    try container.encodeIfPresent(address, forKey: .address)
  }

 

μ»¨ν…Œμ΄λ„ˆμ˜ μ •μ˜ : " an container appropriate for holding multiple unkeyed values."

 

그리고 encode(_, forKey:) λ©”μ„œλ“œλ‘œ container λ‚΄λΆ€μ˜ 데이터(unkeyed values)듀을 μΈμ½”λ”©ν•˜μ—¬ 킀와 λ§€ν•‘ν•΄μ€€λ‹€.

 

encode λŠ” throws ν‚€μ›Œλ“œμ—μ„œ 볼수 μžˆλ“―μ΄ Throwing Method 둜, λ‚΄λΆ€ λΈ”λŸ­μ—μ„œ μ—λŸ¬λ₯Ό 던질 수 μžˆλ‹€.

 

μœ„ μ½”λ“œμ—μ„œλŠ” age μ†μ„±μ˜ 값이 30~60 사이가 μ•„λ‹ˆλΌλ©΄ 미리 μ •μ˜λœ μ—λŸ¬λ₯Ό λ˜μ§€κ³  μžˆλ‹€.

 

 

 

끝!


πŸ€–πŸŸ’[우짱의 iOS λΈ”λ‘œκ·Έ]πŸ”΅πŸ’»

iOSλ₯Ό κ³΅λΆ€ν•˜λ©΄μ„œ 배운 λ‚΄μš©μ„ κΈ°λ‘ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€.

 

참고 자료: https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding