iOS/Refactoring

[UIKit/Refactoring] 홈 화면 UX 개선하기 (리사이징, 메모리 캐싱)

suee97 2024. 7. 27. 00:42

- 문제 상황

홈 화면에서 스크롤 시, 앱이 버벅인다는(렉이 걸린다는) 이슈를 제보받았습니다.

실제로 구동을 해보니, 앱이 버벅이고 있었고 메모리 사용량도 심상치 않았습니다.

이 문제는 저도 알고 있는 문제였습니다. 하지만, 매번 시뮬에서 테스트하다보니 "시뮬이라서" 렉이 걸리는구나라고 생각하고 있었습니다.

 

 

앱의 홈 화면은 이렇게 생겼습니다.

 

- 메모리 디버그

순환참조로 인한 메모리 누수 등 메모리로 인한 문제가 아닐까 생각해 메모리 디버그를 확인했습니다.

 

* 아래 문제 상황 설명은 iPhoneXS 디바이스 기준입니다.

 

앱을 처음 진입한 이후의 메모리 사용입니다.

 

 

 

스크롤을 딱 한번 했을 때의 메모리 사용입니다.

이미지 하나의 크기가 5~10MB인걸 감안했을 때, 뭔가.. 뭔가 비정상적이라고 느꼈습니다.

 

 

위 화면은, 화면 끝까지 스크롤 했을 때의 메모리 사용입니다.

 

 

 

스크롤을 계~~속 하다보면, 메모리가 1.8GB까지 올라갔다가 다시 떨어졌다가(아마 시스템에 의해 조절되는 것 같습니다) 다시 올라갔다가 떨어졌다가 반복합니다. 이후 이렇게 앱이 터져버립니다.

 

요약하면, 메모리 사용량을 줄이고, 메모리 초과 시 앱이 터지기 전에 조치를 취하고, 결과적으로 유저의 사용성을 개선하는 것이 과제입니다.

 

- 해결

1. 이미지 용량 줄이기 (정책)

가장 쉬운 방법입니다.(클라입장에서)

서버에서 썸네일 버전, 원본을 가지고 있다가, 홈 화면에서는 썸네일 버전을 내려주는 것입니다.

기존 받아온 이미지는 1~10MB 였는데, 이는 절대로 작은 용량이 아닙니다.

따라서, 이미지 관리 관련 정책을 개선해서 이 문제를 어느정도 해결할 수 있을 것입니다.

예를 들어, 클라이언트에서 작품을 등록할 때, 압축버전/원본버전 두 가지를 보내거나, 서버에서 이미지 압축 관련 레이어를 하나 더 두는 방식으로 해결할 수 있습니다.

 

 

2. 리사이징

이미지를 받아온 이후 리사이징 하는 방식입니다. 서버에서 용량이 큰 이미지를 받아오는 것은 여전하겠지만, 적어도 메모리 사용량을 줄이는 데에는 큰 도움이 될 것입니다.

KingfisherManager.shared.retrieveImage(with: url!, completionHandler: { [weak self] result in
    switch result {
    case .success(let value):
        let image = value.image.resize(newWidth: UIScreen.main.bounds.width)
        self?.imageView.image = image
    case .failure(let error):
        print(error)
    }
})

 

extension UIImage {
    func resize(newWidth: CGFloat) -> UIImage {
        let scale = newWidth / self.size.width
        let newHeight = self.size.height * scale

        let size = CGSize(width: newWidth, height: newHeight)
        let render = UIGraphicsImageRenderer(size: size)
        let renderImage = render.image { context in
            self.draw(in: CGRect(origin: .zero, size: size))
        }
        return renderImage
    }
}

// 출처: https://nsios.tistory.com/154

 

 

좌: 홈 화면 접근 직후, 우: 스크롤 5회 이상

 

메모리 사용량이 확연히 줄어들었습니다.

하지만, 버벅이는 문제는 여전히 있었습니다.

리사이징을 메인 스레드에서 수행해서 그런 것인지 (이건 어쩔 수 없는 것 같고 ..),

셀을 보여줄 때마다(이미 봤던 셀 포함) 리사이징을 중복 수행해서 그런 것인지,

KingFisher가 캐싱을 하지만 애초에 용량이 커서 불러올 때 드는 비용이 커서 그런건지,

