https://developer.apple.com/videos/play/wwdc2021/10252
Make blazing fast lists and collection views - WWDC21 - Videos - Apple Developer
Build consistently smooth scrolling list and collection views: Explore the lifecycle of a cell and learn how to apply that knowledge to...
developer.apple.com
CollectionView에서 스크롤을 부드럽게 하기 위해서는
최적화를 어떻게 해야할까요?

여러 사진과 텍스트 라벨이 포함된 스크롤 가능한 CollectionView에서
성능을 최적화하는 방법에 대해 알아봅시다.
Performance fundamentals
먼저, 데모 앱의 데이터가 어떻게 구성되는지 살펴보겠습니다.

데모 앱에서는 post list를 가져오는데,
이 각 post들은 DestinationPost 구조체로 표현되고 있습니다.
여기에서 해당 구조체가 Identifiable 프로토콜을 따르고 있다는 걸
확인할 수 있는데요.
때문에 id 속성을 통해 각 게시물을 식별할 수 있고,
Diffuable Data Source에는 이 모델의 객체가 아닌,
객체를 고유하게 식별할 수 있는 ID만 저장하게 됩니다.
다시 말해, DestinationPost 자체를 저장하는 것이 아닌
DestinationPost의 ID 값만 추가하는 것이죠.

위 사진처럼 DestinationPost의 ID 값만 저장합니다.
Diffuable Data Source에 데이터를 채우는 과정은 다음과 같은데요.
1. 새로운 스냅샷(Snapshot) 생성
2. 메인 섹션을 추가
3. 백엔드에서 게시물 리스트를 가져옴
4. 각 게시물의 ID를 추가
5. 최종적으로 스냅샷을 적용(apply)
각 post의 다른 속성이 변경되더라도 ID는 변하지 않기 때문에,
UI 업데이트가 안정적입니다.

iOS 15 이전까지는 snapshot을 애니메이션 없이 적용할 경우,
내부적으로 reloadData()가 호출되었는데요.
reloadData()는 존재하던 모든 셀을 폐기하고 새롭게 생성하기 때문에
성능이 좋지 않습니다.
그러나 iOS 15부터, 기존에 존재한 값과 새로운 값을 비교하여
difference한 부분만 적용하기 때문에
불필요한 렌더링을 하지 않게 되었습니다.
또, reconfigureItems()를 통해 보여지는 셀들만
효율적으로 업데이트 할 수 있습니다.
이제 값들을 UI 화면에 띄워봅시다.
Cell Registration
셀 등록(cell registration)은 각각의 셀 타입 설정을
한 곳에서 관리할 수 있도록 하는 과정인데요.
Diffuable Data Source가 각 여기에 등록된 셀들의 ID에
쉽게 접근할 수 있습니다.

이 때, 셀 등록을 할 때 한 번만 생성해야 하는 것을 잊으면 안됩니다.
그래서 cell Provider 내부가 아니라 외부에서 생성해주어야 합니다.
data Source의 cell Provider 내에서 생성하게 되면
콜렉션 뷰가 셀을 재사용하지 못하기 때문에 성능 저하가 발생하기 때문이죠.
다시 말해,
dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, item in
let registration = UICollectionView.CellRegistration<DestinationPostCell, DestinationPost.ID> { cell, indexPath, postID in
...
}
return collectionView.dequeueConfiguredReusableCell(using: registration, for: indexPath, item: postID)
}
위처럼 내부에서 셀을 생성하면 스크롤 할 때마다 셀이 생성됩니다.
아래와 같이 cell을 한 번 생성해놓고,
재사용해야 합니다.

Cell Lifecycle
그렇다면 Cell은 언제 configure되는 것일까요?
UICollectionView의 셀 생명 주기는 두 가지 단계로 구성됩니다.

1. 준비 (Preparation)
- 셀이 재사용 큐에서 꺼내어짐
- prepareForReuse()가 호출됨 -> new cell로 init
- 셀이 설정(Configuration)됨
- Sizing and Layout

2. 준비 (Preparation)
- willDisplayCell 호출 후 화면에 표시됨
- 사용자가 스크롤하면서 셀이 화면에서 사라지면, didEndDisplayingCell이 호출됨
- 다시 Reuse Pool로 들어감
구조를 이해했으니,
이제 실제로 앱을 실행해보며 스크롤 성능을 확인해보겠습니다.
스크롤 시 발생하는 Hitch (끊김)
앱을 실행하다보면,
스크롤이 매끄럽지 않고 중간에 끊기는 hitching 문제가 발생합니다.
hitching이 발생하는 이유는 무엇일까요?

