[내일배움캠프 61일차] 날씨 앱 팀프로젝트7

NH·2025년 5월 27일

내일배움캠프

목록 보기
61/62
post-thumbnail

Pull to refresh 리팩터링

오늘은 UICollectionView에서 Pull to Refresh 동작을 Rx 방식으로 리팩터링했다.
기존에는 두 개의 옵저버블을 조합해서 처리했지만, 좀 더 간결하고 선언적인 방식으로 개선했다.

기존 구현 방식 (수정 전)

  • 수정 전
/// pull to refresh
    private func reloadMainCellData() {
        
        var isEnd: Bool = false
        
        weatherCollectionView.rx.didEndDecelerating
            .subscribe(onNext: {
                print("스크롤 완전히 멈춤")
                isEnd = true
            })
            .disposed(by: disposeBag)
        
        weatherCollectionView.rx.contentOffset
            .observe(on: MainScheduler.instance)
            .subscribe(onNext: { [weak self] contentOffset in
                guard let self = self else { return }
                let yOffset = contentOffset.y

                // 바닥에 도달했는지 판단
                if yOffset < -210  && isEnd {
                    print("스크롤 동작")
                    
                    // 더미 데이터 넣기
                    self.viewModel.applyDummyData()
                    
                    LocationManager.shared.coordinateSubject
                        .subscribe(onNext: { coordinate in
                            self.viewModel.input.accept(.changeCoordinate)
                        }).disposed(by: self.disposeBag)

                    self.weatherCollectionView.reloadData()
                    
                    print("데이터 갱신")
                    
                    isEnd = false
                }
            }).disposed(by: disposeBag)
    }
  • weatherCollectionView.rx.didEndDecelerating에서 스크롤 멈춤 여부를 isEnd 변수로 감지
  • contentOffset.y < -210 일 때 Pull to Refresh 동작 수행
  • LocationManager.shared.coordinateSubject를 다시 구독하여 viewModel.input에 .changeCoordinate를 전달

코드가 분리되어 있고, 상태 플래그 isEnd로 제어하여 관리가 다소 복잡함

문제점

  • imperative하고 이벤트 흐름이 명확하지 않음
  • isEnd 플래그는 Rx 방식에 어울리지 않음

수정 후

/// pull to refresh
    private func reloadMainCellData() {
        
        weatherCollectionView.rx.didEndDragging
            .filter { $0 } // 사용자가 손을 뗐을 때만
            .observe(on: MainScheduler.instance)
            .map { [weak self] _ in
                guard let self else { return 0 }
                return self.weatherCollectionView.contentOffset.y
            }
            .filter { yOffset in
                yOffset < -210 // 위로 잡아 당긴 높이
            }
            .subscribe(onNext: { [weak self] _ in
                guard let self else { return }
                
                print("당겨짐 감지")

                self.viewModel.applyDummyData() // 더미 데이터 및 날씨 요청
                self.viewModel.input.accept(.changeCoordinate) // 위지 정보 요청
                self.weatherCollectionView.reloadData() // 데이터 UI에 리로드
            }).disposed(by: disposeBag)
    }

장점

  • Rx의 선언적 방식에 충실한 체인형 처리
  • 스크롤 이벤트 흐름이 명확함 (didEndDragging → contentOffset.y → 조건 필터 → 액션)
  • 플래그 상태 없이도 정확한 시점 감지 가능
  • 코드 간결성과 가독성 상승

느낀점

  • Rx는 imperative한 상태 체크보다 이벤트 흐름을 선언적으로 표현하는 것이 유지보수와 디버깅에 유리하다는 것을 다시 한 번 느꼈다.
  • 기존처럼 isEnd 같은 변수 상태로 제어하면 side-effect가 생기기 쉬우므로, 가능한 연산자 체인 안에서 조건과 흐름을 명확히 처리하는 습관을 가져야겠다.
  • 앞으로는 Rx에서 어떤 동작을 만들 때, 의도를 연산자로 표현하는 것을 먼저 고민하자.
profile
iOS 개발 블로그

0개의 댓글