iOS 네트워크 통신과 API 살펴보기 3(Json Parsing)

2021. 10. 18. 22:33iOS/iOS

728x90
반응형
이 포스팅은 꼼꼼한 재은씨의 Swift 기본편 - chapter09 네트워크 통신과 API편을 참고하였습니다.


안녕하세요 bannavi입니다^ㅅ^

이번 포스팅은 제가 네트워킹을 이용한 영화예매앱을 실제로 구현해보면서
중요하다고 생각하는 과정을 적었습니다.


Chapter1. 네트워크 객체를 통한 데이터 요청 기능 구현

1. viewDidLoad()메서드 내부에 RESTAPI 호출하는 코드를 작성

swift에서 GET방식으로 REST API를 호출하여 데이터를 읽어오는 방법은 다음과 같습니다.

var list = Data(contentsOf: <#T##URL#>)


URI와 URL의 차이

URL 과 URI의 차이

URL /URI

medium.com

간단하게는 URI가 URL의 상위 개념이다.

let log = NSString(data: apidata, encoding: String.Encoding.utf8.rawValue) ??

NSString(data: <문자열로 변환할 Data 타입 객체>, encoding: <인코딩 형식>)

String대신 NSString사용하는 이유: NSString 객체는 입력받은 Data 객체를 문자열로 변환해주는 메서드를 지원하지만, 스위프트의 기본 자료형인 String은 이와 같은 직접적인 변환 메서드가 없어서 상대적으로 복잡한 과정을 거쳐 Data객체를 변환해야 하기 때문

listViewController.swift 코드참조

import UIKit class ListViewController: UITableViewController { // 튜플 아이템으로 구성된 데이터 세트 var dataset = [ ("다크나이트", "영웅물에 철학에..예술이 되다", "2008-09-04", 8.95, "darknight.jpg"), ("호우시절", "때를 알고 내리는 좋은 비", "2009-10-08", 7.31, "rain.jpg"), ("말할 수 없는 비밀", "여기서 너까지 다섯 걸음", "2015-05-07", 9.19, "secret.jpg") ] // 테이블 뷰를 구성할 리스트 데이터 lazy var list : [MovieVO] = { var datalist = [MovieVO]() for (title, desc, opendate, rating, thumbnail) in self.dataset { let mvo = MovieVO() mvo.title = title mvo.description = desc mvo.opendate = opendate mvo.rating = rating mvo.thumbnail = thumbnail datalist.append(mvo) } return datalist }() override func viewDidLoad() { // 호핀 api 호출을 위한 uri생성 let url = "http://swiftapi.rubypaper.co.kr:2029/hoppin/movies?version=1&page=1&count=10&genreId=&order=releasedateasc" let apiURI: URL! = URL(string: url) // Rest API를 호출 let apidata = try! Data(contentsOf: apiURI) // 데이터 전송 결과를 로그로 출력(반드시 필요한 코드는 아님) let log = NSString(data: apidata, encoding: String.Encoding.utf8.rawValue) ?? "" NSLog("API Result=\(log)") } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return self.list.count } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { // 주어진 행에 맞는 데이터 소스를 읽어온다. let row = self.list[indexPath.row] // ========= 여기부터 내용 변경됨 ========= let cell = tableView.dequeueReusableCell(withIdentifier: "ListCell") as! MovieCell // 데이터 소스에 저장된 값을 각 아울렛 변수에 할당 cell.title?.text = row.title cell.desc?.text = row.description cell.opendate?.text = row.opendate cell.rating?.text = "\(row.rating!)" // 추가된 부분: 이미지 뷰 처리 cell.thumbnail.image = UIImage(named: row.thumbnail!) // ========= 여기까지 내용 변경됨 ========= return cell } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { NSLog("선택된 행은 \(indexPath.row) 번째 행입니다") } }


ㅋㅋㅋ 어제 배워본 lldb야무지게 써보기. 귀염
이제 굳이 코드안에 print를 추가해보지 않아도 될거같다

만약에 파싱에 실패하고 출력이 안된다면 아래의 설정 해주기!

서버 도메인이 https://로 시작한다면 그냥 코드를 실행시켜도 접속이 가능
http://로 시작한다면 지금 설명하는 ATS 보안 설정을 추가해주어야 함

Info.plist파일 우클릭 -> Open As -> Source Code클릭
그러면 info.plist파일이 xml파일의 형태로 열리는데 내부에 아래의 코드를 추가해준다

<key>NSAppTransportSecurity</key> <dict> <key>NSAllowsArbitraryLoads</key> <true/> </dict>

Chapter2. 전달받은 데이터를 파싱하여 화면에 출력

apidata상수는 Data타입이어서 우리가 바로 꺼내 쓰기는 어렵습니다.
로그를 출력하기 위해 NSString 타입의 문자열로 변환하였듯이,
테이블을 구성하는 데이터로 사용하려면 NSDictionary객체로 변환해야 합니다.

NSDictionary는 키-값으로 된 데이터 구조를 저장하므로 JSONObject 포맷의 데이터와 호환됩니다.
만약 데이터가 리스트 형태로 전달되었다면 JSONArray 포맷과 호환되는 NSArray 객체를 사용해야 합니다.

# 읽어들일때 데이터 형식에 따라 적절히 캐스팅하기 #
리스트 형태일때 -> NSArray
일반 JSON데이터일때 -> NSDictionary
일반 문자열인경우 -> String

데이터 파싱할때 JSONSerialization 객체의 jsonObject()메서드를 사용해봤는데
이 메서드는 두개의 인자값을 받음.
첫번째는 파싱할 데이터, 두번째는 파싱 옵션.(파싱 옵션에 넣을것은 아무것도 없으므로 빈배열 처리해줌)

let apiDictionary = try JSONSerialization.jsonObject(with: apidata, options: []) as! NSDictionary

jsonObject()메서드는 파싱과정에서 오류가 발생하면 예외로 던지도록 설계됨(do~try~catch문)
오류가 발생하면 catch블록쪽으로 오류와 함께 실행 흐름이 전달됌.

jsonObject()메서드의 실행 결과는 입력된 데이터에 따라 NSDictionary 혹은 NSArray형태로 나올 수 있습니다. 양쪽을 모두 지원하기 위해 이 메서드는 옵셔널 Any타입으로 정의된 결과값을 반환하므로 최종적으로는 이 결과값을 원하는 객체로 캐스팅해서 받아야함.
위에 코드를 보면 NSDictionary로 캐스팅했음.


그다음 해야할게 모냐모냐!!! 바로바로~~!!!

데이터 구조에 따라 차례대로 캐스팅하면서 읽어오기

뭔소리야.
봐봐여 이 아래의 json파일이 우리가 파싱할 데이터에요.

{ "hoppin": { "totalCount": "4266", "movies": { "movie": [{ "genreNames": "코미디,드라마", "genreIds": "003,005", "title": "인포먼트", "movieId": "P00000248257", "linkUrl": "http://swiftapi.rubypaper.co.kr:2029/hoppin/detailView?movieId=P00000248257", "participant": "3", "ratingAverage": "6.7", "thumbnailImage": "http://swiftapi.rubypaper.co.kr:2029/thumbnail/REP_00000043009_1_1_0527.jpg" }, { "genreNames": "드라마,스릴러", "genreIds": "005,006", "title": "엣지 오브 다크니스", "movieId": "P00000248305", "linkUrl": "http://swiftapi.rubypaper.co.kr:2029/hoppin/detailView?movieId=P00000248305", "participant": "17", "ratingAverage": "6.9", "thumbnailImage": "http://swiftapi.rubypaper.co.kr:2029/thumbnail/REP_00000036879_1_1_0818.jpg" }, { "genreNames": "로맨스,드라마", "genreIds": "016,005", "title": "베이비 돌", "movieId": "P00000248574", "linkUrl": "http://swiftapi.rubypaper.co.kr:2029/hoppin/detailView?movieId=P00000248574", "participant": "2", "ratingAverage": "6", "thumbnailImage": "http://swiftapi.rubypaper.co.kr:2029/thumbnail/REP_GL0000193614_1_1.jpg" }, { "genreNames": "드라마,로맨스", "genreIds": "005,016", "title": "황야의 역마차", "movieId": "P00000248576", "linkUrl": "http://swiftapi.rubypaper.co.kr:2029/hoppin/detailView?movieId=P00000248576", "participant": "4", "ratingAverage": "6.3", "thumbnailImage": "http://swiftapi.rubypaper.co.kr:2029/thumbnail/REP_GL0000193505_1_1.jpg" }, { "genreNames": "드라마,로맨스", "genreIds": "005,016", "title": "전원 교향곡", "movieId": "P00000248880", "linkUrl": "http://swiftapi.rubypaper.co.kr:2029/hoppin/detailView?movieId=P00000248880", "participant": "1", "ratingAverage": "8", "thumbnailImage": "http://swiftapi.rubypaper.co.kr:2029/thumbnail/REP_GL0000096285_1_1.jpg" }, { "genreNames": "드라마", "genreIds": "005", "title": "아버지의 깃발", "movieId": "P00000249209", "linkUrl": "http://swiftapi.rubypaper.co.kr:2029/hoppin/detailView?movieId=P00000249209", "participant": "47", "ratingAverage": "8", "thumbnailImage": "http://swiftapi.rubypaper.co.kr:2029/thumbnail/REP_00000042749_1_1_0215.jpg" }, { "genreNames": "드라마,로맨스", "genreIds": "005,016", "title": "여덟번의감정", "movieId": "P00000249753", "linkUrl": "http://swiftapi.rubypaper.co.kr:2029/hoppin/detailView?movieId=P00000249753", "participant": "1", "ratingAverage": "6", "thumbnailImage": "http://swiftapi.rubypaper.co.kr:2029/thumbnail/REP_GL0000185556_1_1.jpg" }, { "genreNames": "액션/모험,드라마", "genreIds": "200,005", "title": "레드", "movieId": "P00000250006", "linkUrl": "http://swiftapi.rubypaper.co.kr:2029/hoppin/detailView?movieId=P00000250006", "participant": "1255", "ratingAverage": "8.7", "thumbnailImage": "http://swiftapi.rubypaper.co.kr:2029/thumbnail/REP_00000042890_1_1_0906.jpg" }, { "genreNames": "드라마", "genreIds": "005", "title": "영광의 탈출", "movieId": "P00000250388", "linkUrl": "http://swiftapi.rubypaper.co.kr:2029/hoppin/detailView?movieId=P00000250388", "participant": "5", "ratingAverage": "6.8", "thumbnailImage": "http://swiftapi.rubypaper.co.kr:2029/thumbnail/REP_GL0000158468_1_1.jpg" }, { "genreNames": "드라마", "genreIds": "005", "title": "크로싱", "movieId": "P00000250708", "linkUrl": "http://swiftapi.rubypaper.co.kr:2029/hoppin/detailView?movieId=P00000250708", "participant": "17", "ratingAverage": "8.8", "thumbnailImage": "http://swiftapi.rubypaper.co.kr:2029/thumbnail/REP_00000043430_1_1_0227.jpg" }] } } }


위의 코드를 구조화 하면 이런식이다 맞지요잉
일단 어려우니까 배열은 무시하고 아래처럼 간단하게 표현해봤어요.

보면 hoppin이 최상위 노드이고 그 하위 노드에 여러 노드들이 반복되는 형식이에요.
그래서 우리가 영화 목록 정보를 얻으려면 다음의 순서를 밟아야해요.

빠 - 밤

hoppin > movies > movie > 영화목록정보

와씨. 끝났네 끝났어.

완전 깔끔쓰. 보시면 movie키만 NSArray로 되어있어요. 얘만 왜 배열이냐구요?

 let hoppin = apiDictionary["hoppin"] as! NSDictionary let movies = hoppin["movies"] as! NSDictionary let movie = movies["movie"] as! NSArray


여기 배열이지롱? 아까 배열은 아니 리스트는
리스트 형태일때(영화 목록 배열을 읽어들일때) -> NSArray
이렇게 말했었죠!


아 코드가 기니까 헷갈려..row가 뭐지 싶어
lldb를 이용해서 봤더니 (으쓱)
아.. key와 value로 이뤄진 pairs들이 보여요. 아 뭔가 데이터가 더럽네


po로 보니 더 깔끔하네요


좋아요.

이제 API를 통해서 영화 목록 데이터를 내려받을 수 있기 때문에, 수동으로 데이터를 직접 정의하던 부분은 더이상 필요하지 않아요!
시원하게 주석으로 밀어줄게요.

listViewController.swift 코드참조

// ListViewcontroller.swift // MyMovieChart // // Created by prologue on 2018. 2. 18.. // Copyright © 2018년 SQLPRO. All rights reserved. // import UIKit class ListViewController: UITableViewController { // 튜플 아이템으로 구성된 데이터 세트 // 시원하게 주석처리 /* var dataset = [ ("다크나이트", "영웅물에 철학에..예술이 되다", "2008-09-04", 8.95, "darknight.jpg"), ("호우시절", "때를 알고 내리는 좋은 비", "2009-10-08", 7.31, "rain.jpg"), ("말할 수 없는 비밀", "여기서 너까지 다섯 걸음", "2015-05-07", 9.19, "secret.jpg") ] */ // 테이블 뷰를 구성할 리스트 데이터 lazy var list : [MovieVO] = { var datalist = [MovieVO]() // 시원하게 주석처리 /* for (title, desc, opendate, rating, thumbnail) in self.dataset { let mvo = MovieVO() mvo.title = title mvo.description = desc mvo.opendate = opendate mvo.rating = rating mvo.thumbnail = thumbnail datalist.append(mvo) } */ return datalist }() override func viewDidLoad() { // 호핀 api 호출을 위한 uri생성 let url = "http://swiftapi.rubypaper.co.kr:2029/hoppin/movies?version=1&page=1&count=10&genreId=&order=releasedateasc" let apiURI: URL! = URL(string: url) // Rest API를 호출 let apidata = try! Data(contentsOf: apiURI) /* // 데이터 전송 결과를 로그로 출력(반드시 필요한 코드는 아님) let log = NSString(data: apidata, encoding: String.Encoding.utf8.rawValue) ?? "" NSLog("API Result=\(log)") */ // JSON 객체를 파싱하여 NSDictionary 객체로 받음 do { let apiDictionary = try JSONSerialization.jsonObject(with: apidata, options: []) as! NSDictionary //데이터 구조에 따라 차례대로 캐스팅하며 읽어온다. let hoppin = apiDictionary["hoppin"] as! NSDictionary let movies = hoppin["movies"] as! NSDictionary let movie = movies["movie"] as! NSArray // Iterator 처리를 하면서 API 데이터를 MovieVO 객체에 저장한다. for row in movie { //순회 상수를 NSDictionary 타입으로 캐스팅 let r = row as! NSDictionary //테이블 뷰 리스트를 구성할 데이터 형식 let mvo = MovieVO() //movie배열의 각 데이터를 mvo 상수의 속성에 대입 mvo.title = r["title"] as? String mvo.description = r["genreNames"] as? String mvo.thumbnail = r["thumbnailImage"] as? String mvo.detail = r["linkUrl"] as? String mvo.rating = ((r["ratingAverage"] as! NSString).doubleValue) //list 배열에 추가 self.list.append(mvo) } } catch { } } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return self.list.count } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { // 주어진 행에 맞는 데이터 소스를 읽어온다. let row = self.list[indexPath.row] // ========= 여기부터 내용 변경됨 ========= let cell = tableView.dequeueReusableCell(withIdentifier: "ListCell") as! MovieCell // 데이터 소스에 저장된 값을 각 아울렛 변수에 할당 cell.title?.text = row.title cell.desc?.text = row.description cell.opendate?.text = row.opendate cell.rating?.text = "\(row.rating!)" // 추가된 부분: 이미지 뷰 처리 cell.thumbnail.image = UIImage(named: row.thumbnail!) // ========= 여기까지 내용 변경됨 ========= return cell } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { NSLog("선택된 행은 \(indexPath.row) 번째 행입니다") } }


시뮬레이터 실행화면


크 좋아요. 데이터를 잘 불러온 모습이네요.
섬네일이 표시되지 않아서 허전한 부분은! 내일 이어서 다뤄보도록 할게요
고생하셨습니다 ^ㅅ^

728x90
반응형