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

NH·2025년 5월 26일

내일배움캠프

목록 보기
60/62
post-thumbnail

트러블슈팅

1. 선택한 검색 결과 주소 이름이 MainCell CityLabel에 전달 되지 않음

원인

  • 현재 MainCell에서 직접 regionCodeRelay를 구독하고 있지만, 셀은 재사용되므로 ViewModel 인스턴스를 별도로 만들면 외부에서 변경된 값과 연결되지 않음.
  • MainViewController의 ViewModel과 MainCell 내부의 ViewModel이 서로 다르기 때문에 데이터가 공유되지 않는다!!!

해결

  • 변경 전
  • MainCell 에서 메소드를 만들어서 사용
// MainCell.swift
func bind() {
	viewModel.regionCodeRelay
		.asDriver(onErrorJustReturn: [])
        .map { $0.first?.addressName ?? "주소 없음" }
		.drive(cityLabel.rx.text)
        .disposed(by: disposeBag)
}
  • 변경 후
// MainCell.swift
func bind(with viewModel: ViewModel) {
    viewModel.regionCodeRelay
        .asDriver(onErrorJustReturn: [])
        .map { $0.first?.addressName ?? "주소 없음" }
        .drive(cityLabel.rx.text)
        .disposed(by: disposeBag)
}
// cellForItemAt 안에서
case .main:
    guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MainCell.identifier, for: indexPath) as? MainCell else { return .init() }
    
    guard let weather = viewModel.output.mainCellData.value else { return cell }
    
    cell.setText(weather: weather)
    cell.bind(with: locationViewModel) // 실시간으로 viewModel에서 cityLabel에 바인딩
    
    return cell

2. PublishRelay로 하면 주소 정보가 전달이 안됨. (BehaviorRelay)

PublishRelay는 구독 시점 이전에 방출된 이벤트를 받을 수 없기 때문에, addressLabel이 업데이트되지 않음.

핵심 비교: PublishRelay vs BehaviorRelay

특징PublishRelayBehaviorRelay
초기값없음있음 (init(value:))
최신값 저장저장 안함최신값 저장
구독 시 최신값 방출안 함방출함
용도일회성 이벤트최신 상태 공유

원인

  • MainCell 코드
viewModel.regionCodeRelay
    .asDriver(onErrorJustReturn: [])
    .map { $0.first?.addressName ?? "주소 없음" }
    .drive(cityLabel.rx.text)
    .disposed(by: disposeBag)

이 코드가 동작하려면

  • viewModel.regionCodeRelay값이 존재해야 하고
  • 구독 시점에 값을 방출해야 함.

하지만 PublishRelay

  • 이전에 .accept()로 값을 보냈어도
  • 이미 구독된 이후가 아니면 아무것도 받지 못함.

그래서 MainCell이 새로 만들어져 bind()를 호출할 때는 이미 regionCodeRelay가 emit을 끝낸 상태,
PublishRelay는 아무런 이벤트도 받지 못함 → 그래서 label이 안 바뀜.

해결

  • 최신 상태(주소)를 UI에서 항상 보여줘야 하는 목적
    BehaviorRelay를 써야 함
let regionCodeRelay = BehaviorRelay<[RegionCodeResponse.Document]>(value: [])
  • 이렇게 하면 **초기값이 "[]"**로 세팅,
  • .accept() 호출 시마다 최신값을 저장하며
  • 나중에 구독한 쪽도 최신값을 받을 수 있음!!

요약

상황적절한 Relay
버튼 클릭 등 일회성 이벤트PublishRelay
상태 바인딩, UI 업데이트처럼 최신 상태 공유BehaviorRelay

3. 최초 실행 시, 주소를 못 가져옴

위치 권한 설정이 나올때 이미 주소 요청을 한상태여서 주소를 못가져옴
따라서 권한 설정이 변경되었을때 한번 더 주소 요청을 해야됨

원인

LocationManager.shared.requestLocation()

이 함수는 내부적으로 아래와 같이 동작

switch status {
case .notDetermined:
    locationManager.requestWhenInUseAuthorization() // 권한 요청만 함 (requestLocation 호출 안 됨)
case .authorizedWhenInUse, .authorizedAlways:
    locationManager.requestLocation() // 위치 갱신 요청
  • 처음 실행 시 권한을 허용하면 requestLocation()이 실행되지 않음.
  • 왜냐면 .notDetermined에서 권한 요청만 하고 끝나기 때문.

해결

CLLocationManager의 delegate 메서드 중 아래의 메서드 사용해야됨.

func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus)
  • 여기서 권한 상태가 .authorizedWhenInUse 또는 .authorizedAlways로 바뀌었는지 감지
  • 감지되면 바로 requestLocation()을 다시 호출.

추가한 코드

