iOS 네트워크 통신과 API 살펴보기 5(재사용 메커니즘, 비동기 처리하기)

2021. 10. 21. 01:05iOS/iOS

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


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

오늘은 iOS 특유의 부드러운 화면을 위해 사용되는 몇가지 매커니즘 중 하나인,
재사용 메커니즘에 대해서 배워볼거에요

재사용 메커니즘(Reuse Mechanism)

테이블 뷰가 데이터 소스의 양만큼 셀을 생성하면 스크롤 시 움직여야할 셀의 수와, 이에 대한 처리를 위해 사용되는
메모리도 그만큼 많아지므로 우리가 기대하는 iOS의 부드러운 스크롤을 기대하기 어렵습니다.
하지만 재사용 메커니즘 덕분에 부드러운 UI를 구현할 수 있게되죠.

테이블 뷰에서 재사용 메커니즘이 동작하는 원리는 다음과 같습니다.

1. 테이블 뷰가 화면에 나타낼 셀 객체를 자신의 데이터 소스에게 요청합니다.
2. 데이터 소스는 테이블 뷰의 재사용 큐(Reuse Queue)에서 사용 가능한 셀이 있는지 확인하여 만일 있으면 그중 하나를 꺼내 전달하고, 없으면 새로운 셀을 생성합니다.
3. tableView(_:cellForRowAt:)메서드가 셀의 컨텐츠를 구성한 다음 반환하면 테이블 뷰는 이 셀을 받아 화면에 표시합니다.
4. 사용자가 테이블 뷰를 스크롤 함에 따라 화면을 벗어난 셀은 테이블 뷰에서 제거되지만 완전히 삭제되는것이 아니라 재사용 큐에 추가됩니다.
5. 사용자의 스크롤에 따라 1~4를 반복합니다.


1에서 데이터 소스에 요청할 때 사용하는 메서드가 바로 tableView(_:cellForRowAt:)입니다. 이 메서드에 작성된 내용을 확인해봅시다.
아래의 메서드가 재사용 큐에서 사용 가능한 셀을 확인하고, 없으면 셀을 새로 생성하여 반환하는 2번 역할을 합니다.

tableView.dequeueReusableCell(withIdentifier: "ListCell") as! MovieCell


스크롤로 인해 화면을 벗어나게 된 기존 셀들은 테이블 뷰에서 제거한 후 재사용을 위해 큐에 저장합니다.
새로운 셀이 필요해지는 시점이 되면 큐에 저장된 셀을 꺼내어 재사용하고, 저장된 셀이 없을 경우 새로 생성합니다.

주의할점!
재사용 큐에 저장된 셀 자체는 재사용하지만, 셀의 콘텐츠는 tableView(_:cellForRowAt:)메서드를 통하여 매번 새롭게 구성된다는 점입니다. 다시 말해 화면에 새로운 셀을 표시할 때마다 tableView(_:cellForRowAt:)메서드는 매번 다시 실행됩니다.

이미 만들어져 화면에 노출된 cell이라 하더라도, 일단 화면을 벗어나 테이블 뷰에서 제거되고 나면 이 셀을 다시 화면에 표시하기 위해서는
해당 메서드를 거쳐야합니다.

바로 이거에요.
섬네일을 읽어오는 코드를 구현했을때 스크롤이 원활하지 않고 버벅댔던 이유말이에요..

스크롤할때마다 cellForRowAt메서드가 실행되면서 매번 셀의 콘텐츠를 재구성하다보니,
그때마다 섬네일 이미지를 웹 서버에서 내려 받아야 하기 때문입니다. 메모리에 저장된 데이터를 읽어 들이는 것은 찰나의 순간이지만,
네트워크 통신을 통해 매번 데이터를 읽어 들이면 셀을 구성하는 데 시간이 걸릴 수 밖에 없습니다.


위의 문제점들을 피하려면 아래의 사항을 준수해야합니다.

