- 문제 상황
홈 화면에서 스크롤 시, 앱이 버벅인다는(렉이 걸린다는) 이슈를 제보받았습니다.
실제로 구동을 해보니, 앱이 버벅이고 있었고 메모리 사용량도 심상치 않았습니다.
이 문제는 저도 알고 있는 문제였습니다. 하지만, 매번 시뮬에서 테스트하다보니 "시뮬이라서" 렉이 걸리는구나라고 생각하고 있었습니다.

앱의 홈 화면은 이렇게 생겼습니다.
- 메모리 디버그
순환참조로 인한 메모리 누수 등 메모리로 인한 문제가 아닐까 생각해 메모리 디버그를 확인했습니다.
* 아래 문제 상황 설명은 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)
검색을 하던 와중, 이런 글을 보게되었습니다.
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로 해결하게 되었지만, 나중에 라이브러리 의존성 문제를 해결하거나 옵션을 변경하는 과정에서 제가 시도한 것들이 도움이 되지 않을까 생각합니다.
'iOS > Refactoring' 카테고리의 다른 글
| [UIKit/Refactoring] MyPageViewModel getAllItems() 함수 개선하기 (0) | 2024.08.10 |
|---|---|
| [UIKit/Refactoring] Domain + Presentation + MVVM 아키텍처 개선 (0) | 2024.08.09 |
| [UIKit/Refactoring] MyPageProfileEditViewModel 코드 개선하기 (2) (0) | 2024.07.17 |
| [UIKit/Refactoring] MyPageProfileEditViewModel 코드 개선하기 (1) (0) | 2024.07.13 |
| [UIKit/Refactoring] MyPageNotificationViewModel 코드 개선하기 (0) | 2024.07.09 |