// 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
PublishRelay는 구독 시점 이전에 방출된 이벤트를 받을 수 없기 때문에, addressLabel이 업데이트되지 않음.핵심 비교:
PublishRelayvsBehaviorRelay
특징 PublishRelayBehaviorRelay초기값 없음 있음 ( init(value:))최신값 저장 저장 안함 최신값 저장 구독 시 최신값 방출 안 함 방출함 용도 일회성 이벤트 최신 상태 공유
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이 안 바뀜.
BehaviorRelay를 써야 함let regionCodeRelay = BehaviorRelay<[RegionCodeResponse.Document]>(value: [])
.accept() 호출 시마다 최신값을 저장하며| 상황 | 적절한 Relay |
|---|---|
| 버튼 클릭 등 일회성 이벤트 | PublishRelay |
| 상태 바인딩, UI 업데이트처럼 최신 상태 공유 | BehaviorRelay |
위치 권한 설정이 나올때 이미 주소 요청을 한상태여서 주소를 못가져옴
따라서 권한 설정이 변경되었을때 한번 더 주소 요청을 해야됨
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
}
}
requestLocation()이 빠졌기 때문locationManagerDidChangeAuthorization에서 직접 requestLocation() 호출하는 것에러 메시지:
⚠️ Reentrancy anomaly was detected.
.accept()) 그걸 구독 중인 다른 구문이 또 값을 넣는 상황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 공식 문서에서도 재진입 문제 회피법으로 추천하는 방법
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의 inputBind()에서 .changeCoordinate를 이미 발생시키고 있음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")
실행 흐름
MainViewController가 LocationManager.shared.coordinateSubject를 단 한 번만 구독latitude, longitude를 ViewModel에 직접 세팅.changeCoordinate 이벤트 전달| 잘못된 방식 | 올바른 방식 |
|---|---|
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()
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:)로 바꿔야 함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 등으로 조절 가능 |