-
Notifications
You must be signed in to change notification settings - Fork 0
[iOS] traveline의 이미지 관리하기
여행 일정을 사진과 함께 공유하는 서비스의 특성상, 이미지 데이터를 많이 다루게 되어 메모리, 네트워킹 관리 및 사용성을 위해 이미지 캐싱과 다운샘플링은 필수적이었습니다.
수많은 이미지를 매번 다운로드 하지 않기 위해 메모리 캐시와 디스크 캐시 로직을 구현해 사용하기로 했습니다!
- 우선 메모리 캐시에서 이미지 fetch를 시도합니다. 메모리 캐시에 있다면 가지고 옵니다.
- 메모리 캐시에 없다면, 디스크 캐시에서 이미지 fetch를 시도합니다. 디스크 캐시에 있다면 가지고 옵니다. 그리고 해당 데이터는 메모리 캐시에 저장합니다.
- 디스크 캐시에도 없다면, 이미지 다운로드를 진행하고, 다운받은 이미지 데이터는 메모리 캐시와 디스크 캐시에 저장합니다.
메모리 캐시는 NSCache
를 사용했습니다.
NSCache
는 다양한 자동 제거 정책 (Auto-eviction policy)들이 포함되어 있어 데이터의 크기가 큰 이미지를 캐싱하더라도 시스템 메모리를 너무 많이 사용하게 되는 경우를 미연에 방지할 수 있고, MutableDictionary와 다르게 저장되는 객체를 복사하지 않기 때문에 이미지 캐싱 시에 복사되지 않는 장점이 있어 사용하게 되었습니다.
디스크 캐시는 FileManager를 통해 이미지 데이터를 파일로 저장하게 구현했습니다.
traveline에서 반복적으로 만나게 되는 홈 화면에서도 여러 이미지를 보여주고 있기 때문에 앱을 껐다 키더라도 같은 이미지를 다시 다운로드하지 않기 위해 적용하게 되었습니다.
이미지 캐싱 사용 간 편의성을 위해 UIImageView의 Extension으로 아래와 같이 사용했습니다!
extension UIImageView {
/// urlString으로부터 이미지를 Load 합니다.
/// 캐시에 존재한다면 캐시에서 가져오고, 없으면 다운로드합니다.
/// - Parameter urlString: 이미지 URL 문자열
func setImage(from urlString: String?) {
guard let urlString else { return }
if let cachedImageData = TLImageCache.shared.fetch(urlString) {
image = UIImage(data: cachedImageData)
return
}
Task {
guard let downloadImageData = await TLImageDownloader.shared.download(key: self, urlString: urlString) else { return }
TLImageCache.shared.store(downloadImageData, urlString: urlString)
image = UIImage(data: downloadImageData)
}
}
}
재사용 가능한 cell 안에 UIImageView
가 있고 이미지 캐싱을 적용할 때, cell이 재사용됨에 따라 이전 cell에서 요청한 downloadTask
의 응답이 뒤늦게 도착해서 이미지가 여러번 변경되는 현상이 있었습니다.
해당 경우에 기존 task
를 cancel
하는 로직이 필요하다고 느껴졌고, 이를 위해 TLImageDownloader
에서 downloadTask
를 딕셔너리로 저장하고 있다가 cancelDownload
시에 해당 downloadTask
를 cancel
해주었습니다.
이때 딕셔너리가 thread safe
하지 않기 때문에 downloadTaskQueue
를 만들고 해당 DispatchQueue
에서 딕셔너리에 write 하도록 했습니다.