각 프레임, 즉 화면에 보여지는 장면마다
앱은 뷰의 상태를 업데이트 해야합니다.
예를 들어, 스크롤을 하기 위해 사용자가 화면을 터치합니다.
이 때 현재 스크롤 좌표값인 contentOffset이 변경되는데요,
이로 인해서 뷰의 위치가 다시 계산됩니다.
각 프레임마다는 commit deadline이 존재합니다.
이는 모든 뷰 업데이트가 지정한 시간 안에 완료가 되어야 함을 의미하는데요,
iPad Pro와 같이 120Hz의 디바이스에서는
이 시간이 더 짧습니다.
(iPhone은 60Hz)

스크롤에 따라 화면에 새로운 셀이 나타나면,
해당 셀을 설정하는 시간이 오래 소요됩니다.
그리고 이후 프레임에서는 단순 기존 셀의 위치만 변경하기 때문에
시간 소요가 짧게 걸립니다.
또, 사용자가 스크롤을 계속할 경우
새롭게 셀이 설정되면서 오래 시간이 소요됩니다.
길고 짧고 짧고 .. 이 패턴이 계속해 반복되는 양상이 보여지는데요.

이 때, 새로운 셀이 설정되는 시간이 너무 오래걸릴 경우
deadline을 지키지 못하게 됩니다.
이렇게 될 경우 프레임을 놓쳐,
hitch가 발생하게 됩니다.
Cell prefetching (사전 로딩)
iOS 15부터는 이러한 hitch 현상을 해결하기 위해
필요한 셀을 미리 로딩하는 Cell prefetching 기능이 도입되었는데요.

필요할 때마다 새로운 셀을 생성하는 기존 방식과는 다르게,
미리 셀을 생성해두어 스크롤을 부드럽게 만듭니다.
Prefetching은 정확히 어떻게 동작하나요?

한 프레임 안에서 짧게 commit이 끝난 경우,
deadline 전까지 많은 시간이 남게 됩니다.
이 때 다음 프레임이 올 때까지 기다리는 것이 아니라
이 시간을 활용해서 미리 셀을 생성하는 방식입니다.

다음 commit의 경우, prefetch가 끝난 이후에 시행되어도 되는 이유는
시간 소요가 적기 때문에 deadline을 초과하지 않기 때문입니다.


(속도가 두 배나 빨라진대요~)

Cell prefetcing은 iOS 15로 빌드만 해도 적용됩니다.
이는 스크롤 성능 뿐만 아니라,
프레임을 놓치는 경우가 줄어들어 CPU 부하가 줄어들 뿐더러
더 적은 에너지를 사용하기 때문에 배터리 지속 시간이 증가합니다.
Prefetching이 적용된 Cell Lifecycle
prefetching이 적용되면서 셀의 Lifecycle에도 변화가 생겼는데요.

기존에는 셀이 화면에서 사라지면 즉시 Reuse Pool로 들어갔지만,
이제는 Waiting State가 추가되었습니다.
이 경우 미리 생성된 셀이 바로 사용되지 않을수도 있게 되는데요.
사용자가 스크롤 화면을 바꾸면, 미리 준비된 셀이 사용되지 않고 폐기될 수 있기에
무거운 연산이 대기 상태에서도 실행될 수 있으므로
가능한 가벼운 연산만을 수행해야 합니다.
Updating cell content: 비동기적으로
이제 비동기적으로 데이터를 업데이트하는 방법을 알아볼텐데요.
특히, 이미지 로딩 시 발생하는 hitch를 어떻게 해결할 수 있을까요?

문제는 서버에서 이미지를 다운로드 하는 경우,
셀이 먼저 화면에 표시되고 나중에 이미지가 로딩되는 것인데요.

configuration handler를 살펴보면
등록된 assetsStore에서 항상 이미지를 fetch 받아오도록 설정되어 있는데요.
이미지를 항상 반환하고 있지만 (아직 다운로드 받지 않아)
이미지가 없을 수도 있겠습니다.

따라서 만약 assetsStore 객체의 isPlaceholder 값이 true라면,
서버에 요청해서 이미지를 받아와야 합니다.
만약 이미지를 다운로드 받아오면, 셀의 imageView를 업데이트 할텐데요.
위와 같은 코드는 잘못된 방식입니다.
왜냐하면 셀이 여러 다른 게시물에서 재사용 될 수 있기 때문에,
이미지를 다운로드 받는 동안
이미 다른 이미지를 설정할 수도 있기 때문입니다.
다시 말해,
셀의 이미지를 바로 업데이트하면
잘못된 이미지가 보여질 수도 있다는 것입니다.

