[25.06.24] MOUP 트러블슈팅 - Listener와 Rx의 timeout

2025. 6. 24. 01:50·iOS/Swift

1. 문제 상황

홈 내 테이블뷰에 들어갈 데이터를 불러오는 과정에서 combineLatest를 호출한다.

extension HomeViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        guard let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: HomeHeaderView.identifier) as? HomeHeaderView else {
            return UIView()
        }

        Observable.combineLatest(output.headerData, output.userType)
            .observe(on: MainScheduler.instance)
            .subscribe(onNext: { headerData, userType in
                print("headerData: \(headerData)")
                print("구독 실행! ID: \(UUID().uuidString.prefix(8))")
                headerView.update(with: headerData, userType: userType)
            }) // TODO: - viewForHeaderInSection 다중 호출 특성으로 인한 다중 구독 해결
            .disposed(by: disposeBag)

        // headerView 내 액션 정의
        headerView.rx.todaysRoutineCardTapped
            .subscribe(onNext: { [weak self] in
                guard let self else { return }
                let vc = self.makeManageRoutineViewController(type: .today) // 추가 params 입력을 통해 오늘 or 전체 여부 분기
                self.navigationController?.pushViewController(vc, animated: true)
            }).disposed(by: disposeBag)
        headerView.rx.allRoutineCardTapped
            .subscribe(onNext: { [weak self] in
                guard let self else { return }
                let vc = self.makeManageRoutineViewController(type: .all)
                self.navigationController?.pushViewController(vc, animated: true)
            }).disposed(by: disposeBag)
        
        headerView.rx.plusButtonTapped
            .subscribe(onNext: {
                let workplaceAddModalVC = WorkplaceAddModalViewController()
                let nav = UINavigationController(rootViewController: workplaceAddModalVC)
                nav.modalPresentationStyle = .overFullScreen
                nav.modalTransitionStyle = .crossDissolve
                self.present(nav, animated: true, completion: nil)
            })
            .disposed(by: disposeBag)

        return headerView
    }

viewModel에서 output으로 보내주는 스트림들을 종합해 이 데이터들을 기반으로 header를 업데이트하도록 한다.

viewModel에서 관련 데이터를 스트림으로 fetch해오는 부분은 다음과 같다.

