기어가더라도 제대로

[번역] 스위프트에서 재사용 가능한 이미지 캐시 본문

생각정리/번역글(blog, WWDC)

[번역] 스위프트에서 재사용 가능한 이미지 캐시

Damagucci-juice 2022. 8. 30. 16:13

목차

- NSCache 를 저장소로 사용하기
- Image rendering pipeline 구축하기
- In-memory Image Cache
- Image 캐시 구현
- `ImageLoader` 로 통합
- 결론

image

거의 모든 앱은 어느 정도는 그래픽을 가지고 있다. 이로 인해 앱 개발자들에게 이미지를 다운로드하고 표시하는 것이 가장 자주 하는 일 중 하나가 되었다. 결국, 앱이 같은 이미지를 여러번 리로드하는 것은 불필요한 일일 수 있다.

이 글에서는 이미지 캐시를 생성하고, 컴바인 프레임 워크를 사용한 이미지 로더와 통합함으로써 이미지 다운로드와 캐싱을 어떻게 개선할지를 보여줄 것이다.

NSCache 를 저장소로 사용하기

그동안 캐싱 메커니즘을 iOS 프로젝트에서 사용할 때 대부분은 당신은 NSCache 클래스 사용을 고려했을 것이다. 이 클래스의 사용 장점은 스레드-세이프하고 다른 앱에 의해 메모리가 필요해질 경우 캐시에서 아이템을 지운다는 것인데, 방출 과정에서 불분명한 점도 가지는 단점도 존재한다.

어쨌든, NSCache는 캐싱에 있어서는 기본 스위프트 라이브러리나 Foundation 프레임워크에 있는 콜렉션 클래스에 비하면 더 나은 옵션이다. 이 기사에선, 우리는 NSCache 를 내부 이미지 캐시 저장소로 사용할 것이다.

대안으로, 당신은 “cache replacement policies” 를 따르는 다른 어떤 솔루션으로도 교체 가능하다.

이미지 렌더링 파이프 라인

만약 당신의 앱이 이미지를 웹에서 다운로드 받는다면, 주된 도전은 앱 반응성과 성능이다. 예를 들어 이미지를 가지고 있는 테이블 뷰를 스크롤링 하는 동안 버벅일지도 모른다. 이 이슈는 이미지 뷰에 보여질 때  이미지의 렌더링이 한번에 이루지지 않아서 생긴다. 이미지 렌더링 파이프라인은 여러 스텝으로 구성된다.

  • loading - 압축된 이미지를 메모리에 로드하기
  • decoding - 인코딩 된 이미지 data 를 픽셀 이미지 정보로 전환하기
  • rendering - 이미지 data 를 복사하고 스케일해서(규격화?) 이미지 버퍼에서 프레임 버퍼에 넣기

이들은 당신의 앱을 반응성이 떨어지게 하고, 상당한 양의 일을 메인 스레드에 더할 수 있다. 당신은 UIImageView 에 이미지를 할당하기 전에 이미지를 디코딩 하고 렌더링하는 것과 같은 잠재적인 개전 사항들을 생각할 수도 있다.

이 함수는 UIImage 를 인자로 받고 decompressed(압축의 반대) 되고 렌더링 된 버전을 반환한다. 캐시는 가벼운 이미지를 가지는 것이 말이 된다. 이는 그리는 성능을 향상시키지만, 추가적인 저장소 비용을 동반한다.

이 주제에 더 깊이 빠져보고 싶다면, WWDC 에서 발표한 iOS Memory Deep DiveImage and Graphics Best Practices 를 보길 바란다.

메모리 내부에서 이미지 캐시

이미지 캐시의 요구사항을 정의하면서 시작하는 것은 좋은 아이디어다. 캐시는 CRUD 기능(craete, read, update, delete)을 수행해야 한다. 이를 위해 script 를 사용해 우리 코드를 더욱 읽기 쉽게 하는 것이 좋겠다.

대부분의 경우에 네트워크에서 로드된 이미지를 캐싱할 것이다. 이는 왜 URL 을 key 로 했는지에 대한 완벽한 이유이다. 결국 우리는 ImageCacheType 을 다음과 같이 선언 할 수 있다.

 

* 클래스 말고 AnyObject 를 붙여줘야합니다.(사실 안붙여줘도 상관은없음)

이미지 캐시 구현

