UICollectionView 셀의 이미지 로딩 속도 개선: NSCache로 이미지 캐싱

jane·2022년 6월 9일
0

iOS

목록 보기
23/32
post-thumbnail

UICollectionView나 UITableView를 만들때 셀에 이미지가 포함된 경우 다음과 같은 두가지 상황을 고려해야한다.
1. 이미지가 로딩되는 속도가 느린 문제
2. 빠르게 스크롤시 맞지 않는 이미지가 나타나는 문제

이번 포스팅에서는 첫번째 이슈인 이미지 로딩 속도에 대해 알아보자

이미지가 로딩되는 속도가 느린 문제

두가지 방법으로 해결 가능하다.
1. 이미지 네트워크 통신 global Queue에서 하기
2. 이미지 캐싱

이미지 네트워크 통신 global Queue에서 하기

다들 테이블뷰의 cellForRowAt이나 컬렉션뷰의 cellForItemAt에서는 무거운 작업을 하지 않는 것이 좋다는 것을 알고 있을 것이다
이 메서드는 최대한 가볍게 유지해야하기 때문에,,

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MovieListCollectionViewCell", for: indexPath) as! MovieListCollectionViewCell
        cell.configure(with: viewModel.itemViewModels[indexPath.row])
        return cell
    }

따라서 URL을 통해 이미지를 로딩하는 작업같이 무거운 작업은 비동기적으로 일어나기도 해서 메인 스레드에서 작업하기보다는 concurrent한 Global Queue에서 하도록 하자.

이미지 로딩을 하기 위해서 URLSession.shared.dataTask를 활용해보자.
이 경우에는 URLSession 자체가 Global Queue에서 동작하기 때문에 직접 Dispatch Queue를 만들어줄 필요 없이 사용 가능하다.

 if let imageUrl = URL(string: urlString) {
    let urlRequest = URLRequest(url: imageUrl, cachePolicy: .returnCacheDataElseLoad)
    self.dataTask = URLSession.shared.dataTask(with: urlRequest) { data, response, error in
        if let _ = error {
            DispatchQueue.main.async {
                self.image = UIImage()
            }
            return
        }
    }
    self.dataTask?.resume()
}

이미지 캐싱

다음으로는 캐시다.
캐시에는 디스크 캐시와 메모리 캐시 두 종류가 있다.

  • 디스크 캐시의 경우 데이터를 파일 형태로 디스크 영역에 저장하여 앱을 종료하고 다시 실행해도 디스크에 캐시가 남아있지만,

  • 메모리 캐시의 경우에는 앱이 종료되면 사용중이던 메모리 영역을 반환하여 앱 종료와 함께 캐싱된 정보도 같이 사라지게 된다...ㅎ

따라서 디스크 캐시로는 앱이 처음 실행되었을 때의 이미지 로딩속도를 개선할 수 있고, 메모리 캐시로는 이미 한번 보여진 이미지의 로딩속도를 개선할 수 있다.

NSCache로 이미지 메모리 캐싱

앱을 실행하는 도중에 컬렉션뷰를 스크롤하여 셀이 화면 밖으로 나간 경우 셀이 reuseable queue에 들어가게 되는데,
이때 셀의 모든 데이터가 초기화되어서 다시 뒤로 스크롤하면 이미지를 다시 로딩해야하는 상황이 발생한다.
이때 한번 네트워크 통신을 통해 받아온 이미지는 캐싱을 하여 더이상 무거운 네트워크 통신 작업을 하지 않고 캐시된 이미지를 사용하여 이미지 로딩 속도를 개선할 수 있다.

guard let urlString = MovieURL.image(posterPath: posterPath).url?.absoluteString else {
    return
}
let cacheKey = NSString(string: urlString)
if let cachedImage = ImageCacheManager.shared.object(forKey: cacheKey) {
    self.image = cachedImage
    return
}

디스크 캐싱에 대한 고민

메모리 캐싱을 통해 한번 화면에 보여진 이미지를 다시 볼 때는 속도가 개선된다고 해도,
앱을 실행할때는 초기화되기 때문에 초기 화면에서 이미지를 로딩하려면 3초정도의 딜레이가 발생한다.

따라서 디스크 캐시를 추가로 구현하여
한번 받아온 리스트의 이미지들을 앱을 종료하고 다시 실행하더라도 캐싱된 이미지들을 사용하여 이미지 로딩속도를 개선할 수 있지 않을까는 생각도 해보았다.

