[RxSwift][iOS] 무한스크롤 구현 (feat.RxSwift, scrollViewDidScroll) + flatmap, zip

팔랑이·2024년 8월 7일
0

iOS/Swift

목록 보기
57/71

RxSwift를 이용한 무한스크롤 구현

과제를 하다가 강의에서 못 본 부분을 발견하여 작성해보는 무한스크롤 구현방법
무한스크롤은 사용자가 스크롤을 내릴 때마다 새로운 데이터를 비동기적으로 불러와 리스트를 확장하는 방식이다.

RxSwift를 사용하고 있었기 때문에 RxSwift의 flatMapzip을 활용했고, MainViewController의 UICollectionViewDelegate 안의 메서드를 활용해 처리한다.


1. fetchData() 비즈니스 로직

기존에 작성했던 MainViewModel의fetchPokemon() 메서드를 수정해야 했다.

주요 흐름

  1. 중복 요청 방지: isFetching 플래그를 사용해 현재 데이터 요청이 진행 중인지 확인하고, 이미 요청 중이라면 함수를 종료해 중복 요청을 방지한다. 이걸 처리를 안했더니 네트워크 요청과 메모리에 난리부르스가 났다.

  2. API 요청 및 데이터 처리: zip을 사용해 20개씩 묶어서 데이터를 불러온다.

  3. 데이터 병합 및 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]로 처리하기 위함이라고 한다.
SingleflatMap을 엮어서 하나씩 처리할 수도 있긴 한데
다음과 같은 이유로 묶어서 처리하는게 좋다고 한다.

  • flatMap: flatMap을 사용하면 각 포켓몬의 상세 데이터를 개별적으로 처리하고, 로드된 데이터가 있을 때마다 UI에 반영할 수 있다. 그러나 이 방법은 모든 데이터를 한꺼번에 처리하는 데 비해 UI가 자주 갱신되며, 데이터가 모두 로드되기 전까지 UI가 부분적으로 업데이트될 수 있다.
  • zip: 반면, zip은 모든 데이터를 한꺼번에 처리하여 UI가 한 번에 업데이트되도록 하므로, 데이터 로드 중에도 UI가 일관되게 유지될 수 있다. 이는 사용자 경험을 더욱 안정적으로 만들 수 있는 중요한 요소다.

+ 다른분이랑 얘기하다가 zip에 대해 추가로 알게 된 내용:

이미지가 20개씩 받아와지긴 하는데, 순서가 랜덤으로 나오는 문제를 겪으셨다고 한다.
코드를 보니 .zip을 안 쓰고 있어서, 여기서 순차적으로 처리하는 기능도 있는건가? 했는데
찾아보니까 맞는 것 깉아서 추가로 작성

  • 순서가 랜덤하게 나오는 것은 비동기 작업의 특성 때문인데, 비동기 작업은 각 작업이 독립적으로 수행되며 작업의 완료 시간은 네트워크 상태나 작업의 복잡성에 따라 달라질 수 있다. 예를 들어, 여러 이미지 URL을 비동기적으로 다운로드하는 경우, 첫 번째 URL이 가장 늦게 완료되고 마지막 URL이 가장 먼저 완료될 수도 있다.

  • flatMap이나 merge 같은 연산자를 사용하면, 작업이 완료된 순서에 따라 결과가 방출된다. 이로 인해, 이후 결과를 처리할 때 순서가 뒤바뀔 수 있다. 예를 들어, flatMap을 사용해 여러 비동기 작업을 실행하면, 완료된 순서대로 결과가 처리되므로 UI를 업데이트하거나 데이터를 추가할 때 순서가 섞일 수 있다.

  • 그러나 zip은 여러 비동기 작업을 병렬로 실행하면서 각 작업의 결과를 순서대로 결합하여 반환한다. 예를 들어, 20개의 이미지를 비동기적으로 다운로드할 때, 각 다운로드가 완료된 후 zip은 다운로드된 이미지를 배열에 담아 원래 지정한 순서대로 제공한다.

2. UICollectionViewDelegate - scrollViewDidScroll 메서드

UICollectionViewDelegatescrollViewDidScroll 이라는 메서드가 있었다.
스크롤을 감지할 때마다 안의 코드를 실행해주는 메서드인데,
조건을 설정하지 않으면 스크롤이 조금만 움직여도 바로 실행되니 다음과 같이 조건을 설정했다.

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: 전체 스크롤뷰 Height
  • frameHeight: 한 화면에 보이는 스크롤뷰 영역 Height

설정해두긴 했는데 데이터가 20개씩밖에 안 불러와지니 한 화면에 거의다 보여서... 무한 스크롤 느낌은 안나지만 기다리면 잘 나타난다. 그럼 이만.

profile
정체되지 않는 성장

0개의 댓글