이 모든 점들을 고려해서 우리는 ImageCache 클래스를 선언할 수 있다. 이는 내부적으로 두 개의 NSCache 속성을 가지는데, 하나는 압축된 이미지를 저장하고, 다른 하나는 압축이 해제된 이미지를 저장한다. 우리는 객채의 최대 개수와 모든 메모리의 바이트 사이즈와 같은 총 코스트로 캐시 사이즈를 제한한다. NSLock 인스턴스가 상호 베타적인 접근을 제공하고 캐시를 스레드-세이프 하게 만든다.

이제 우리는 위에 정의한 IamgeCacheType 의 요구 조건을 만족하는 여러 메서드를 구현해야한다. 다음은 캐시로부터 이미지를 삽입하고 지우는 방법이다.

 

 

당신은 디코딩한 이미지를 위해 비용을 설정하고 있다는 것을 주의해야한다. 바로, decodedImageCachetotalCostLimit 로 구성되어 있다. 이는 총 코스트가 허용된 최대 코스트를 초과하였을 때 몇몇 요소를 제거해야한다. 이미지를 캐시로 부터 가져오기 위해서, 디코딩 된 이미지가 최선의 시나리오인지 확인해야한다. (이 단락은 무슨 말을 하는지 잘 이해가 되지 않네요.) 다음은 이미지 캐시에 있는 이미지를 검색하고 실패하면 nil 을 반환하는 fallback 이다.

 

 

ImageCache 에 대한 서브스크립트를 규정해야한다.

위와 같이, Image Cache 를 구축했고, 이는 당신의 프로젝트에서 재사용되고 앱을 더욱 빠르고 반응성이 좋아지도록 할 것이다.

Image Loader 와 통합

어떻게 ImageCache 를 당신의 프로젝트에 통합시킬수 있을지 확인해보자. 당신이 Image Loader 를 이미 가졌다고 가정해보자. 아니라면, 다음과 같이 Combine framework 를 사용해 될 수 있을 것이다.

 

 

  1. dataTaskPublisher URL session data tasks 를 수행하고 난 결과를 전달하는 퍼블리셔를 생성한다.
    이것은 파이프 라인을 통해서 튜플을 반환한다. (data: Data, response: URLResponse)
  2. map 연산자는 옵셔널 UIImage 객체를 생성한다.
  3. 우리는 error 핸들링을 위해 catch 연산자를 사용할 것이다. 이는 업스트림 퍼블리셔를 Just(nil) 퍼블리셔로 대채한다.
  4. 백그라운드 큐에서 작업을 수행한다.
  5. 메인 큐에서 이미지를 받도록 전환합니다.
  6. eraseToAnyPublisher 는 연산자 체인에서 타입을 지우는 역할을 합니다. loadImage(from:) 메서드가 AnyPublisher<UIImage?, Never> 를 반환하도록 합니다. (disposedBag? 인가요?.. 아니면 우리가 사용한 특정 퍼블리셔뿐아니라 범용적으로 사용되는 퍼블리셔를 반환하도록 하는 작업같네요… RxSwift, Combine 전혀 모릅니다..하핫)

다음으로는 우리는 ImageLoader 가 만약 이미지를 가지고 있다면 데이터 로딩이 끝났을 때 이미지를 캐싱하고, 이미지를 즉시 반환하도록 몇가지 조정을 해야합니다. 결국, ImageLoader 는 다음과 같을 수 있습니다.

  1. 캐시된 이미지가 있다면 Just 퍼블리셔를 반환합니다.
  2. data 는 퍼블리셔로 사용 가능하게 receiveOutput 으로 넘겨집니다. 여기서 우리는 이미지를 데이터가 로딩되자마자 캐시할 수 있고, dataTaskPublisher 가 새로운 값을 방출합니다.

결론

ImageCache 와 함께, 앱에서의 이미지 로딩을 최적화하고 사용자 경험을 향상시킬 수 잇습니다. 결국, 캐시로부터 이미지를 로딩하는 것은 네트워크에서 이미지를 받아오는 것보다 항상 빠릅니다.

추가 댓글에서 유용한 부분

You can add, remove, and query items in the cache from different threads without having to lock the cache yourself.

  • 따라서, 별도로 NSLock 을 잠고 푸는 코드는 없어도 됩니다.. defer 키워드를 사용한 곳을 삭제해서 사용하시길 바랍니다.

출처

https://medium.com/@mshcheglov/reusable-image-cache-in-swift-9b90eb338e8d

소감

저작권 허락을 받고 싶었는데, Twitter 로만 접촉이 가능했습니다.
다만 트위터에서 러시아어를 사용하시길래 해킹 당한 줄 알고 굳이 접촉은 하지 않았습니다. 문제시 엉엉 울겠습니다. (삭제도 하고..)

Combine 사용법을 몰라서 적어놓습니다.

Comments