디스크 캐싱은 UserDefault나 FileManager에 저장하는 형태로 구현하는데,
UserDefault의 경우 사용자의 간단한 정보를 저장하는 용도이기 때문에 배제하였다.

서치해보니... 이 글에서처럼
FileManager에 이미지 data를 저장해놓고 저장된 path를 SQLite에 저장하는 방식을 많이 사용하는 것 같다.

처음에는 Core Data에 저장하는 방식을 생각했는데,
서치해보니 스택오버플로우나 CoreData 공식문서에서도 이미지나 소리 데이터를 Core Data에 저장하는것은 비추천이라고 한다.

Core Data 공식문서에서 SQLite를 추천하는 모습..ㅋㅋㅋㅋ

디스크 캐싱을 적용한 예를 찾아보았는데...
카카오톡에서 이미지를 다운받지 않고 그냥 보기만 하더라도 처음 봤을때만 이미지를 받아오고, 디스크 캐시에 저장한다고 한다.
따라서 그 다음에는 앱을 껐다가 켰음에도 불구하고 캐싱된 이미지를 불러올 수 있는 것이다.

카카오톡 사용하다보면 이렇게 캐시 용량이 늘어난다. 심한 사람은 몇기가까지 찬다는데 나는 적군..ㅎ

암튼 어떻게 구현할지 찾아보기는 했는데, 아무래도 디스크에 저장하는 방식은 좀 아닌것 같다.

내 앱의 경우에는 모든 영화 포스터를 디스크 캐싱해놓기는 사용자 입장에서 부담스러울 것 같다.

그냥 메모리 캐싱만 사용하는 걸로~ㅎ

-> 처음에는 이렇게 생각했는데 결국 디스크 캐싱도 사용하였고, 그 대신 캐시의 유효기간과 최대 저장가능 용량을 제한하였다.

결론, NSCache만 사용함

NSCache 객체를 가진 ImageCacheManager와,
UIImageView를 상속받은 DownloadableUIImageView 클래스를 만들어서 이미지 로딩을 관리하였다.

getImage()는 url을 이용하여 이미지를 가져오는 함수이다.

먼저 String으로 변환한 url을 key로 하여 저장된 이미지가 캐시에 존재하는지 확인하고 나서 캐시에 없다면 그제서야 네트워크 통신을 통해 이미지를 가져오고 캐싱한다.

class ImageCacheManager {
    static let shared = NSCache<NSString, UIImage>()
    private init() {}
}

class DownloadableUIImageView: UIImageView {
    var dataTask: URLSessionDataTask?
    
    func getImage(with posterPath: String) {
        self.image = UIImage()
        
        guard let urlString = MovieURL.image(posterPath: posterPath).url?.absoluteString else {
            return
        }
        let cacheKey = NSString(string: urlString)
        if let cachedImage = ImageCacheManager.shared.object(forKey: cacheKey) {
            self.image = cachedImage
            return
        }
        
        if let imageUrl = URL(string: urlString) {
            let urlRequest = URLRequest(url: imageUrl, cachePolicy: .returnCacheDataElseLoad)
            self.dataTask = URLSession.shared.dataTask(with: urlRequest) { data, response, error in
                if let _ = error {
                    DispatchQueue.main.async {
                        self.image = UIImage()
                    }
                    return
                }
                DispatchQueue.main.async {
                    if let data = data, let image = UIImage(data: data) {
                        ImageCacheManager.shared.setObject(image, forKey: cacheKey)
                        self.image = image
                    }
                }
            }
            self.dataTask?.resume()
        }
    }
    
    func cancelLoadingImage() {
        dataTask?.cancel()
        dataTask = nil
    }
}
개선 전개선 후
오른쪽에서 왼쪽으로 스크롤시 버벅임오른쪽에서 왼쪽으로 스크롤시 자연스러움
  • 이미 네트워크 통신이 완료되어 캐시된 셀의 이미지를 보여줄때 시간 단축됨
  • 하지만 스크롤시 처음으로 네트워크 통신을 통해 이미지를 받아오는 경우에는 아직 해결하지 못함
    -> 디스크 캐싱으로 해결 ^^*

Reference

https://greate-future.tistory.com/103
https://yusufkamilak.com/the-correct-approach-for-image-loading-in-uicollectionview/
WWDC19 Image and Graphics Best Practices

profile
제가 나중에 다시 보려고 기록합니다 ✏️

0개의 댓글