처음에는 reloadData()를 사용했다. 하지만 무한스크롤 진입 시 셀을 처음부터 다시 그리는 문제가 발생했다. 사용자가 열심히 스크롤해서 내려온 위치가 다시 맨 위로 돌아가버리는 것이다. 그래서 reloadSection을 시도해봤다. 섹션별로 업데이트하면 될 것 같았는데, 여전히 해당 섹션이 처음부터 다시 그려지는 동일한 문제가 있었다. ( 근데 생각해보면 너무 당연했던 걸지도..?)
이제 insertItems로 정확한 위치에 셀을 추가하는 방식을 써보자고 생각했다. 하지만 여기서 더 복잡한 문제가 나타났다.
핵심 문제가 된 것: CompositionalLayout에서 섹션별 무한스크롤 구현 시 발생하는 동시 업데이트 충돌이었다. ( 잠재적이긴 했다. 실제로 에러가 난것은 아니지만 에러가 날 가능성이 높아 보였다.)
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
// 여러 섹션에서 동시에 마지막 셀에 도달할 수 있음// → 동시 API 호출 → 동시 insertItems 실행 위험
}
동시업데이트로 인한 충돌 문제가 생길 가능성이 있었다.
insertItems를 실행하고 있는 도중에 또 insertItems가 실행될 수 있는 상황이 있다는 게 문제였다.
섹션별 무한 스크롤 구현을 위해서 willDisplay 메서드를 넣고 각 섹션의 switch문으로 처리를 했는데, 여러 섹션에서 동시 API 응답이 오면 동시 insertItems 실행 가능성이 있었다.
왜?
동시 실행 가능성이 있냐면 여러 섹션의 빠른 스크롤로 인해서 각 섹션이 동시 업데이트를 진행할 수 있다는 점 때문이었다.
(예기치 않은 동작)
그렇게 되면 내부상태 충돌이 발생하고 "Invalid number of items" 크래시가 나게 된다.
(레이아웃 정합성 문제, 이 정합성에 대한 것은 추후에 다루도록 하겠다.)
잠재적인 에러를 달고 있는 코드라고 생각했다.
현재 Combine 기반의 반응형으로 구독 처리를 해주는 각 변수를 구독하고 있는 방식이었는데,
왜 PassthroughSubject였냐면 정확한 타이밍에 대한 제어와 어느 섹션에 몇 개의 아이템이 어느 위치에 추가될지를 알려주기 위해서였다.
업데이트 타이밍을 중앙 집중식으로 관리하는 추가 계층을 만드는 느낌이었다.
결국 문제점은 동시에 그리려고 하는 상황이 발생할 수도 있다는 잠재적인 부분이 문제 정의였고, 이것을 해결하기 위해서 정확한 위치, 정확한 갯수, 어떤 섹션을 전달하고, 동시 업데이트를 막게 해주면 되는 문제였다고 생각했다.
UICollectionView의 동작원리에 대한 이해가 생겼다.
CollectionView가 완전히 명령형 방식이었다는 것이다. 그니까 어떤 데이터를 보고 그 데이터에 맞게 그리는게 아니라 정확히 몇개를 그리라고 요청을 하는 방식이었다는 거다.
// CollectionView: "몇 개 그려야 해?"
func numberOfItemsInSection(_ section: Int) -> Int {
return movies.count// "20개요"
}
// CollectionView: "3번째 위치에 뭘 그려야 해?"
func cellForItemAt(_ indexPath: IndexPath) -> UICollectionViewCell {
let movie = movies[indexPath.item]// 3번째 영화 데이터 전달// 셀 구성 후 반환
}
그니까 질의 응답 시스템이었다는 것이다.
지금까지 그냥 써오기만 했었는데 뭔가 "아하~ 이런거였구나"의 느낌이었다.
즉 새로운 영화가 들어오면 ViewModel이 indexPath를 계산해서 이 퍼블리셔로 내보내고, VC는 그것을 받아서 해당 위치에만 셀을 추가해야 하는 방식인 건데, 그말인즉슨 정확히 어디에 무엇을 할지 지정해야 하고 세밀한 제어와 최적화 가능하지만 복잡하고 실수하기 쉽다는 것이다.
그렇기 때문에 정확한 위치와 갯수, 섹션에 대한 구분점이 필요했던 것이었고 좋은 선택이었다.
PassthroughSubject의 역할
let insertedIndexPathsPublisher = PassthroughSubject<(MovieRequestType, [IndexPath]), Never>()
PassthroughSubject로 지금 업데이트 하라는 트리거 역할과 어떤 섹션에서 몇 번째 위치에 몇 개를 추가할지 전달하는 중간 계층 역할을 하는 것이다.
오는 순서대로 처리하고 다음 순서를 처리할 수 있도록 해주며 정확한 제어가 가능하겠다 싶었다.
viewModel.insertedIndexPathsPublisher
.receive(on: RunLoop.main)
.sink { [weak self] type, indexPaths in
self?.collectionView.insertItems(at: indexPaths)
}
.store(in: &cancellables)
insertedIndexPathsPublisher는 단순히 index를 넘겨주는 도구가 아닌것이고
비동기 페이징 구조에서 UI 성능과 사용자 경험을 동시에 보장하기 위한 핵심 중간 계층이다.
MVVM 구조를 유지하면서도 Combine으로 상태 흐름을 관리하고 UICollectionView + Compositional Layout에서도 안정적인 insert가 가능해지게끔 한 것이다.
그렇게 되면 ViewController는 그 위치에만 insertItems(at:) 호출하여 순차적으로 부분 UI 업데이트를 할 수 있게끔 한 것이다.
🐍 Poetry 설치 중 PermissionError? 나도 그랬다 (0) | 2025.06.13 |
---|---|
TMDB에서 생긴일 - 2 (0) | 2025.05.24 |
TMDB에서 생긴일 - 1 (0) | 2025.05.11 |