내용정리
UITextField를 RxSwift를 활용하여 사용하다가 텍스트필드의 변경이 감지되지 않는 문제가 발생했다.
이를 해결하는 과정을 정리해 보려고 한다.
최종 프로젝트를 진행하며 RxSwift를 사용하여 UITextField의 값 변경을 감지하는 과정에서 값이 변경되었음에도 불구하고 Rx에서 이를 감지하지 못하는 문제가 발생했다.
로직의 경우 아래의 configureTextField() 메소드를 사용했는데...
/// DatePicker 뷰의 날짜를 설정하는 메소드
/// - Parameter date: 입력할 날짜
func configureTextField(date: Date) {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy년 MM월 dd일"
self.textField.text = formatter.string(from: date)
}
이 메소드를 사용해서 UITextField의 텍스트를 직접 변경할 경우 RxSwift의 바인딩이 이를 감지하지 못하고, 이후의 로직이 예상한 대로 흐르지 않는 버그가 발생했다.
이 문제를 해결하기 위해 브레이크 포인트를 걸어 원인을 찾아보고, UITextField와 Rx간의 관계를 찾아보았다.
먼저, 위에서 작성한 메소드를 확인해 보면
self.textField.text = "새로운 값"
이런 형식으로 텍스트를 정의한다. 그러나 UITextField의 텍스트 값을 이렇게 직접 변경하면, RxSwift의 rx.text.orEmpty와 바인딩된 옵저버블이 이를 감지하지 못한다고 한다.
그 이유는 RxSwift의 rx.text.orEmpty 코드는 UIControlEvents.editingChanged 이벤트를 기반으로 동작하기 때문이다.
즉, 유저의 입력으로 값을 입력하는 것이 아닌 프로그램적으로 값을 설정하면 editingChanged 이벤트가 발생하지 않기 때문에 Rx에서 텍스트가 변경됨을 감지할 수 없는 것이다.
RxSwift는 UI에서 실제 입력이 발생해야 이벤트를 감지하는 구조이므로, 코드에서 직접 값을 변경할 경우 이벤트 흐름이 끊기게 되는 것이다.
이제 원인을 파악했으니 문제를 해결해보자.
이 문제를 해결하기 위해서는 RxSwift가 변경 사항을 감지하도록 이벤트를 발생시켜야 한다.
이를 위해 사용할 수 있는 메소드로 sendAction(for:)이 있다.
textField.text = "새로운 값"
textField.sendAction(for: .valueChanged) // Rx 흐름 유지
이렇게 하면 RxSwift가 valueChanged 이벤트를 감지하고, 기존의 바인딩된 옵저버블이 정상적으로 동작하게 된다.
이를 함수로 분리해서 적용하면 더욱 깔끔하게 정리할 수 있다.
private func updateTextField(with date: Date) {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy년 MM월 dd일"
DispatchQueue.main.async { [weak self] in
self?.textField.text = formatter.string(from: date)
self?.textField.sendActions(for: .valueChanged) // Rx 흐름 유지
}
}
위의 메소드에서 DispatchQueue.main.async를 사용해 준 이유는, self?.textField.text = formatter.string(from: date) 코드를 실행하게 되면 UI 변경이 발생하는데, 이 때 sendAction(for:) 메소드를 호출해도 반응이 없는 문제가 발생할 수도 있기 때문이다.
이는 RxSwift에서 UI의 업데이트는 항상 메인 스레드에서 실행되어야 하지만, 일부 경우 백그라운드에서 실행될 가능성이 있기 때문이다.
즉, self?.textField.text = formatter.string(from: date) 코드가 백그라운드 스레드에서 실행되면 UI 업데이트가 반영되지 않거나 sendAction(for:) 메소드가 기대한 대로 동작하지 않을 가능성이 있기 때문에 DispatchQueue.main.async를 사용하여 명시적으로 메인 스레드에서 실행됨을 보장하는 것이다.
그렇다면 왜 UI 업데이트가 백그라운드에서 실행될 가능성이 있을까?
RxSwift에서는 다양한 스트림(Observable, Binder)을 통해 데이터를 받아 UI에 반영하는데, 이 데이터가 네트워크 응답, 파일 로딩, 비동기 작업 등의 결과라면 백그라운드 스레드에서 실행될 가능성이 높다.
특히 .subscribe(on:)이나 .observe(on:) 등으로 스레드를 명시적으로 지정해주지 않은 경우 예상치 못하게 백그라운드에서 실행될 가능성이 있다.
때문에 이를 방지하기 위해서는 스케줄러를 통해 스레드를 명시적으로 지정해주거나, 알맞은 시점에서 DispatchQueue.main.async 등을 사용하여 메인 스레드의 작업임을 보장해 주어야 하는 것이다.
이번에 발생한 문제는 UIKit의 이벤트 처리 방식과 RxSwift의 반응형 프로그래밍 간 차이에서 발생한다.
UIKit에서는 UITextField.text를 변경하는 방법이 두 가지이다:
사용자가 직접 입력하는 경우 → editingChanged 이벤트 발생 (Rx 감지 가능)
코드에서 직접 변경하는 경우 → 이벤트 없음 (Rx 감지 불가능)
RxSwift의 rx.text.orEmpty는 1번 방식만 감지할 수 있기 때문에, 2번 방식에서 sendActions(for:)를 호출해 이벤트를 강제로 발생시켜야 한다.
이러한 문제는 UISwitch, UISlider, UIDatePicker 등 UIControl을 상속하는 컴포넌트 전반에서 발생할 수 있다.
즉, RxSwift로 이벤트를 감지하려면 이벤트가 UIKit 내부에서 발생해야 하며, 코드에서 직접 값을 변경하면 감지되지 않는다.
RxSwift를 활용한 반응형 프로그래밍에서는 UI 이벤트가 발생해야만 옵저버블이 정상적으로 동작한다. 하지만 코드에서 값을 직접 변경하면 이벤트가 발생하지 않기 때문에, RxSwift의 바인딩이 깨지는 문제가 발생할 수 있다.
이 문제를 해결하기 위해서는 sendActions(for: .valueChanged) 등의 이벤트 강제 발생 메소드를 활용하는 방식을 고려해볼 수 있다.
이러한 문제는 UITextField뿐만 아니라, UISwitch, UISlider, UIDatePicker 등의 다른 UIControl에서도 동일하게 적용할 수 있다. RxSwift를 사용할 때 이벤트 흐름을 유지하려면 이러한 원리를 이해하고 적용하는 것이 필수적이다.
이제는 RxSwift를 사용할 때 프로그램적으로 UI 값을 변경할 때는 반드시 이벤트를 트리거해야 한다는 점을 기억하자
RxSwift와 친해지려면 아직도 멀었을지도 모르겠다