Skip to content

[iOS] traveline의 이미지 관리하기

TaeHyun edited this page Dec 12, 2023 · 2 revisions

🏞️ 이미지 관리의 필요성

여행 일정을 사진과 함께 공유하는 서비스의 특성상, 이미지 데이터를 많이 다루게 되어 메모리, 네트워킹 관리 및 사용성을 위해 이미지 캐싱과 다운샘플링은 필수적이었습니다.
수많은 이미지를 매번 다운로드 하지 않기 위해 메모리 캐시디스크 캐시 로직을 구현해 사용하기로 했습니다!


✅ TLImageCache 구현

1. 캐싱 처리 로직

  • 우선 메모리 캐시에서 이미지 fetch를 시도합니다. 메모리 캐시에 있다면 가지고 옵니다.
  • 메모리 캐시에 없다면, 디스크 캐시에서 이미지 fetch를 시도합니다. 디스크 캐시에 있다면 가지고 옵니다. 그리고 해당 데이터는 메모리 캐시에 저장합니다.
  • 디스크 캐시에도 없다면, 이미지 다운로드를 진행하고, 다운받은 이미지 데이터는 메모리 캐시와 디스크 캐시에 저장합니다.

2. 메모리 캐시와 디스크 캐시

메모리 캐시NSCache를 사용했습니다.
NSCache는 다양한 자동 제거 정책 (Auto-eviction policy)들이 포함되어 있어 데이터의 크기가 큰 이미지를 캐싱하더라도 시스템 메모리를 너무 많이 사용하게 되는 경우를 미연에 방지할 수 있고, MutableDictionary와 다르게 저장되는 객체를 복사하지 않기 때문에 이미지 캐싱 시에 복사되지 않는 장점이 있어 사용하게 되었습니다.

디스크 캐시는 FileManager를 통해 이미지 데이터를 파일로 저장하게 구현했습니다.
traveline에서 반복적으로 만나게 되는 홈 화면에서도 여러 이미지를 보여주고 있기 때문에 앱을 껐다 키더라도 같은 이미지를 다시 다운로드하지 않기 위해 적용하게 되었습니다.


3. UIImageView Extension으로 사용

이미지 캐싱 사용 간 편의성을 위해 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)
        }
    }

}

4. UIImageView 재사용 시 downloadTask 밀림 현상 해결

재사용 가능한 cell 안에 UIImageView가 있고 이미지 캐싱을 적용할 때, cell이 재사용됨에 따라 이전 cell에서 요청한 downloadTask의 응답이 뒤늦게 도착해서 이미지가 여러번 변경되는 현상이 있었습니다. 해당 경우에 기존 taskcancel하는 로직이 필요하다고 느껴졌고, 이를 위해 TLImageDownloader에서 downloadTask를 딕셔너리로 저장하고 있다가 cancelDownload 시에 해당 downloadTaskcancel 해주었습니다. 이때 딕셔너리가 thread safe하지 않기 때문에 downloadTaskQueue를 만들고 해당 DispatchQueue에서 딕셔너리에 write 하도록 했습니다.

Clone this wiki locally