func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
    let status = manager.authorizationStatus
    switch status {
    case .authorizedWhenInUse, .authorizedAlways:
        print("권한 허용됨 → 위치 요청 실행")
        manager.requestLocation()
    case .denied, .restricted:
        errorSubject.onNext("위치 권한이 거부되었습니다.")
    default:
        break
    }
}

정리

  • "주소 없음"은 주소 API가 호출되지 않아서 생긴 문제
  • 원인은 초기 권한 승인 시 requestLocation()이 빠졌기 때문
  • 해결책은 locationManagerDidChangeAuthorization에서 직접 requestLocation() 호출하는 것

4. RxSwift 이벤트 중첩 발생

에러 메시지:

⚠️ Reentrancy anomaly was detected.
  • RxSwift에서 "이벤트 중첩 발생"을 감지했다는 경고.
  • Observable이 이전 이벤트 처리 도중에 또 다른 이벤트를 방출했을 때 발생
  • "재진입 문제(Reentrancy Issue)"를 의미.

원인

  • RxSwift는 Observable 시퀀스가 다음의 정확한 문법을 따르길 기대함
  • 근데 지금은 이벤트가 완전히 끝나기도 전에 또 다른 이벤트가 방출되고 있어서 문법이 깨진 것.
  1. Relay (예: BehaviorRelay)에 값을 넣자마자 (.accept()) 그걸 구독 중인 다른 구문이 또 값을 넣는 상황
  2. 즉, accept()subscribe → 다시 accept() ... 식의 순환 참조 또는 이벤트 재진입

재진입 하는 코드

private func transform() {
        self.input
            .subscribe(onNext: { [weak self] input in
                guard let self else { return }
                switch input {
                case .changeCoordinate:
                    self.locationViewModel.fetchRegionCode(longitude: longitude.value, latitude: latitude.value)
                    self.loadWeatherResponseData()
                    self.loadForecastListData()
                case .searchAddressData(let selectedAddress):
                    guard let x = selectedAddress.x,
                          let y = selectedAddress.y else { return }
                    .
                    .
                    .
                    self.input.accept(.changeCoordinate) // 재진입 발생!!!
                case .setUnitButtonTap(let unit):
                    self.tempUnit.accept(unit)
                    print(unit)
                    print(latitude.value, longitude.value)
                    self.loadWeatherResponseData()
                    self.loadForecastListData()
                    
                }
            }).disposed(by: disposeBag)
    }

.accept()로 값을 전달했는데, 그걸 .subscribe해서 뭔가 UI 갱신 또는 또 다른 .accept()를 하여 재진입이 발생!!


해결

  • .observe(on: MainScheduler.asyncInstance) 추가
    private func transform() {
        self.input
            // 재진입 문제를 해결하기 위해 이벤트를 비동기적으로 전달하도록 처리
            .observe(on: MainScheduler.asyncInstance)
            .subscribe(onNext: { [weak self] input in
                guard let self else { return }
                switch input {
                case .settingButtonTap:
                    output.showSettingMenu.accept(())
                .
                .
                .
                    self.input.accept(.changeCoordinate)
                case .setUnitButtonTap(let unit):
                    self.tempUnit.accept(unit)
                    print(unit)
                    print(latitude.value, longitude.value)
                    self.loadWeatherResponseData()
                    self.loadForecastListData()
                    
                }
            }).disposed(by: disposeBag)
    }
  • 이벤트를 비동기적으로 처리해서 "이벤트 중첩"이 발생하지 않게 함!

RxSwift 공식 문서에서도 재진입 문제 회피법으로 추천하는 방법


5. 좌표 데이터 전달이 안되는 현상

원인

case .changeCoordinate:
    LocationManager.shared.coordinateSubject
        .subscribe(onNext: { coordinate in
            self.latitude = "\(coordinate.latitude)"
            self.longitude = "\(coordinate.longitude)"
            print("좌표 값 받아옴 : \(self.latitude), \(self.longitude)")
            self.NOHUNloadForecastListData()
            print("gh")
        }).disposed(by: disposeBag)
  • 이미 MainViewController에서 LocationManager.shared.coordinateSubject를 구독하고 있는 상황에서,
    ViewModel 안에서 다시 구독하는 구조

왜 문제인가

  • MainViewControllerinputBind()에서 .changeCoordinate를 이미 발생시키고 있음
  • 그걸 ViewModel에서 switch input으로 받고,
  • 그 안에서 다시 coordinateSubject를 또 subscribe하고 있음

→ 이 구조는 다음과 같은 문제를 유발합니다:

문제설명
이중 구독좌표가 바뀔 때마다 새 구독이 생기며 disposeBag에 계속 쌓임
실행되지 않음좌표가 이미 emit된 이후라면 새로 구독한 시점에서는 값이 안 들어옴 (ReplaySubject가 아니니까)