1. 반복적으로 호출되는 메서드의 내부에는 네트워크 통신 등 처리 시간이 긴 로직을 포함하지 않아야 합니다.
2. 네트워크 통신을 통해 읽어온 데이터는 재사용할 수 있도록 캐싱(Caching)처리하여 될 수 있으면 네트워크 통신횟수 줄이기
3. 네트워크 통신이나 시간이 오래 걸리는 코드를 사용할때는 비동기(Asynchronize)로 처리하는 것이 바람직합니다.

2번 원칙은 메모이제이션(memoization)기법이라고 불리는 것으로서, 프로그램이 동일한 계산을 반복해야할 때
이전에 계산한 값을 메모리에 저장함으로써 반복 수행을 제거하고 프로그램의 실행 속도를 빠르게 하는 기술입니다.
(이미지를 네트워크로부터 읽어오는 것도 일종의 계산이므로 메모이제이션 기법을 사용하면 높은 효율성을 얻을 수 있습니다.)




자 말로만 해선 잘 모르겠습니다. 실제 코드를 수정해보면서 익혀봅시다!

step1. 이미지를 읽어오는 코드를 cellForRowAt이 아닌 다른 위치로 옮기는 것입니다.

적절한 위치로는 callMovieAPI() 메서드에서 API데이터를 읽어온 다음이 좋겠습니다.

즉, API호출로부터 읽어온 데이터를 순회하는 과정에서 이미지를 내려받아서 배열에 저장하고,
cellForRowAt메서드가 호출될 때는 미리 내려받은 이미지를 사용하는 것입니다.

import Foundation import UIKit class MovieVO { var thumbnail: String? // 영화 섬네일 이미지 주소 var title: String? // 영화 제목 var description: String? // 영화 설명 var detail: String? // 상세정보 var opendate: String? // 개봉일 var rating: Double? // 평점 //영화 썸네일 이미지를 담을 UIImage 객체를 추가한다. var thumbnailImage: UIImage? }

step2. ListViewController.swift 파일에서 callMovieAPI()메서드와 cellForRowAt메서드 두 곳을 수정합니다.

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) //섬네일 이미지 URL을 이용하여 이미지를 읽어들인 다음 MovieVO 객체에 UIImage를 저장하는 코드입니다. //웹상에 있는 이미지를 읽어와 UIImage 객체로 생성 let url: URL! = URL(string: mvo.thumbnail!) let imageData = try! Data(contentsOf: url) mvo.thumbnailImage = UIImage(data: imageData) //list 배열에 추가 self.list.append(mvo) //전체 데이터 카운트를 얻는다 let totalCount = (hoppin["totalCount"] as? NSString)!.integerValue //totalCount가 읽어온 데이터 크기와 같거나 클 경우 더보기 버튼을 막는다. if (self.list.count >= totalCount) { //메시지로 남기는거 구현해보기 self.moreBtn.isHidden = true } } } catch { } }

cellForRowAt()메서드

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!)" /* // 섬네일 경로를 인자값으로 하는 URL객체를 생성 let url: URL! = URL(string: row.thumbnail!) // 이미지를 읽어와 Data객체에 저장 let imageData = try! Data(contentsOf: url) // UIImage객체를 생성하여 아울렛 변수의 image 속성에 대입 cell.thumbnail.image = UIImage(data: imageData) */ //이미지 객체를 대입한다. cell.thumbnail.image = row.thumbnailImage return cell }

이 방식으로 코드를 작성하면 최초 한번만 이미지를 내려받을 뿐,
화면을 스크롤 해서 셀이 재구성되어도 이후로는 이미지를 내려받지 않습니다. 이미 내려받아둔 이미지 객체를 꺼내어 쓸 뿐!

메모이제이션 기법 기억하시죠?
2. 네트워크 통신을 통해 읽어온 데이터는 재사용할 수 있도록 캐싱(Caching)처리하여 될 수 있으면 네트워크 통신횟수 줄이기

아 잠만요.. 캐싱..캐싱.. 제대로 알고갑시다.

더보기

캐시란 잠시 저장해준다는 의미이고 기능이다.

캐시 메모리라고 하면 실제 메모리와 cpu 사이에서 빠르게 전달을 위해서 미리 데이터들을 저장해두는 좀더 빠른 메모리이다. 네트워크에서 캐시는 로컬에 파일을 미리 받아놓고, 그 내용을 보거나 웹서버에서도 매번 로딩을 해야하는 파일들을 미리 로등해두고, 응답을 주기도 한다.

