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 |