RxSwift를 사용해서 UI작업을 할 때

RxCocoa에 존재하는 Relay, Signal, Driver를 많이 사용하곤 한다. 

이 3개의 차이점에 대해서 알아보자. 

 

 

Relay

RelaySubject의 Wrapper Class이다.

이는 completed, error이벤트를 무시하며, next이벤트만 Subscriber에게 전달한다. 

 

UI Update를 위해 사용하는 Subject가 

error 혹은 completed이벤트로 스트림이 끊기면 안 되기 때문에,

RelayUI작업에 특화된 Subject이다.

 

더 자세한 내용은 해당 포스팅에 있으니 참고 바란다.

 

 

Driver & Signal

DriverSignalRxCocoa의 Trait

Main Thread에서 동작하는 것을 보장하며,

error이벤트를 무시한다.

Trait란 좀 더 제한적인 기능만 하는 Observable이다.

 

따라서, UI에 직접 바인딩하는 경우라면

반드시 Main Thread에서 동작해야 되기 때문에 

해당 경우에는 Driver 혹은 Signal이 더 적합하다. 

/// Driver
let driver = Driver<Int>.just(1)

driver.drive(
	onNext: { print("driver: \($0)") }
) 
.disposed(by: disposeBag)

/// Signal
let signal = Signal<Int>.just(1)

signal.emit(
	onNext: { print("signal: \($0)") }
)
.disposed(by: disposeBag)

Driver를 구독하기 위해선, drive 메서드를 사용하고,

Signal을 구독하기 위해선, emit 메서드를 사용한다. 

 

또한 DriverSignal은 위와 같이 just, of, from을 사용해서 생성할 수는 있지만, 

create를 통해 섬세하게 생성할 수 없기 때문에, 

보통 Observable을 asDriver() 메서드를 혹은 asSignal() 통해 변환한다.

변환 시에는 error 이벤트를 방출할 때 어떻게 처리할지에 대한 처리를 해주어야 한다.

func observableCreate() -> Observable<Int> {
	return Observable.create { emitter in
		emitter.onNext(1)
		emitter.onError(MyError.defaultError)

		return Disposables.create()
	}
}

/// Error시에 클로져 내부를 수행한다.
driver1 = observableCreate().asDriver(onErrorRecover: { error in
	return Driver.of(1)
})
/// Error시에 지정한 Driver로 Drive한다
driver2 = observableCreate().asDriver(onErrorDriveWith: recoverDriver)
/// Error시에 지정한 값을 리턴한다.
driver3 = observableCreate().asDriver(onErrorJustReturn: 3)

 

/// Error시에 클로져 내부를 수행한다.
signal1 = observableCreate().asSignal(onErrorRecover: { error in
	return Signal.of(1)
})
/// Error시에 지정한 Driver로 Drive한다
signal2 = observableCreate().asSignal(onErrorSignalWith: recoverSignal)

/// Error시에 지정한 값을 리턴한다.
signal3 = observableCreate().asSignal(onErrorJustReturn: 3)

asDriverSubject, Relay에 사용이 가능하다. 

 

 

Driver와 Signal 차이점 

DriverSignal 내부 구현부를 본다면, 아래와 같다. 

source.share(replay: 1, scope: .whileConnected) // Driver

source.share(scope: .whileConnected)  // Signal

위와 같이 share연산자를 통해 스트림을 공유하기 때문에,

여러 번 구독한다 해서 스트림이 여러 개가 생기지 않고 

스트림을 공유하게 된다.

다만 공유 스트림Subscriber가 1개 이상이고, completed 이벤트가 발생되지 않을 때 유지된다. 

만약 share를 잘 모른다면, 해당 포스팅을 참고 바란다.

 

Driver의 경우 replay가 1로 설정되어 있기 때문에, 

구독 시에 이전의 값을 받아볼 수 있다

let publishSubject = PublishSubject<Int>()

let driver = publishSubject.asDriver(onErrorJustReturn: 1)

publishSubject.onNext(1)