그래서, iOS 15부터는 새롭게 reconfigureItems API가 도입되었습니다.
기존 셀을 폐기하지 않고도, 데이터만 갱신할 수 있는 것인데요.

이렇게 하면 비동기적으로 이미지가 로딩되었을 때,
기존 셀을 유지한 채 UI만 업데이트 할 수 있게 됩니다.
reconfigureItems는 reloadItems와 다르게
기존 셀을 유지하기 때문에, 성능이 훨씬 좋습니다.

만약 준비 시간을 최대한 확보하려면,
downloadAsset 메서드를 prefetchingDataSource 내부에서 사용할 수도 있습니다.
이렇게 하면,
셀이 화면에 나타날 때 이미지가 미리 준비되어 있기 때문에
사용자가 placeholder를 보는 시간이 줄어들게 됩니다.
이렇게 했는데도 새로운 이미지가 불러와질 때마다
여전히 hitching이 발생한다구요?
Image Preparation API

새로운 셀이 준비될 때는 끊김이 없지만,
이미지가 고해상도로 업데이트 될 때 hitching이 발생하는데요.

그 이유는 모든 이미지가 화면에 표시되기 전에 Decoding을 거쳐야 하기 때문입니다.
고해상도일수록 Decoding 시간이 오래걸리겠죠?
때문에 이 디코딩 시간이 오래 걸리면,
또 deadline을 맞추지 못하고 hitch가 발생하게 됩니다.

이미지는 png, jpeg, heic 등 다양한 압축 포맷을 가지고 있는데요.
이를 화면에 표시하기 위해서는,
bitmap 데이터로 변환(decoding)하는 과정이 필요합니다.
이 변환 과정이 너무 오래 걸리면,
메인 스레드가 block되면서 hitch가 발생하는 것인데요.
어떻게 해결해야 할까요?
이상적으로는,
이미지를 미리 준비해두고 UI 업데이트 시 바로 적용하는 것입니다.

다시 말해, 이미지 디코딩을 백그라운드에서 수행해
메인 스레드의 부담을 줄이는 것입니다.

Image Preparation API를 사용하면
UI 업데이트 전에 미리 이미지를 변환해서 스크롤을 부드럽게 유지할 수 있는데요.
이 API는 동기, 비동기 두 가지 방식으로 나뉩니다.
동기는 모든 스레드에서 사용이 가능하고,
즉시 결과가 나오는 반면
비동기는 UIKit 내의 Serial Queue에서 실행되어,
준비가 완료되면 UI 업데이트가 가능합니다.

(위처럼 활용)

그러나 preparedImage는 순수한 픽셀 데이터,
즉 원본 이미지이기 때문에
너무 많은 이미지를 캐싱하면 메모리 사용량이 급격히 늘어납니다.
(Disk에 저장하기에는 부적합)

image preperation을 효과적으로 활용하려면,
prefetching과 함께 사용하는 것이 좋습니다.
이미지 다운로드와 디코딩을 모두
미리 끝내버리는 것이지요!

demo app에서는
이미지를 다운로드 한 후,
preparation을 수행한 뒤 화면에 업데이트하는데요.
에셋들은 크기가 크지만 중요한 자원이기 때문에 캐싱할 수도 있습니다.
이 때, 캐시에 너무 많은 데이터가 저장되지 않도록
이미지 크기를 기반으로 메모리 사용량을 추정해서 관리하는 것이 좋습니다.
새로운 셀이 이미지 요청을 하면 캐시에서 찾아보고,
없을 경우 서버에 요청합니다.

이미지가 너무 클 경우, Thumbnail API가 있습니다.

이 API를 사용하면 이미지를 작은 크기로 변환하여 최적화해주는데요.
미리 최적화된 이미지 크기를 사용해서 메모리 사용량을 줄일 수 있습니다.
'WWDC' 카테고리의 다른 글
[WWDC] Design protocol interfaces in Swift (0) | 2025.02.14 |
---|---|
[WWDC] Embrace Swift generics (1) | 2025.02.08 |
[WWDC] Advances in Collection View Layout (2) | 2025.02.04 |
[WWDC] Advances in UI Data Sources (0) | 2025.02.03 |
[WWDC] ARC in Swift: Basic and beyond (0) | 2024.12.20 |