본문 바로가기
iOS/RxSwift

[RxSwift] drive는 뭘까

by 0inn 2022. 12. 18.

subscribe랑 bind를 사용하곤 했는데 drive를 쓰면 UI 작업에 안전하다고들 한다.

왜일까 궁금해져서 각 메서드를 비교해야겠다 결심했다 . . !

 

Rx 내부 코드 보면서 subscribe vs. bind vs. drive 를 언제쓸지에 대해 정리해보려고 한다.

 

subscribe

/**
     Subscribes an element handler, an error handler, a completion handler and disposed handler to an observable sequence.
     
     - parameter onNext: Action to invoke for each element in the observable sequence.
     - parameter onError: Action to invoke upon errored termination of the observable sequence.
     - parameter onCompleted: Action to invoke upon graceful termination of the observable sequence.
     - parameter onDisposed: Action to invoke upon any type of termination of sequence (if the sequence has
     gracefully completed, errored, or if the generation is canceled by disposing subscription).
     - returns: Subscription object used to unsubscribe from the observable sequence.
     */
    public func subscribe(
        onNext: ((Element) -> Void)? = nil,
        onError: ((Swift.Error) -> Void)? = nil,
        onCompleted: (() -> Void)? = nil,
        onDisposed: (() -> Void)? = nil
    ) -> Disposable {
            let disposable: Disposable
            
            if let disposed = onDisposed {
                disposable = Disposables.create(with: disposed)
            }
            else {
                disposable = Disposables.create()
            }
            
            #if DEBUG
                let synchronizationTracker = SynchronizationTracker()
            #endif
            
            let callStack = Hooks.recordCallStackOnError ? Hooks.customCaptureSubscriptionCallstack() : []
            
            let observer = AnonymousObserver<Element> { event in
                
                #if DEBUG
                    synchronizationTracker.register(synchronizationErrorMessage: .default)
                    defer { synchronizationTracker.unregister() }
                #endif
                
                switch event {
                case .next(let value):
                    onNext?(value)
                case .error(let error):
                    if let onError = onError {
                        onError(error)
                    }
                    else {
                        Hooks.defaultErrorHandler(callStack, error)
                    }
                    disposable.dispose()
                case .completed:
                    onCompleted?()
                    disposable.dispose()
                }
            }
            return Disposables.create(
                self.asObservable().subscribe(observer),
                disposable
            )
    }
 public func subscribe(
        onNext: ((Element) -> Void)? = nil,
        onError: ((Swift.Error) -> Void)? = nil,
        onCompleted: (() -> Void)? = nil,
        onDisposed: (() -> Void)? = nil
    )

 

파라미터로 onNext, onError, onCompleted, onDisposed를 받고 각 이벤트에 대한 처리가 가능하다.

기본값 nil로 지정되어 있기 때문에 각 이벤트들은 상황에 따라 필요한 것만 사용이 가능하다.

에러처리가 가능하다는 것이 가장 중요하고, 쓰레드 지정이 가능하다.

bind

/**
    Subscribes an element handler to an observable sequence.
    In case error occurs in debug mode, `fatalError` will be raised.
    In case error occurs in release mode, `error` will be logged.

    - parameter onNext: Action to invoke for each element in the observable sequence.
    - returns: Subscription object used to unsubscribe from the observable sequence.
    */
    public func bind(onNext: @escaping (Element) -> Void) -> Disposable {
        self.subscribe(onNext: onNext,
                       onError: { error in
                        rxFatalErrorInDebug("Binding error: \\(error)")
                       })
    }

 

내부적으로 subscribe를 사용하고 있다.

그렇다면 다른 점은 무엇일까?

onNext만 처리하므로 에러처리가 불가능하다.

결국 onNext만 구현한 subscribe가 되는 것이다.

drive

/**
    Subscribes an element handler, a completion handler and disposed handler to an observable sequence.
    This method can be only called from `MainThread`.
    
    Error callback is not exposed because `Driver` can't error out.
    
    - parameter onNext: Action to invoke for each element in the observable sequence.
    - parameter onCompleted: Action to invoke upon graceful termination of the observable sequence.
    gracefully completed, errored, or if the generation is canceled by disposing subscription)
    - parameter onDisposed: Action to invoke upon any type of termination of sequence (if the sequence has
    gracefully completed, errored, or if the generation is canceled by disposing subscription)
    - returns: Subscription object used to unsubscribe from the observable sequence.
    */
    public func drive(
        onNext: ((Element) -> Void)? = nil,
        onCompleted: (() -> Void)? = nil,
        onDisposed: (() -> Void)? = nil
    ) -> Disposable {
        MainScheduler.ensureRunningOnMainThread(errorMessage: errorMessage)
        return self.asObservable().subscribe(onNext: onNext, onCompleted: onCompleted, onDisposed: onDisposed)
    }

 

파라미터로 onNext, onCompleted, onDisposed를 받고 각 이벤트에 대한 처리가 가능하다.

일단 에러처리가 불가능하단 사실을 알았다.

또 주의깊게 봐야할 부분은 MainScheduler.~ 쪽이다.

