2021. 10. 19. 23:58ㆍiOS/iOS
이 포스팅은 꼼꼼한 재은씨의 Swift 기본편 - chapter09 네트워크 통신과 API편을 참고하였습니다.
안녕하세요 bannavi입니다^ㅅ^
뭔가 네트워크 통신의 기본을 챡챡 밟아가고 있는것같아 기분이 좋네요
그럼 오늘도 어제의 포스팅에 이어서 진행해보겠습니다!
섬네일 처리(thumbnail)
UIImage(named:)는 앱의 로컬 경로에 있는 이미지를 읽어오는 방식입니다.
프로젝트 내부에 이미지 파일이 있는 경우에만 사용할 수 있다는 말이에요.
cell.thumbnail.image = UIImage(named: row.thumbnail!)
근데 우리가 웹상에서 전달받은 thumbnail(이하 섬네일)데이터는 웹상에 존재하는 경로이죠.
따라서 웹에 있는 이미지를 읽어와서 화면에 출력할 수 있도록 코드를 변경해주는 작업이 필요합니다. 아.. 그래서 섬네일이 안보였었구나
우리는 이미 웹상에 있는 경로에서 데이터를 읽어오는 Data(contentsOf:)초기화 메서드에 대해 학습한 바 있습니다.
Data(contentsOf:)를 이용하여 웹에 있는 섬네일 이미지를 데이터로 읽어온 다음, 이를 UIImage객체로 만들면 이 객체를 이용하여 화면에 이미지를 표시해줄 수 있습니다.
그래서 위의 코드를 아래와 같이 바꿔주면..!
// 섬네일 경로를 인자값으로 하는 URL객체를 생성 let url: URL! = URL(string: row.thumbnail!) // 이미지를 읽어와 Data객체에 저장 let imageData = try! Data(contentsOf: url) // UIImage객체를 생성하여 아울렛 변수의 image 속성에 대입 cell.thumbnail.image = UIImage(data: imageData)
크,, 믓찌다 믓찌다. 아래와 같은 화면이 보여집니다!
더보기 기능 구현
우리는 지금까지 REST API를 통해 읽어온 데이터를 테이블 뷰에 출력하기 작업을 성공적으로 구현했지만,
사실 이것은 절반만 완성된 것이라고 할 수 있어요.
API가 전달해주는 영화차트 데이터 중 우리가 읽어온 부분은 아주아주 일부에 불과하기 때문이에요!
아놔 그냥 한번에 데이터 가져오지 왜 굳이 더보기 기능을 또 추가하고 어쩌고 저쩌고 하는거야.....
이유가 있어요.
서버 측 문제점
대체로 서버측에선 제공 데이터를 database의 테이블에 나누어 저장하는데,
전체 데이터를 한꺼번에 제공하려면 관련된 테이블들 전체의 데이터를 읽어와야 합니다.
이를 풀 스캔(Full-Scan)이라고 하는데, 우리가 생각하는것 이상으로 많은 양의 데이터가 저장된 경우가 많대요.
근데 이걸 한번에 읽어온다?
서버측에서도 매우 부담될거고 처리속도가 늦어지는 성능상의 문제를 일으킬 수 있습니다.
클라이언트 측 문제점
데이터를 읽어오는 범위를 잘못 설계할 경우, 대부분의 데이터가 낭비될 수 있어요.
정보를 많이 제공해주면 뭐하나요? 사용자는 이 정보를 모두 필요로 한다는 보장이 없어요.
영화예매 앱만 보더라도, 당장 1-2위를 다투는 영화가 뭔지 궁금하지, 무슨 영화가 꼴등이지..에 대한 요구는 사실 적잖아요..?
사용 빈도가 낮은 데이터를 메모리에 저장하는 건 메모리 사용 효율성에 있어서도 심각한 문제에요.
그리고 데이터를 대량으로 주고받으면 서버와 클라이언트 간 네트워크 통신 패킷의 크기가 커져서 데이터를 받는 데에도 오래걸리고,
받을때도 네트워크 자원을 필요 이상으로 소모하게 돼요.
뭐 무한 요금제를 사용하는 사람들이라면 몰라도, 용량 한정 요금제를 사용하는 사람들은 킹받는 문제입니다 이거..
그래서! 위와 같은 이유로,
서버와 클라이언트 간 데이터를 주고받을 때는 전체 데이터를 한꺼번에 주고받기 보다 작은 크기로 나누어 받도록 설계하는 것이 좋습니다.
코드참조
엣헴.
여기까지 어렵지 않았죠?
그래도 다행이에요. 우리가 사용하는 API에서 page인자값을 통해 데이터를 나눠받을 수 있도록 옵션을 지원하고 있거든요.
더보기 버튼과 more(_:)메서드 추가해보기
더보기 기능은 명시적으로 사용자가 더보기 버튼을 누르면 데이터를 더 읽어오거나, 목록의 가장 아래쪽까지 스크롤이 도착하면 앱이 이를 인식하고 스스로 더보기 기능을 수행하는 방식으로 구현하는 것이 일반적입니다.
cell밑에 더보기 버튼을 추가하고 ListViewController에 more라는 액션메서드로 이어줬습니다.
ListViewController에서 현재까지 읽어온 데이터의 페이지 정보를 저장하는 변수를 선언합니다.
var page = 1
변수를 메서드 내에서 선언하면 메서드 실행이 종료될 때 변수도 함께 제거됩니다.
그래서 반드시 프로퍼티로 선언해줘야해요.
프로퍼티로 선언되면 클래스 객체가 완전히 제거되지 않는 한 변수의 값은 유지되기 때문입니다.
최초에 화면을 실행할 때 이미 1페이지에 해당하는 데이터를 읽어왔으므로 page 변수의 초기값으로 1을 할당하는 것이 맞습니다.
more함수 구현
@IBAction func more(_ sender: Any) { // 현재 페이지 값에 1을 추가한다. self.page += 1 // 호핀 API 호출을 위한 URI를 생성. let url = "http://swiftapi.rubypaper.co.kr:2029/hoppin/movies?version=1&page=\(self.page)&count=10&genreId=&order=releasedateasc" let apiURI: URL! = URL(string: url) 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 { } }
테이블 뷰 갱신 처리
이제 한가지 작업만 더 처리해주면 됩니다. 추가한 데이터를 테이블 뷰가 다시 읽어올 수 있도록 하는 부분이에요!!!
근데 일단 화면 구현이 끝나면 테이블 뷰는 데이터 소스를 다시 읽어들이지 않습니다.
정확하게는 tableView(_:numberOfRowsInSection)를 다시 호출하지 않아요.
이말은 화면 구현이 완료된 후에 (더보기 버튼을 눌러서) 데이터가 추가되더라도 테이블 뷰는 기존에 있던 데이터 크기를 유지합니다.
엥...;;;;;;??????!!!!!!! 큰일이다.
그럼 더보기 버튼이 있을 이유가......없잖아..
다 방법이 있습니다.
갱신해서 데이터를 다시 읽어들이도록 하는게 핵심이에요. 이걸 해볼게요. 어떻게 하냐구요?
하. 나왔다
reloadData() ^ㅅ^/
reloadData()이거 어디서 많이 보시지 않았나요? 저는 이거 iOS공부하면서 되게 많이 마주쳤는데
지금 아주우 정확하게 알았어요 reloadData()는 갱신할때 쓰여지는 거구나!!!!!! 하고 말이에요.
하....... 뇌속에서 공부했던 것들이 이어지는 이 느낌... 이래서 많이 고민해보는게 중요한가봐요!
음음 짜릿하다
반복되는 코드의 리팩토링까지 마친 모습이에요.
이제 더보기 버튼을 누르면 갱신이 된답니다!
import UIKit class ListViewController: UITableViewController { // 현재까지 읽어온 데이터의 페이지 정보를 저장하는 변수 var page = 1 // 테이블 뷰를 구성할 리스트 데이터 lazy var list : [MovieVO] = { var datalist = [MovieVO]() return datalist }() @IBAction func more(_ sender: Any) { // 현재 페이지 값에 1을 추가한다. self.page += 1 self.callMovieAPI() self.tableView.reloadData() } override func viewDidLoad() { callMovieAPI() } func callMovieAPI() { // JSON 객체를 파싱하여 NSDictionary 객체로 변환 let url = "http://swiftapi.rubypaper.co.kr:2029/hoppin/movies?version=1&page=\(self.page)&count=10&genreId=&order=releasedateasc" let apiURI: URL! = URL(string: url) let apidata = try! Data(contentsOf: apiURI) let log = NSString(data: apidata, encoding: String.Encoding.utf8.rawValue) ?? "데이터가 없습니다." NSLog("API Result=\(log)") 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!) // ========= 여기까지 내용 변경됨 ========= */ // 섬네일 경로를 인자값으로 하는 URL객체를 생성 let url: URL! = URL(string: row.thumbnail!) // 이미지를 읽어와 Data객체에 저장 let imageData = try! Data(contentsOf: url) // UIImage객체를 생성하여 아울렛 변수의 image 속성에 대입 cell.thumbnail.image = UIImage(data: imageData) return cell } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { NSLog("선택된 행은 \(indexPath.row) 번째 행입니다") } }
더보기 버튼 숨김 처리
자 만약에 API의 정보가 담긴 1000페이지를 마침내! 다 보여줬다고 합시다.
그럼 그럴땐 더보기 버튼이 있을 필요가 없겠죠? 더 볼게 없으니까요..ㅋㅋ
더 보여줄게 없는데 더보기 버튼이 남아있다면 사용자 입장에서도 뭐야 이거..싶을겁니다.
API에 따라서는 오류가 발생하거나 앱이 다운되는 경우가 생길수도 있을거에요.
우리가 호출하는 API는 다행히 응답 데이터에 총 데이터 건수를 포함하여 전달하고 있으므로
이를 이용하면 잔여 데이터 여부를 쉽게 계산할 수 있습니다.
더보기 버튼을 컨트롤 하기 위해 Outlet변수 moreBtn을 추가합니다.
버튼의 경우 대부분 클릭과 같은 이벤트를 처리하기 위해 액션 메서드만 추가했지만 지금은 버튼의 속성을 제어해야 하므로
아울렛 변수를 추가해야 합니다.
그리고 callMovieAPI()밑에 이 내용 추가,,
'iOS > iOS' 카테고리의 다른 글
앱을 testflight버전으로 배포해서 테스트 요청하기(feat. 참교육) (2) | 2021.10.22 |
---|---|
iOS 네트워크 통신과 API 살펴보기 5(재사용 메커니즘, 비동기 처리하기) (0) | 2021.10.21 |
iOS 네트워크 통신과 API 살펴보기 3(Json Parsing) (0) | 2021.10.18 |
iOS 네트워크 통신과 API 살펴보기 2(Open API와 공공콘텐츠) (0) | 2021.10.18 |
iOS 네트워크 통신과 API 살펴보기 1(SOAP, Restful, XML, JSON) (0) | 2021.10.17 |