내용정리
RxCocoa를 사용하면서
Driver와Signal을 많이 사용했는데, 정작 둘의 차이는 잘 모른 채로 사용했던 것 같다.
오늘은 둘의 역할과 차이를 명확히 공부하여 앞으로 어떤 상황에서 어떤 trait을 사용하는 것이 옳은지 알아보려고 한다.
Driver는 UI 상태 업데이트를 안전하게 처리할 수 있도록 RxCocoa에서 제공하는 특수한 옵저버블 타입이다.
내부를 살펴보면 아래의 소스 코드를 볼 수 있는데,

여기서 핵심 코드는 scheduler와 share 부분이다.
먼저 scheduler를 살펴보면 SharingScheduler.make()를 getter로 반환하는데, 이 코드 내부를 살펴 보면

이렇게 MainScheduler를 반환하는 코드임을 알 수 있다.
즉, Driver는 이 코드를 통해 항상 메인스레드에서 동작되는 것을 보장하는 것이다. Driver는 애초에 UI 업데이트를 위해 사용하는 옵저버블이므로, 항상 메인스레드에서 동작하는 것이 보장되어 개발자의 일을 하나 줄여주는 것이라고 볼 수 있다.
다음으로 share를 살펴보면 source.share(replay: 1, scope: .whileConnected)이라는 코드를 볼 수 있는데, 이는 옵저버블의 연산자 중 하나인 share를 사용한 코드이다.
즉, Driver는 항상 가장 최근의 값을 저장하여 새로운 구독자가 최신 상태를 받을 수 있도록 지원한다.
원래는 새로운 옵저버(구독자)가 옵저버블을 구독할 때마다 새로운 스트림이 생겨나지만, Driver는 share를 사용하여 구독이 1개 이상 존재하는 상황에서만 유지되는 공유 옵저버블 타입임을 명시하는 것이다.
이렇게 하면 UI 이벤트 소스는 하나 뿐이기 때문에 같은 이벤트가 모든 옵저버에게 보여지게 되게 된다. 이 덕분에 Driver는 Hot Observable로 사용되어 즉시 구독이 가능하기도 하다.
또, Driver를 반환하는 asDrive()의 구현 메소드를 보면 아래와 같은데,

여기서 .catchAndReturn(onErrorJustReturn) 코드 덕분에 RxSwift에서 오류가 발생해도 기본값을 반환하고 스트림을 정상적으로 종료한다.
UI 업데이트는 흐름이 끊기면 안되기 때문에, UI 업데이트 과정에서 오류가 발생하더라도 앱이 멈추지 않도록 도와주는 것이다.
즉, Driver는 Error가 발생하더라도 이를 이벤트로 방출하지 않고 기본값을 방출하기 때문에 UI의 흐름이 깨지지 않고 앱이 종료되지 않음을 보장해준다.
이러한 이유들 덕분에 Driver는 UI의 업데이트를 담당하는 옵저버블로 유용히 사용된다.
Signal은 RxCocoa에서 제공하는 특수한 옵저버블 타입으로, 주로 UI 이벤트를 처리할 때 사용된다. 기본적으로 Driver와 유사한 특징을 가지고 있지만, 특정 상황에서 더 적합한 특성을 가지고 있다.
내부 코드를 살펴보면 아래의 소스 코드를 볼 수 있다.

여기서 scheduler는 Driver와 동일하기 때문에 Signal도 마찬가지로 MainScheduler에서 사용되어 메인스레드에서 동작하는 것을 보장함을 알 수 있다.
다른 점은 share인데, source.share(scope: .whileConnected)로 되어있다는 것은 옵저버(구독자)는 구독 이후의 이벤트만을 받을 수 있다는 것이다.
이는 여러 구독자가 있어도 새로운 스트림을 만들지 않고 자동으로 공유되는 것을 뜻하며, whileConnected 옵션 덕분에, 구독자가 없으면 리소스를 해제하고 새로운 구독자가 생기면 다시 생성되는 자동 관리가 이루어진다.
또, Signal을 만드는 asSignal()을 살펴보면 Driver와 같이 에러를 반환하지 않고 기본값을 반환하도록 처리가 되어있는 모습을 볼 수 있다.

즉, Signal도 UI 이벤트에서 오류가 발생하더라도 흐름을 끊거나 앱을 종료하지 않는 것을 보장한다는 것을 알 수 있다.
둘의 공통점을 정리하자면 아래와 같다.
Main Thread에서 동작
Hot Observable (즉시 구독 가능)
Error를 방출하지 않음 (never 에러 전략 사용)
asDriver(onErrorJustReturn:) 또는 asSignal(onErrorJustReturn:)을 사용하면 에러가 발생해도 기본값을 반환하고 계속 실행된다.Driver와 Signal의 차이점을 정리하면 아래와 같다.
| 특징 | Driver | Signal |
|---|---|---|
| 상태 유지 | ✅ (Replay 1) | ❌ (Replay 없음) |
| UI 이벤트 | ✅ 주로 상태 기반 UI 업데이트(UILabel, TableView) | ✅ 주로 순간적인 UI 이벤트(버튼 탭, 알림) |
| 특성 | 과거 데이터를 1개 유지하여 새로운 구독자에게 최신 값 전달 | 과거 데이터 유지X, 새로운 구독자는 새로운 이벤트부터 수신 |
| 사용 예시 | UI 상태 업데이트(텍스트, 테이블 뷰 데이터) | 버튼 클릭, 팝업 표시, 알림 |
import RxSwift
import RxCocoa
let disposeBag = DisposeBag()
// 1. Driver 예제 (과거 값 유지)
let driver = Observable.of("A", "B", "C")
.asDriver(onErrorJustReturn: "Error")
driver.drive(onNext: { value in
print("Driver: \(value)")
}).disposed(by: disposeBag)
// 2. Signal 예제 (과거 값 유지 X)
let signal = Observable.of("1", "2", "3")
.asSignal(onErrorJustReturn: "Error")
signal.emit(onNext: { value in
print("Signal: \(value)")
}).disposed(by: disposeBag)
// 출력 결과
Driver: C // 가장 최근 값인 "C"만 방출됨 (Replay 1)
Signal: 1
Signal: 2
Signal: 3 // 전체 값이 방출됨 (Replay 없음)
UITableView, UILabel, UITextField와 같은 UI 요소를 지속적으로 업데이트할 때Driver도 Signal도 모두 UI의 이벤트를 감지하고 업데이트 해주기 위해 사용하는 trait이지만, 둘의 특성은 비슷하면서도 달라서 적절할 시점에 사용하는 것이 좋은 것임을 알게 되었다.
두 trait은 모두 메인스레드에서 작동을 보장하는데, 사실 observe(on:)이나 subscribe(on:)으로 스케줄러를 지정하면 메인스케줄러가 아닌 다른 스케줄러, 즉, 다른 스레드에서 실행되도록 설정할 수 있다고 한다.
그러나 이는 권장하는 방법은 아니라고 하니 주의하도록 하자.
결론적으로 RxCocoa를 사용해서 UI 요소를 지속적으로 업데이트 해야한다면 Driver를, 순간적인 UI 이벤트를 처리하려면 Signal을 사용하는 것이 적합하다.
RxCocoa가 UI 업데이트와 관련된 데이터 바인딩을 진행할 때
무척이나 유용해서 잘 사용했는데,
정작 둘의 차이도 잘 모른 채로 Signal을 남용해왔다.
이제는 이런 실수를 하지 않고 상황에 따라 적절하게 Driver와 Signal을
잘 사용해야겠다고 생각했다.