해결

  • ViewModel에서는 coordinateSubject를 다시 subscribe하지 말고, MainViewController에서 넘겨준 좌표를 사용

  • MainViewController에서 좌표를 input에 같이 넘겨주도록 변경

    LocationManager.shared.coordinateSubject
       .subscribe { [weak self] coordinate in
           guard let self else { return }
           self.viewModel.latitude = "\(coordinate.latitude)"
           self.viewModel.longitude = "\(coordinate.longitude)"
           self.viewModel.input.accept(.changeCoordinate)
       }.disposed(by: disposeBag)
  • ViewModel에서는 coordinateSubject에 다시 subscribe하지 않기

    case .changeCoordinate:
       print("좌표 값 받아옴 : \(self.latitude), \(self.longitude)")
       self.NOHUNloadForecastListData()
       print("gh")
  • 실행 흐름

  1. MainViewControllerLocationManager.shared.coordinateSubject단 한 번만 구독
  2. 좌표가 바뀌면 → latitude, longitude를 ViewModel에 직접 세팅
  3. 그 뒤에 .changeCoordinate 이벤트 전달
  4. ViewModel은 이미 갖고 있는 좌표를 가지고 바로 API 요청

정리

잘못된 방식올바른 방식
ViewModel 안에서 coordinateSubject를 다시 구독ViewModel은 이미 전달받은 좌표만 사용
구독 안에 또 구독 (subscribe 중첩)MainViewController에서 구독은 한 번만 하고, 필요한 값만 넘김
좌표가 emit된 이후면 ViewModel에서 못 받음ViewController가 최신 좌표를 가지고 직접 ViewModel에 전달

최종 코드

  • MainViewController의 inputBind()

    LocationManager.shared.coordinateSubject
       .subscribe { [weak self] coordinate in
           guard let self else { return }
           self.viewModel.latitude = "\(coordinate.latitude)"
           self.viewModel.longitude = "\(coordinate.longitude)"
           self.viewModel.input.accept(.changeCoordinate)
       }.disposed(by: disposeBag)
  • ViewModel의 transform()

    case .changeCoordinate:
       print("좌표 값 받아옴 : \(self.latitude), \(self.longitude)")
       self.NOHUNloadForecastListData()

6. 위치가 표시가 안됨

원인

  • LocationManager.shared.coordinateSubject를 구독해서 .changeCoordinate input을 viewModel.input에 전달하고 있음:

    LocationManager.shared.coordinateSubject
        .subscribe { [weak self] _ in
            self?.viewModel.input.accept(.changeCoordinate)
        }
  • 그런데 ViewModel의 transform() 메서드 안의 case .changeCoordinate호출되지 않음


핵심 원인

input.bind(onNext:)초기 이벤트만 한 번 바인딩되고 끝남
→ 즉, .bind(onNext:)future-style 단방향 바인딩이기 때문에 새로운 값이 전달되어도 다시 바인딩 로직을 실행하지 않습니다.


해결

  • .subscribe(onNext:)로 바꿔야 함
  • ViewModel의 transform() 수정

-수정 전

private func transform() {
    self.input
        .bind(onNext: { [weak self] input in
            guard let self else { return }

            switch input {
            case .settingButtonTap:
                self.output.showSettingMenu.accept(())

            case .changeCoordinate:
                LocationManager.shared.coordinateSubject
                    .take(1) // 좌표가 바뀔 때마다 최신 값 한 번만 처리
                    .subscribe(onNext: { coordinate in
                        self.latitude = "\(coordinate.latitude)"
                        self.longitude = "\(coordinate.longitude)"
                        print("좌표 변경됨: lat = \(self.latitude), lon = \(self.longitude)")
                    }).disposed(by: self.disposeBag)
            }
        })
        .disposed(by: disposeBag)
}
private func transform() {
    self.input
        .subscribe(onNext: { [weak self] input in
            guard let self else { return }

            switch input {
            case .settingButtonTap:
                self.output.showSettingMenu.accept(())

            case .changeCoordinate:
                LocationManager.shared.coordinateSubject
                    .take(1) // 좌표가 바뀔 때마다 최신 값 한 번만 처리
                    .subscribe(onNext: { coordinate in
                        self.latitude = "\(coordinate.latitude)"
                        self.longitude = "\(coordinate.longitude)"
                        print("좌표 변경됨: lat = \(self.latitude), lon = \(self.longitude)")
                    }).disposed(by: self.disposeBag)
            }
        })
        .disposed(by: disposeBag)
}

.bind(onNext:)가 안 됐을까?

메서드설명사용 용도
.bind(onNext:)UI Binding 전용UILabel.rx.text, button.rx.tap
.subscribe(onNext:)일반 스트림 구독ViewModel 내부 로직 처리 시 사용해야 함

정리

문제해결책
input.bind(onNext:)로는 enum 값 변화 감지 안 됨input.subscribe(onNext:)로 변경
.changeCoordinate case가 안 타는 문제구독 방식 교체로 해결
불필요한 중복 구독 방지.take(1) 또는 throttle, distinctUntilChanged 등으로 조절 가능
profile
iOS 개발 블로그

0개의 댓글