단점은 캐시가 붙은 기능에 저장된 데이터는 지워질 수 있다는 것이다.


끝!!!!!!!


인줄 알았는데... 아직 문제점들이 남아있습니다.
화면을 구성할때 섬네일 이미지 여러개를 내려받아야 하므로 초기 화면 로딩이 지연되는 문제가 생깁니다. 이미지의 크기나 초기 데이터가 적으면 차이가 적겠지만, 모바일 특수성을 감안한다면 네트워크가 매우 느려지는 상황을 고려해야합니다.

사용자가 네트워크 통신이 느린 지역에서 앱을 실행하면 한동안 스플래시 화면만 보일 수도 있다는 거죠..
그리고 초기 데이터가 많아지면 그만큼의 이미지를 메모리에 저장하고 있어야 한다는 부담도 있습니다.
(이미지는 텍스트보다도 메모리 사용량이 많아요)

악... 스크롤 하지 않는다 하더라도 표현하지 않을 데이터까지 내려받아야한다는게 너무 킹받네요..

재사용 메커니즘을 고려하지 않는다면! 사실 섬네일 이미지를 내려받기에 가장 좋은 위치는 처음에 작성했던 것처럼 셀을 만드는 cellForRowAt내부입니다. 사용자의 요청이 있을때만 이미지를 받으면 되니까요.

게다가 재사용 메커니즘의 이슈는 메모이제이션 기법을 사용하면 대응할 수 있습니다. 셀을 처음 생성할때만 이미지를 내려받고,
재사용 메커니즘에 의해 테이블 뷰에서 제거했던 셀을 다시 생성할 때는 내려받기 대신 앞에서 받아두었던 이미지를 사용하는 것이죠.
근데 그래도 스크롤이 매끄럽지 못한 현상을 피할수는 없어요..ㅎㅎ

셀 하나하나가 이미지를 내려받은 다음에야 반환되므로 그 동안에는 계속 기다릴 수 밖에 없죠..
이러한 현상을 블로킹(Bloking)이라고 합니다.
서버측 프로그래밍에서 많이 사용되는 용어인데, 하나의 긴 요청을 처리하는 동안 다른 요청은 처리할 수 없게되어 앞의 처리가 끝날 때까지
대기 상태가 발생하는 경우를 말합니다.

이러한 블로킹현상을 피하려면 앞서 말한 3번의 원칙에 해당하는 비동기 처리를 적용해야 합니다.
3. 네트워크 통신이나 시간이 오래 걸리는 코드를 사용할때는 비동기(Asynchronize)로 처리하는 것이 바람직합니다.


비동기(Asynchronize) 처리 기법

프로그램이 기능이나 연산을 처리하는 방식에 대한것
ex) 팩스 기다리는 동안 음료 다과도 준비하고, 유도리 있는 업무 처리 가능
차례대로 처리하되, 시간이 걸리는 업무는 진행해둔 채로 기다리는 동안 다른 업무를 처리하는것.
중간에 업무가 완료되면 그에 이어서 처리해야 할 다음 업무를 처리.
그 사이에 다시 기다리는 시간이 생기면 다른 업무를 계속 처리하는 방식.

동기(synchronize) 방식

1번부터 6번까지의 업무를 반드시 순서대로 진행합니다.
담당자들한테 팩스 답변이 다 올때까지 그저 가만히...기다려야 해요. 그래야 다음 작업이 가능하니까요.
근데 이건, 너무 비효율적이잖아요.

1. 회의실을 예약하세요
2. 협력업체 담당자 모두에게 팩스를 보내, 회의 참석 여부를 확인받으세요
3. 오늘 회의할 자료를 인원수대로 복사하세요
4. 회의를 위한 간단한 음료와 다과를 준비하여 회의 테이블에 세팅해두세요. 주문은 온라인몰로 하고 배송받으면 됩니다.
5. 참, 물이 떨어졌네요. 생수 주문해서 정수기에 채워두세요
6. 이번 진행할 프로젝트 C에 대한 보고서를 마무리해서 결재받으세요