driver.drive(
	onNext: { print($0) }
)
.disposed(by: disposeBag)

publishSubject.onNext(2)


/* Print:
 2
 */

다만 whileConnected이기 때문에, Subscriber가 0개인 경우엔 버퍼에 값을 저장하지 않는다. 

 

추가적으로 BehaviorSubject같은 경우는 

아래와 같은 모습을 볼 수 있는데, 

et behaviorSubject = BehaviorSubject<Int>(value: 1)

let driver = behaviorSubject.asDriver(onErrorJustReturn: 1)

behaviorSubject.onNext(1)

driver.drive(
	onNext: { print($0) }
)
.disposed(by: disposeBag)

behaviorSubject.onNext(2)


/* Print:
 1
 2
 */

이는 sharereplay값이 반영된 것이 아닌,

BehaviorSubject 자체적으로 구독 시에 이전의 값까지 같이 방출되기 때문에, 

위와 같은 결과가 나오게 된다.

 

Signal의 경우는 replay의 값이 default인 0으로 세팅되어 있기 때문에,

source.share(scope: .whileConnected)  // Signal

구독 시점에서 이전의 값이 방출되지 않는다. 

 

 

Relay와 같이 사용하는 경우

BehaviorRelayDriver로 변환하는 경우 에러에 대한 처리 필요 없이 전환이 가능하다. 

let behaviorRelay = BehaviorRelay<Int>(value: 1)

behaviorRelay.asDriver()

또한, PublishRelaySignal로 변환해주는 경우 에러에 대한 처리 없이 변환이 가능하다.

let publishRelay = PublishRelay<Int>()

publishRelay.asSignal()

 

개인적인 생각으론, Relay는 모두 error이벤트를 방출하지 않기 때문에, 

Driver, Signal 모두 에러에 대한 처리 없이 변환이 가능할 수 있다고 생각하는데, 

실제론 위와 같이 (PublishRelay ⇨ Signal), (BehaviorRelay ⇨ Driver) 이렇게만 에러 처리 없이 변환이 가능하다

이후에 알게된다면 추가적으로 포스팅하겠다.

 

 

Summary

Relay의 경우 completed와 error이벤트를 무시하지만, 

Main Thread에서 작동하는 것을 보장하지 않는다

하지만, Subject를 Wrapper했기 때문에,

구독 여부와 관계없이 원하는 시점에 accept를 통해 데이터를 방출할 수 있으며, 

하나의 Stream을 통해서 여러 구독자들에게 이벤트를 방출한다. (Multicast)

 

DriverSignalerror이벤트만 무시하며, 

Main Thread에서 작동하는 것을 보장한다. 

또한 이 둘은 Subscriber가 1명 이상이고,

completed이벤트가 오지 않는 이상 공유 stream을 유지한다.

 

Driver의 경우, 공유 Stream에서 구독 시점에서 이전의 값을 방출해 주기 때문에, 

하나의 소스에서 같은 이벤트로 여러 UI를 업데이트 하는 경우 유용하다. 

 

반면 Signal의 경우, 구독 시점 이전의 값을 방출하지 않기 때문에, 

유저의 액션과 같은 같은 이벤트를 View에서 ViewModel로 전달하는데 유용하다.

 

 

Reference

https://github.com/fimuxd/RxSwift/blob/master/Lectures/13_Intermediate%20RxCocoa/Ch13.Intermediate%20RxCocoa.md

https://github.com/fimuxd/RxSwift/blob/master/Lectures/12_Beginning%20RxCocoa/Ch12.%20Beginning%20RxCocoa.md

'iOS > RxSwift' 카테고리의 다른 글

[RxSwift] Single With Share Operator  (0) 2023.07.26
[RxSwift] Operator(4) - Share  (0) 2023.07.24
[RxSwift] Operator(3) - Combining  (0) 2023.01.08
[RxSwift] Traits  (0) 2022.12.28
[RxSwift] Operator(2) - Transforming  (2) 2022.12.27
복사했습니다!