과제를 하다가 강의에서 못 본 부분을 발견하여 작성해보는 무한스크롤 구현방법
무한스크롤은 사용자가 스크롤을 내릴 때마다 새로운 데이터를 비동기적으로 불러와 리스트를 확장하는 방식이다.
RxSwift를 사용하고 있었기 때문에 RxSwift의 flatMap
과 zip
을 활용했고, MainViewController의 UICollectionViewDelegate 안의 메서드를 활용해 처리한다.
기존에 작성했던 MainViewModel의fetchPokemon()
메서드를 수정해야 했다.
중복 요청 방지: isFetching
플래그를 사용해 현재 데이터 요청이 진행 중인지 확인하고, 이미 요청 중이라면 함수를 종료해 중복 요청을 방지한다. 이걸 처리를 안했더니 네트워크 요청과 메모리에 난리부르스가 났다.
API 요청 및 데이터 처리: zip
을 사용해 20개씩 묶어서 데이터를 불러온다.
데이터 병합 및 UI 업데이트: 모든 데이터가 로드된 후, 기존 데이터에 새로 가져온 데이터를 병합하고 이를 pokemonSubject
에 방출하여 UI가 자동으로 업데이트되도록 한다.
NetworkManager.shared.fetch(url: url)
.flatMap { [weak self] (pokemonResponse: PokemonResponse) -> Single<[PokemonDetail]> in
guard let self else { return Single.just([]) }
let pokemonDetail = pokemonResponse.results.compactMap { self.fetchPokemonDetail(from:$0.url).asSingle() }
return Single.zip(pokemonDetail)
}
.observe(on: MainScheduler.instance)
.subscribe(onSuccess: { [weak self] details in
guard let self else { return }
var currentDetails = try? self.pokemonSubject.value()
currentDetails?.append(contentsOf: details)
self.pokemonSubject.onNext(currentDetails ?? [])
self.offset += self.limit
self.isFetching = false
}, onFailure: { error in
self.pokemonSubject.onError(error)
self.isFetching = false
}).disposed(by: self.disposeBag)
앞에서 공부했던 것처럼, subject를 사용하면 외부에서 데이터를 계속 가져와 추가할 수 있다.
zip
을 활용한 이유zip
을 처음 써 봐서 이게 왜 들어가야 하지? 싶었는데,
여러개의 Single
요청을 묶어서 Single[T]
로 처리하기 위함이라고 한다.
Single
과 flatMap
을 엮어서 하나씩 처리할 수도 있긴 한데
다음과 같은 이유로 묶어서 처리하는게 좋다고 한다.
flatMap
: flatMap
을 사용하면 각 포켓몬의 상세 데이터를 개별적으로 처리하고, 로드된 데이터가 있을 때마다 UI에 반영할 수 있다. 그러나 이 방법은 모든 데이터를 한꺼번에 처리하는 데 비해 UI가 자주 갱신되며, 데이터가 모두 로드되기 전까지 UI가 부분적으로 업데이트될 수 있다.zip
: 반면, zip
은 모든 데이터를 한꺼번에 처리하여 UI가 한 번에 업데이트되도록 하므로, 데이터 로드 중에도 UI가 일관되게 유지될 수 있다. 이는 사용자 경험을 더욱 안정적으로 만들 수 있는 중요한 요소다.zip
에 대해 추가로 알게 된 내용:이미지가 20개씩 받아와지긴 하는데, 순서가 랜덤으로 나오는 문제를 겪으셨다고 한다.
코드를 보니 .zip을 안 쓰고 있어서, 여기서 순차적으로 처리하는 기능도 있는건가? 했는데
찾아보니까 맞는 것 깉아서 추가로 작성
순서가 랜덤하게 나오는 것은 비동기 작업의 특성 때문인데, 비동기 작업은 각 작업이 독립적으로 수행되며 작업의 완료 시간은 네트워크 상태나 작업의 복잡성에 따라 달라질 수 있다. 예를 들어, 여러 이미지 URL을 비동기적으로 다운로드하는 경우, 첫 번째 URL이 가장 늦게 완료되고 마지막 URL이 가장 먼저 완료될 수도 있다.
flatMap
이나 merge
같은 연산자를 사용하면, 작업이 완료된 순서에 따라 결과가 방출된다. 이로 인해, 이후 결과를 처리할 때 순서가 뒤바뀔 수 있다. 예를 들어, flatMap
을 사용해 여러 비동기 작업을 실행하면, 완료된 순서대로 결과가 처리되므로 UI를 업데이트하거나 데이터를 추가할 때 순서가 섞일 수 있다.
zip
은 여러 비동기 작업을 병렬로 실행하면서 각 작업의 결과를 순서대로 결합하여 반환한다. 예를 들어, 20개의 이미지를 비동기적으로 다운로드할 때, 각 다운로드가 완료된 후 zip은 다운로드된 이미지를 배열에 담아 원래 지정한 순서대로 제공한다. UICollectionViewDelegate
에 scrollViewDidScroll
이라는 메서드가 있었다.
스크롤을 감지할 때마다 안의 코드를 실행해주는 메서드인데,
조건을 설정하지 않으면 스크롤이 조금만 움직여도 바로 실행되니 다음과 같이 조건을 설정했다.
extension MainViewController: UICollectionViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offsetY = scrollView.contentOffset.y
let contentHeight = scrollView.contentSize.height
let frameHeight = scrollView.frame.size.height
if offsetY > contentHeight - frameHeight - 200 {
viewModel.fetchPokemon()
}
}
}
offsetY
: 현재 스크롤 위치contentHeight
: 전체 스크롤뷰 HeightframeHeight
: 한 화면에 보이는 스크롤뷰 영역 Height설정해두긴 했는데 데이터가 20개씩밖에 안 불러와지니 한 화면에 거의다 보여서... 무한 스크롤 느낌은 안나지만 기다리면 잘 나타난다. 그럼 이만.