250x250
반응형
Notice
Recent Posts
Recent Comments
Link
관리 메뉴

스윞한 개발자

이미지 렌더링 + 캐싱/UIGraphicsImageRenderer 본문

Swift 이론

이미지 렌더링 + 캐싱/UIGraphicsImageRenderer

스윞남 2025. 1. 12. 18:56
728x90
반응형
SMALL

안녕하세요! 

 

요즘, 캐싱에 대해 많은 글을 쓰고 있는 것 같습니다..!

 

캐싱의 한 부분을 공부하다 보니 관련된 여러 주제로 뻗어나가는 것 같아요. 그래서 이번 포스팅은..! 이미지 캐시와 이미지 로더를 통합해 개선하는 과정과 개념에 대해 정리해 보려고 합니다.

 

 

 

어플이 이미지를 다운로드하는 경우, 일반적으로 이미지를 로딩하는데 시간이 걸립니다. 앱이 동일한 이미지를 여러 번 다시 로드할 때 불필요한 작업이 될 수 있고 사용자는 불편함을 느낄 수 있습니다. 테이블뷰나 컬렉션뷰를 가지고 있는 어플이 대부분일 것입니다. 앱을 실행하고 스크롤할 때 거의 대부분의 사용자가 버벅거리는 환경을 경험해 보았을 것입니다.

-> 이러한 이슈는 이미지 렌더링이 한 번에 이루어지지 않아서 생긴다고 합니다. 

 

LazyStack이나, 테이블, 컬렉션 뷰는 화면에 이미지가 보일 때 이미지 렌더링을 실행합니다. 이 과정에서 버벅거리는 현상을 줄이기 위해 많은 양의 데이터를 한 번에 렌더링 한다면, 메인 스레드에서는 방대한 양의 일을 하게 될 것입니다.

# 이미지 렌더링 파이프라인

이미지 렌더링은 화면에 이미지를 보여주기 위해 데이터를 처리하고 화면에 표시하는 과정입니다. 

* 로딩 : 압축된 이미지를 메모리에 로드하는 과정(로컬 디스크에 이미지를 읽는 과정)

* 디코딩 : 인코딩 된 이미지 데이터를 픽셀당 이미 정보로 변환하는 과정

 

먼저 앞 포스팅에서 정리한 캐시 기능을 이용해 네트워크 요청을 감소시킬 수 있습니다. 그리고 이미지 유효성을 검증하기 위해 백그라운드에서 처리할 수 있습니다. 

 

렌더링 하는 과정에 대해 부담을 줄이기 위해 생각해 보았는데, 로딩과 디코딩하는 과정에 대해 접근해 볼 수 있겠다 생각했습니다. 

if let decodedImage = decodedImageCache.object(forKey: url as AnyObject) as? UIImage {
    return decodedImage
}

 

처음 이미지 캐시에 접근할 때, 디코딩이 이미 완료된 이미지가 캐시에 있다면 바로 반환을 시키고

if let image = imageCache.object(forKey: url as AnyObject) as? UIImage {
    let decodedImage = image.decodedImage()
    decodedImageCache.setObject(image as AnyObject, forKey: url as AnyObject, cost: decodedImage.diskSize)
    return decodedImage
}

 

 

그렇지 않다면 즉, 원본 이미지가 캐시에 있다면! 디코딩된 이미지를 새로운 캐시에 저장해 주며, 디코딩된 이미지를 반환해 줄 수 있습니다. 

 

 

캐시에 저장된 이미지를 로딩할 때도 마찬가지로 이미지 로딩 메서드를 커스텀해서 과정을 최적화할 수 있습니다. 마찬가지로 캐시에 저장되어 있는 유무를 판단하고, 백그라운드에서 네트워크와의 통신을 통해 유효성을 검증할 수 있습니다. 

 

if let image = cache[url] {
            return Just(image).eraseToAnyPublisher() ➊
        }

 

무거운 작업을 백그라운드에서 처리하고 메인스레드에서는 UI 업데이트 과정만을 수행하기 위해 커스텀할 수 있습니다.

 

URLSession.shared.dataTaskPublisher(for: url)
            .map { (data, response) -> UIImage? in return UIImage(data: data) }
            .catch { error in return Just(nil) }
            .handleEvents(receiveOutput: {[unowned self] image in ➋
                guard let image = image else { return }
                self.cache[url] = image
            })
            .subscribe(on: backgroundQueue)
            .receive(on: RunLoop.main)

 

WWDC18의 iOS Memory Deep Dive를 보다 UIGraphicsImageRenderer부분을 살펴보게 되었는데, 기존에 사용했던 과정보다 더 적절한 해상도로 이미지를 생성해 주기 때문에 더 효율적으로 사용될 수 있습니다. 

 

https://developer.apple.com/documentation/uikit/uigraphicsimagerenderer

 

UIGraphicsImageRenderer | Apple Developer Documentation

A graphics renderer for creating Core Graphics-backed images.

developer.apple.com

 

 

 

extension UIImage {
    func decodedImageUsingRenderer() -> UIImage? {
        let size = self.size
        let renderer = UIGraphicsImageRenderer(size: size)
        return renderer.image { _ in
            self.draw(in: CGRect(origin: .zero, size: size))
        }
    }
}

 

UIGraphicsImageRenderer를 사용하면 위 과정에서 디코딩되는 과정과 다르게 원하는 설정한 크기로 줄이고 디코딩 된 후, 렌더링 할 수 있게 도와줍니다. 

# 결론!

이미지 렌더링하는 과정을 Lazy 하게 처리하는 것이 제일 효율적인 과정이기에, 이 부분을 커스텀하는 것보다! 이미지 렌더링을 하는 자체 과정 중에서의 조금이나마 효율적인 방법이 없을지를 생각해 볼 수 있습니다.

 

- Lazy 한 이미지 렌더링

화면에 표시될 때 이미지를 로드하고 디코딩하는 방식입니다. 필요하지 않은 이미지는 로드하지 않기 때문에 메모리 사용량이 감소합니다. 총 100개의 이미지에 대해 초기 화면에 보이는 10개의 이미지만 보이기 때문에 렌더링 속도가 빨라져 버벅거림이나, 시간이 단축하게 됨으로 사용자가 더 편해질 수 있습니다.

 

- 로딩/디코딩 과정의 정제

이미지 렌더링을 lazy 하게 처리하더라도, 이미지 로딩과 디코딩 과정을 최적화하게 된다면 렌더링의 단점을 조금이나마 완화할 수 있습니다. 무거운 작업을 백그라운드에서 처리하고, 디코딩된 이미지를 캐시에 저장시키면 이후 렌더링된 속도가 빨라지게 됩니다. 

 

lazy + 로딩/디코딩 과정의 정제를 생각해 보다.. 프리로딩이라는 방법을 알게 되었습니다! 또 두 가지 방법을 사용하되, 사용자가 스크롤하여 볼 가능성이 높은 이미지를 미리 로드해 두면 이 과정에서 렌더링 속도를 좀 더 개선할 수 있지 않을까 생각합니다.

 

이번 포스팅은 해외 자료들을 살펴보며 정리했는데, 어려운 내용이 많이 있었던 터라..! 혹시 글을 읽으시며, 잘못된 점이 있거나 보완사항이 필요하다 생각되신다면..! 피드백은 언제나 환영입니다! 긴 글 읽어주셔서 감사합니다.

 

728x90
반응형
LIST