동기방식보다 비동기방식보다 대기 시간을 줄여줄 수 있어서 훨씬 효율 적인건 맞아요.
근데 무한정 비동기 방식으로 처리할 순 없어요. 이러면 업무의 구성이 훨씬 복잡해지기 때문입니다.
이거 하다가 저거하다가 하면 내가 뭘 하고 있었는지도 헷갈릴거고..

이런면에서 보면 동기방식은 중간에 대기하는 시간이 좀 효율성을 떨어트리긴 하지만,
일관된 업무 흐름을 보장할 수 있고 동시 다발적으로 업무가 발생하지 않아,
이를 제어하기 위한 대응이 불필요하여 업무 구성이 단순해진다는 장점이 있습니다.

앞서 작성한 코드를 예로 들어보면 우리는 섬네일 이미지를 내려받을 때까지 셀 구성을 완료하지 못하고 기다려야 했습니다.
그리고 그 결과 화면의 스크롤이 매끄럽지 못한 현상이 발생했었죠. 이러한 구성이 동기방식이라고 할 수 있습니다.

프로그램에서 비동기 방식의 처리는 긴 시간이 걸릴것으로 예상하는 기능을, "새로운 실행 흐름을 만들어서 실행하는 방식"입니다.
이를 통해 "프로그램 메인 실행 흐름이 아무 영향을 받지 않도록 하여", 응답을 대기하는 상황이 발생하지 않게 처리해줍니다.
이런 구조로 된 프로그래밍을 비동기 프로그래밍이라고 합니다.


Swift에서는 크게 두가지 방식의 비동기 구현 기능을 제공합니다

1. delegate 패턴

네트워크 통신 자체에만 국한된 비동기 처리로서 NSURLConectionDelegate 객체를 이용하는데,
델리게이트 객체에 이미지 내려받기에 대한 처리를 위임한 다음, 내려받기가 완료되면 델리게이트 객체가 특정 메서드를 호출하게 하여
이 메서드 내부에 처리할 작업을 정의하는 방식으로 구현합니다.

2. 비동기 실행 함수 DispatchQueue.main.async()

개발자가 내부적으로 프로세스나 스레드에 직접 접근하지 않고도 비동기 방식으로 처리를 할 수 있도록 지원합니다.
글로벌 범위에서 사용할 수 있는 이 함수는 블록(Block)GCD(Global Centeral Dispatch)를 이용하는데,
이중 GCD는 애플에서 개발한 기술로서 병렬처리스레드 풀에 기반을 둔 비동기 방식을 구현해줌으로써
멀티코어 프로세서에 최적화된 앱을 지원하고자 만들어진 것입니다.
이 함수는 디스패치큐를 생성하여 비동기 실행 흐름을 만들어내고 그 흐름 위에서 원하는 코드가 독립적으로 실행되도록 해줍니다.


자 이제 실제로 코드로 작성해보겠습니다. 아래의 두가지를 처리해볼건데 구현 목적은 다음과 같습니다.

1. 비동기 기법 : 이미지를 내려받을 때를 위한 처리
2.메모이제이션 기법 : 테이블 뷰에서 제거된 셀이 재사용 큐에 의해 다시 구성될 때를 위한 처리


먼저 섬네일 이미지를 처리하는 커스텀 메서드를 정의하고, 이 메서드 내부에서 메모이제이션 기법을 적용합니다.

ListViewController.swift