메모리 사용량을 줄었지만 버벅이는 문제는 해결되지 않았습니다.

* KingFisher 라이브러리를 사용했기 때문에, 원본 이미지에 대한 별도의 캐싱을 진행하지는 않았습니다.

 

 

3. 리사이징 한 이미지를 캐싱하기

KingFisher를 사용해서 원본 이미지를 캐싱하긴 하지만, 캐싱이 되었다 하더라도 그 이미지가 너무 커서 디스크나 메모리에서 왔다갔다 하는 이유로 버벅이는 것이 아닌가 하는 생각이 들었습니다.

따라서, 원본 이미지를 리사이징하고 그 이미지를 메모리에 캐싱해보겠습니다.

let cache = NSCache<NSString, UIImage>()

func update(with item: String) {
    let url = URL(string: item)

    // MARK: 메모리 캐시에서 이미지 불러오기 + 있으면 해당 이미지 사용
    if let cachedImage = cache.object(forKey: item as NSString) {
        print("load from memory cache : \(item)")
        self.imageView.image = cachedImage
        return
    }

    KingfisherManager.shared.retrieveImage(with: url!, completionHandler: { [weak self] result in
        switch result {
        case .success(let value):

            let image = value.image.resize(newWidth: UIScreen.main.bounds.width)

			// MARK: 메모리 캐시에 이미지 저장
            cache.setObject(image, forKey: NSString(string: item))
            self?.imageView.image = image
        case .failure(let error):
            print(error)
        }
    })
}

 

이렇게 하면 처음 스크롤 할 때에는 메모리 캐시에 이미지가 없기 때문에 버벅이는건 여전하지만, 스크롤을 다시 올릴 때(봤던 이미지를 다시 보여줄 때) resized된 이미지를 보여줄 수 있습니다.

 

 

왼쪽: resize만 적용 + 스크롤 올릴 때

오른쪽: resize 적용 + 메모리 캐싱 + 스크롤 올릴 때

확실히 resized된 이미지를 캐싱했을 때 더 부드럽게 동작합니다. 

 

여기서, 디스크 캐시에도 이미지를 보관한다면, 버벅이는 문제를 개선할 수 있다는 확신이 들었습니다.

 

 

4. 디스크 캐시

resized된 이미지를 디스크에도 캐싱을 한다면 이미지를 처음 볼 때를 제외하고는 빠른 이미지 렌더링이 가능할거라 생각합니다.

메모리 캐시보다 접근 시간이 길기 때문에 메모리 캐시를 먼저 확인하는 방식으로 구현해야 합니다.

 

 

5. Kingfisher (+24.07.31)

검색을 하던 와중, 이런 글을 보게되었습니다.

https://stackoverflow.com/questions/54082478/main-thread-blocking-on-scroll-and-kingfisher-setting-the-image

 

Main thread blocking on scroll and Kingfisher setting the image

I've been running down slow scroll performance and I've noticed that when I scroll and setImage gets called with non-cached images, the performance lags while the download happens. if let imageURL...

stackoverflow.com

요약하면, 이미지가 이미지 뷰보다 더 큰 경우, 리사이징 하지 않으면 iOS에서 이미지를 조작하느라 버벅거림이 발생한다는 것입니다.

관련해서 Kingfisher에서 옵션을 제공하고 있었습니다.

imageView.kf.setImage(
    with: url,
    options: [
        .processor(DownsamplingImageProcessor(size: CGSize(width: itemWidth, height: itemHeight))),
        .scaleFactor(UIScreen.main.scale),
        .cacheOriginalImage
    ]
)

 

디스크 캐시에 리사이즈 된 이미지와 원본 이미지를 모두 저장하는 것 같습니다.

 

결론적으로는 이런 방식으로 해결하게 되었습니다 ㅎㅎ ...

 

 

 

 

- 마무리 (+24.07.31)

글에선 다루지 않았지만, 메모리 사용 그래프, 메모리 프로파일링, allocation 등 다양한 도구를 배워볼 기회가 되었습니다.

그리고 결국에는(?) Kingfisher로 해결하게 되었지만, 나중에 라이브러리 의존성 문제를 해결하거나 옵션을 변경하는 과정에서 제가 시도한 것들이 도움이 되지 않을까 생각합니다.