subscribe와 달리 메인스레드에서만 동작한다.

 

여기서 drive의 주의사항이 또 있다.

drive 메서드가 구현된 Driver+Subscription 파일을 보자.

 

extension SharedSequenceConvertibleType where SharingStrategy == DriverSharingStrategy { }

 

drive는 이 extension 내부에 구현되어 있다.

그럼 subscribe랑 bind는 어디에 구현되어 있는데 ?

ObservableType에 구현되어있다.

여기서 ObservableType이란 말 그대로 값 관찰이 가능한 타입을 뜻한다.

즉, 관찰 가능한 값들(ex. 버튼 클릭, 텍스트 필드 작성)들에 사용할 수 있는 메서드란 뜻이다.

그렇다면, drive는 ObservableType에 못쓰나 ?

못 쓴다.

코드를 보면 알 수 있듯이 SharingStrategy == DriverSharingStrategy 상황에서 쓸 수 있다고 한다.

 

DriverSharingStrategy 코드로 가보자 . .

 

public typealias Driver<Element> = SharedSequence<DriverSharingStrategy, Element>

public struct DriverSharingStrategy: SharingStrategyProtocol {
    public static var scheduler: SchedulerType { SharingScheduler.make() }
    public static func share<Element>(_ source: Observable<Element>) -> Observable<Element> {
        source.share(replay: 1, scope: .whileConnected)
    }
}

extension SharedSequenceConvertibleType where SharingStrategy == DriverSharingStrategy {
    /// Adds `asDriver` to `SharingSequence` with `DriverSharingStrategy`.
    public func asDriver() -> Driver<Element> {
        self.asSharedSequence()
    }
}

 

Driver<Element> 는 구조체 DriverSharingStrategy를 가진 Sequence 형태이다.

그러므로 생각해보면, drive()를 사용하기 위해선 지금 타입의 Strategy가 DriverSharingStrategy여야 하는데, 그러기 위해선 Driver<Element>로 구현되어 있어야 한다는 것이다.

그래서 보면 asDriver() 메서드가 있는데, 이 메서드가 Driver<Element>를 리턴해줌으로써 DriverSharingStrategy를 만족하게 된다.

Observable 시퀀스를 Driver trait으로 변경할 수 있을까 ?

asDriver()로 변경할 수 있다.

 

extension ObservableConvertibleType {
    /**
    Converts observable sequence to `Driver` trait.
    
    - parameter onErrorJustReturn: Element to return in case of error and after that complete the sequence.
    - returns: Driver trait.
    */
    public func asDriver(onErrorJustReturn: Element) -> Driver<Element> {
        let source = self
            .asObservable()
            .observe(on:DriverSharingStrategy.scheduler)
            .catchAndReturn(onErrorJustReturn)
        return Driver(source)
    }
    
    /**
    Converts observable sequence to `Driver` trait.
    
    - parameter onErrorDriveWith: Driver that continues to drive the sequence in case of error.
    - returns: Driver trait.
    */
    public func asDriver(onErrorDriveWith: Driver<Element>) -> Driver<Element> {
        let source = self
            .asObservable()
            .observe(on:DriverSharingStrategy.scheduler)
            .catch { _ in
                onErrorDriveWith.asObservable()
            }
        return Driver(source)
    }

    /**
    Converts observable sequence to `Driver` trait.
    
    - parameter onErrorRecover: Calculates driver that continues to drive the sequence in case of error.
    - returns: Driver trait.
    */
    public func asDriver(onErrorRecover: @escaping (_ error: Swift.Error) -> Driver<Element>) -> Driver<Element> {
        let source = self
            .asObservable()
            .observe(on:DriverSharingStrategy.scheduler)
            .catch { error in
                onErrorRecover(error).asObservable()
            }
        return Driver(source)
    }
}

 

Observable의 가장 큰 차이점은 Driver는 에러를 처리하지 않는다는 것과 메인 쓰레드에서 동작하는 것이라고 했다.

그러므로 Driver로 변환하는 과정에서 각각 에러를 처리해주고, DriverSharingStrategy.scheduler로 동작하게 함으로써 Driver로 변환해준다.

 

그렇다면 Subect와 Relay의 경우를 생각해보자.

Subject와 달리 Relay는 error나 complete를 발생시키지 않는다.

그러므로 에러를 방출하지 않는 Relay의 경우 .asDriver()로 변환할 수 있고, Subject의 경우 .asDriver(Error~)로 변환할 수 있다.

Driver는 RxCocoa에서 제공하는 Traits 중 하나라고 할 수 있다.

Traits는 직관적이고 작성하기 쉬운 코드를 작성하는데 도움이 되는 특수 클래스이다.

Driver trait는 에러를 방출하지 않고, 메인 쓰레드에서만 동작하며, 사이드 이펙트를 공유한다는 특징을 가진다.

drive()를 Driver trait만 정의할 수 있게 함으로써 drive()가 있다는 것은 해당 Observable sequence는 에러를 방출하지 않고, 메인 쓰레드에서 동작한다는 것을 의미한다.

그러므로 UI 요소들을 바인딩할 때 안전하게 사용할 수 있다.