func getThumbnailImage(_ index: Int) -> UIImage { //인자값으로 받은 인덱스를 기반으로 해당하는 배열 데이터를 읽어옴 let mvo = self.list[index] //메모이제이션 : 저장된 이미지가 있으면 그것을 반환하고, 없을 경우 내려받아 저장한 후 반환 if let savedImage = mvo.thumbnailImage { return savedImage } else { let url: URL! = URL(string: mvo.thumbnail!) let imageData = try! Data(contentsOf: url) mvo.thumbnailImage = UIImage(data: imageData) // UIImage를 MovieVO객체에 우선 저장 return mvo.thumbnailImage! //저장된 이미지를 반환 } }


cellForRowAt()

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { // 주어진 행에 맞는 데이터 소스를 읽어온다. let row = self.list[indexPath.row] NSLog("호출된 행번호: \(indexPath.row), 제목:\(row.title!)") // ========= 여기부터 내용 변경됨 ========= 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객체도 내부에서 참조할 수 있음. //클로저 내부의 코드가 실행되는 시점이 cellForRowAt메서드의 실행이 모두 종료된 후라면 이 메서드 내부에서 선언된 cell객체도 함께 제거되어야 하지만, 클로저는 내부 함수에서 사용되는 외부 환경을 계속 유지해주는 특성 때문에 cell객체가 제거되지 않고 계속 살아있을 수 있습니다. DispatchQueue.main.async(execute: { cell.thumbnail.image = self.getThumbnailImage(indexPath.row) }) return cell } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { NSLog("선택된 행은 \(indexPath.row) 번째 행입니다") }


1. 클로저의 특성상 연관된 외부 범위 변수를 그대로 사용 가능해서 인자값으로 전달되지 않은 cell객체도 내부에서 참조할 수 있습니다.
2. 클로저 내부의 코드가 실행되는 시점이 cellForRowAt메서드의 실행이 모두 종료된 후라면, 이 메서드 내부에서 선언된 cell객체도 함께 제거되어야 하지만, 클로저는 내부 함수에서 사용되는 외부 환경을 계속 유지해주는 특성 때문에 cell객체가 제거되지 않고 계속 살아있을 수 있습니다.
3. cellForRowAt()메서드는 재사용 큐로부터 받은 테이블 뷰 셀을 데이터를 이용하여 구성한 다음, 섬네일 이미지를 처리할 차례가 되면 비동기 함수를 사용하여 새로운 실행 흐름을 만들어 냅니다.
새로운 실행 흐름에서는 self.getThumbnailImage(indexPath.row) 함수를 호출하여 섬네일 이미지를 가져오게 한 다음,
섬네일 이미지를 가져오는 과정을 기다리지 않고 다음 행으로 이동하여 셀을 반환하는 것으로 메서드를 종료합니다.
4. 새로운 실행 흐름에서는 섬네일 이미지를 반환받으면 그 결과를 테이블 뷰 셀의 섬네일 항목에 할당합니다.
바깥함수인 cellForRowAt메서드가 종료되었어도 내부 함수인 클로저는 영향을 받지 않습니다.

주의할점

비동기 방식으로 처리된 코드는 기존 실행 흐름과 별도로 처리되므로 실행 순서를 보장하지는 못한다는 사실입니다.
즉, DispatchQueue.main.async()메서드 내부에 있는 코드가 먼저 작성되었다고 해서,
DispatchQueue.main.async() 다음에 작성된 코드보다 먼저 실행된다는 보장이 없습니다.
실제로 .async()앞뒤에 로그를 추가하여 실행되는 순서를 확인해보면 .async()아래에 있는 코드가 먼저 실행됨을 확인할 수 있습니다.

cellForRowAt()

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { // 주어진 행에 맞는 데이터 소스를 읽어온다. let row = self.list[indexPath.row] NSLog("호출된 행번호: \(indexPath.row), 제목:\(row.title!)") // ========= 여기부터 내용 변경됨 ========= 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객체도 내부에서 참조할 수 있음. //클로저 내부의 코드가 실행되는 시점이 cellForRowAt메서드의 실행이 모두 종료된 후라면 이 메서드 내부에서 선언된 cell객체도 함께 제거되어야 하지만, 클로저는 내부 함수에서 사용되는 외부 환경을 계속 유지해주는 특성 때문에 cell객체가 제거되지 않고 계속 살아있을 수 있습니다. DispatchQueue.main.async(execute: { NSLog("비동기 방식으로 실행되는 부분입니다") cell.thumbnail.image = self.getThumbnailImage(indexPath.row) }) NSLog("메서드 실행을 종료하고 셀을 리턴합니다") return cell }

이때문에 비동기로 구현된 기능의 결과를 받아 또 다른 무언가를 처리해야하는 연관된 기능들이 있다면, 이 기능들은 모두 비동기로 분리된 새로운 실행 흐름 내에서 처리하도록 구성해야 합니다.동기 방식의 프로그래밍 스타일 코드를 작성했다면 종종 문제가 발생할 수 있습니다.

또한, 비동기로 작성하는 프로그램은 실행 흐름이 눈에 보이는 것과 다르게 동작할 수 있어서 예기치 않은 버그가 발생할 수도 있습니다.
따라서 비동기 방식의 프로그래밍은 필요한 부분에 한해서만 구현해야 합니다.




이렇게 메모이제이션, 비동기처리기법이 모두 적용된 테이블 뷰는 스크롤시 훨씬 자연스러운 흐름을 보여줄 뿐만 아니라,
여러번 반복해서 이미지를 내려받아 네트워크 통신량을 늘리지도 않습니다.
앱을 실행해보면 매우 매끈하게 화면이 스크롤 되는 것을 볼 수 있을 것입니다.

본인이 만든 앱의 실행 성능과 문제점을 예상하고 그에 맞는 대응을 해줄 수 있는 것은 부단히 고민하고 노력한 경험에 의한 결과입니다.
이때부터 비로소 한 단계 발전하기 시작하는 것이라 할 수 있습니다.


전체코드 참고

MovieVO.swift

import Foundation import UIKit class MovieVO { var thumbnail: String? // 영화 섬네일 이미지 주소 var title: String? // 영화 제목 var description: String? // 영화 설명 var detail: String? // 상세정보 var opendate: String? // 개봉일 var rating: Double? // 평점 //영화 썸네일 이미지를 담을 UIImage 객체를 추가한다. var thumbnailImage: UIImage? }


ListViewController.swift

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() } @IBOutlet weak var moreBtn: UIButton! 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) //섬네일 이미지 URL을 이용하여 이미지를 읽어들인 다음 MovieVO 객체에 UIImage를 저장하는 코드입니다. //웹상에 있는 이미지를 읽어와 UIImage 객체로 생성 let url: URL! = URL(string: mvo.thumbnail!) let imageData = try! Data(contentsOf: url) mvo.thumbnailImage = UIImage(data: imageData) //list 배열에 추가 self.list.append(mvo) //전체 데이터 카운트를 얻는다 let totalCount = (hoppin["totalCount"] as? NSString)!.integerValue //totalCount가 읽어온 데이터 크기와 같거나 클 경우 더보기 버튼을 막는다. if (self.list.count >= totalCount) { //메시지로 남기는거 구현해보기 self.moreBtn.isHidden = true } } } catch { } } func getThumbnailImage(_ index: Int) -> UIImage { //인자값으로 받은 인덱스를 기반으로 해당하는 배열 데이터를 읽어옴 let mvo = self.list[index] //메모이제이션 : 저장된 이미지가 있으면 그것을 반환하고, 없을 경우 내려받아 저장한 후 반환 if let savedImage = mvo.thumbnailImage { return savedImage } else { let url: URL! = URL(string: mvo.thumbnail!) let imageData = try! Data(contentsOf: url) mvo.thumbnailImage = UIImage(data: imageData) // UIImage를 MovieVO객체에 우선 저장 return mvo.thumbnailImage! //저장된 이미지를 반환 } } 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] NSLog("호출된 행번호: \(indexPath.row), 제목:\(row.title!)") // ========= 여기부터 내용 변경됨 ========= 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객체도 내부에서 참조할 수 있음. //클로저 내부의 코드가 실행되는 시점이 cellForRowAt메서드의 실행이 모두 종료된 후라면 이 메서드 내부에서 선언된 cell객체도 함께 제거되어야 하지만, 클로저는 내부 함수에서 사용되는 외부 환경을 계속 유지해주는 특성 때문에 cell객체가 제거되지 않고 계속 살아있을 수 있습니다. DispatchQueue.main.async(execute: { NSLog("비동기 방식으로 실행되는 부분입니다") cell.thumbnail.image = self.getThumbnailImage(indexPath.row) }) NSLog("메서드 실행을 종료하고 셀을 리턴합니다") return cell } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { NSLog("선택된 행은 \(indexPath.row) 번째 행입니다") } }
728x90
반응형