return Observable.combineLatest (
            self.workplaceUseCase.fetchAllWorkplacesForUser(uid: userId),
            self.workplaceUseCase.fetchMonthlyWorkSummary(uid: userId, year: currentYear, month: currentMonth),
            self.workplaceUseCase.fetchMonthlyWorkSummary(uid: userId, year: previousYear, month: previousMonth),
            routineUseCase.fetchTodayRoutineEventsGroupedByWorkplace(uid: userId, date: Date())
                .timeout(.seconds(5), scheduler: MainScheduler.instance)
        )
        .map { workplaces, currentSummaries, previousSummaries, todayRoutines in
            print("내 근무지들: \(workplaces)")
            print("내 유저타입: \(userType)")
            print("내 루틴들: \(todayRoutines)")

 

모아오는 usecase 관련 호출 중에 오늘의 루틴이 빈 값으로 올 때 문제가 생겨서 해당 문제에 대한 해결 방안으로 이전에 timeout 기능을 적용해두었고, 해당 기능이 잘 작동하다가 이런 상황이 생겼다. 맨 처음 데이터 로딩은 분명 잘 되는데 5초 이후 sequence timeout이 뜨는 문제가 있었다.

timeout은 api 호출 등 어느 작업을 했을때 스트림에 그 시간내로 방출이 없을 시 오류 상황임을 판단하고 처리를 하게 설정하는 메서드라고 이해하고 있다. 단, 그 시간 내로 데이터 방출이 원활하게 되면 타임아웃 자체가 의미가 없어질텐데..? 계속 원점으로 돌아오는 문제 해결 시간이었다.

 

2. 문제 해결

온갖 시도를 해봤는데 잘 안되다가 useCase - Repository - Service 측으로 타고 들어가니 서비스 레이어 구현 방식이 기존 단순 Observable에 데이터를 방출하던 방식에서 리스너 기반으로 바뀌었던 것. (Firebase에서 제공하는 실시간 리스너)

 

timeout은 한번 받으면 오케이라고 위에서 말했는데, 이 리스너는 어느 값의 방출에 대해서 계속 감시하고 있는 방식이다보니 처음에 값을 방출한다고 해도 바로 새로 감시를 함과 동시에 타임아웃도 걸려버리는 것이다.

 

func transform(input: Input) -> Output {
        // 데이터 fetch 트리거
        let dataLoadTrigger = Observable.merge(
            input.viewDidLoad.map { _ in RefreshType.normal },
            input.refreshBtnTapped.map { _ in RefreshType.normal },
            silentRefreshTrigger.map { _ in RefreshType.silent }
        )

        dataLoadTrigger
            .withLatestFrom(refreshTypeRelay) { _, refreshType in refreshType }
            .flatMapLatest { [weak self] refreshType -> Observable<User> in
                print("transform - user triggered")
                switch refreshType {
                case .normal:
                    LoadingManager.start()
                case .silent: break
                }
                guard let self else { return .empty() }
                return self.userUseCase.fetchUser(uid: userId)
            }

 

코드 중 일부인데 트리거가 결국 직접 새로고침 버튼을 누르지 않는 이상 맨 처음 viewDidLoad 시에 걸리게 되는데, 이때 한번 받고 타임아웃 초기화 후 인위적으로 데이터를 받아오도록 하지 않아서 그 타임아웃이 해제가 된 것이었다.

 

결론은 Listener는 계속 값 방출을 기다리고 감시하는 반면, 나는 해당 함수 호출 시 타임아웃을 걸어두어도 제한시간은 매 데이터 방출할 때마다 초기화가 되는 게 문제의 핵심이었다.

 

해당 시점에서 오류가 생겨 combineLatest의 요소 중 하나가 문제가 생기고 결국 해당 스트림은 끊기게 되어 이후 새로고침을 눌렀다는 트리거의 이벤트가 방출이 되어도 새로고침이 전혀 되지 않던게 사이드 이펙트였다.

 


너무 졸린데 .. 이건 시간을 너무 많이 써서 꼭 써야했다.. 

리스너와 타임아웃에 대해 좀 더 깊게 알 수 있게 되어서 좋았다. combineLatest의 스트림 성질도 포함해서 !

'iOS > Swift' 카테고리의 다른 글

[25.08.04] MOUP 트러블슈팅 - .xcconfig 내 url 설정 시 주의사항  (5) 2025.08.05
[25.07.31] MOUP 트러블슈팅 - tableHeaderView 레이아웃 제약 경고  (2) 2025.07.31
[TIL / 25.05.27] 날씨 앱 main 페이지 구조 트러블슈팅  (5) 2025.05.27
[TIL / 25.05.18] 의존성 주입 담당 DIContainer를 처음 적용해보았습니다  (4) 2025.05.18
[TIL / 25.05.13] BookSearchApp Lv 3 트러블슈팅 - Core Data 크래시  (0) 2025.05.14
'iOS/Swift' 카테고리의 다른 글
  • [25.08.04] MOUP 트러블슈팅 - .xcconfig 내 url 설정 시 주의사항
  • [25.07.31] MOUP 트러블슈팅 - tableHeaderView 레이아웃 제약 경고
  • [TIL / 25.05.27] 날씨 앱 main 페이지 구조 트러블슈팅
  • [TIL / 25.05.18] 의존성 주입 담당 DIContainer를 처음 적용해보았습니다
subkyu-ios
subkyu-ios
subkyu-ios 님의 블로그 입니다.
  • subkyu-ios
    subkyu-ios 님의 블로그
    subkyu-ios
  • 전체
    오늘
    어제
    • 분류 전체보기 (56)
      • iOS (38)
        • Swift (38)
      • 내일배움캠프 (7)
      • Git, Github (3)
      • Algorithm (6)
      • 회고 (1)
      • 면접 질문 정리 (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    프로그래머스
    Wil
    stackview
    RxSwift
    의존성 주입
    트러블슈팅
    KPT
    TableView
    사전캠프
    Swift
    내일배움캠프
    algorithm
    알고리즘
    tabman
    본캠프
    til
    github
    회고
    ios
    UIKit
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
subkyu-ios
[25.06.24] MOUP 트러블슈팅 - Listener와 Rx의 timeout
상단으로

티스토리툴바