개발/iOS

[RxSwift] .observe(on:) vs .subscribe(on:)

baerogramming 2025. 2. 19. 14:55

RxSwift를 사용하다 보면 Thread를 변경해 가며 작업을 해야 할 때가 있다.

 

특히 데이터를 가공한 후 이를 뷰에 반영할 때는 특히나 신경을 써줘야한다. (그렇지만 Drive를 쓴다면?)

 

오늘은 RxSwift에서 Thread를 변경하는 두가지 방법에 대해 정리해보려고 한다.

 


 

잠깐, 그 전에

이 녀석을 Operator라고 해야 할까 Method라고 해야 할까?

출처 - https://reactivex.io/documentation/operators

 

ReactiveX 공식 문서에서는 ObserveOn과 SubscriveOn을 Operator로 명시하고 있다.

 

간단하게 '데이터 스트림'에 영향을 준다면 Operator라고 구분하면 될 것 같다.

 

그럼 우선 ObserveOn부터 살펴보도록 하자.

 


 

.observe(on:)

Wraps the source sequence in order to run its observer callbacks on the specified scheduler
옵저버 콜백이 특정 스케줄러에서 실행되도록 감쌉니다.
* RxSwift 내에서는 Thread를 Schedular라고 칭한다.

 

Documentation에서는 위와 같이 설명하고 있다. 여기서 포인트는 '옵저버 콜백이 ~에서 실행되도록'이다.(어라 전부 아닌가)

 

다음 예시 코드를 보자.

output.totalNumber
    .compactMap { $0 }
    .map { "\($0) 개의 검색 결과" }
    .drive(resultCntLabel.rx.text)
    .disposed(by: disposeBag)

 

현재 코드에서는 Driver로 데이터를 받아오고 있기 때문에 별다른 Shedular 지정 없이 바인딩을 해주고 있다. 

 

그렇지만 Driver가 아닌 다른 Observable이고, 이전 작업이 Main Schedular가 아닌 곳에서 이뤄졌다면, 데이터 스트림 안에서는 앞단의 Schedular를 따르기 때문에 현재 상태를 명확히 알 수 없다.

 

이런 상태에서 UI를 업데이트한다면 당연히 

안녕? 나는 Thread Checker야. MainThread 밖에서 UI를 업데이트하는 친구들을 이놈 해주고 있어.

 

이 친구를 만나게 될 것이다.

 

이런 경우에(또는 반대로 Background에서 작업을 처리하고 싶을 때는)

output.totalNumber
    .compactMap { $0 }
    .map { "\($0) 개의 검색 결과" }
    .observe(on: MainSchedular.instance)
    .bind(with: self) { owner, 
    
    }
    (resultCntLabel.rx.text)
    .disposed(by: disposeBag)

 

위와 같이 ObserveOn Operator를 호출하고, 원하는 Schedular(*Main -> MainSchedular, Background -> ConcurrentDispatchQueueScheduler)를 Argument로 넘겨주면 된다.

 

Driver에 대한 설명도 없이 RxSwift에 대한 아티클을 남기려다 보니 어색한 부분이 있는 거 같다..

 

차차 Observable과 여러 가지 Traits들도 정리할 예정이니 좋댓구알.

 

Documentation을 더 보면, Discussion 항목에 다음과 같은 내용이 있다.

Discussion
This only invokes observer callbacks on a scheduler. In case the subscription and/or unsubscription actions have side-effects that require to be run on a scheduler, use subscribeOn.
이 메서드는 스케줄러에서 옵저버 콜백만을 호출합니다. 구독/구독 해제를 특정 스케줄러에서 해야 하는 경우 subscribeOn을 사용하세요.

 

위에서 언급했듯 '옵저버  콜백'만을 호출한다. 친절하게도 구독이나 구독 해제를 특정 스케줄러에서 해야 하는 '사이드 이펙트'를 가진 경우에는 SubscribeOn을 사용하라고 알려준다. 이렇게까지 명시한 거 보니 어지간하면 사용하지 말라는 뜻 같기도 하다.

 

그럼 SubscribeOn을 한번 보자.

 


 

.subscribe(on:)

Wraps the source sequence in order to run its subscription and unsubscription logic on the specified scheduler.
구독/구독 해제 로직이 특정 스케줄러에서 실행되도록 감쌉니다.

 

ObserveOn과는 다르게 구독과 구독 취소 작업이 원하는 스케줄러에서 실행되도록 한다고 되어있다. ObserveOn은 Observer가 데이터를 받는 스케줄러를 변경하는 것과는 다른 모습이다.

 

그럼 Discussion을 보도록 하자.

Discussion
This operation is not commonly used.
This only performs the side-effects of subscription and unsubscription on the specified scheduler.
In order to invoke observer callbacks on a scheduler, use observeOn.
이 메서드는 일반적으로 잘 사용되지 않습니다.
오직 '특정 스케줄러에서 구독/구독 해제'를 해야 할 경우에만 사용됩니다.
단순히 스케줄러에서 옵저버 콜백을 호출하려는 경우, observeOn을 사용하세요.

 

Operator Description과 같은 내용을 반복한다. 보통 경고문 같은 게 이렇게 반복적으로 나오지 않나??

 

예시를 찾아보면 보통 네트워크 요청등의 작업을 할 경우, Background에서 구독을 걸어놓고, 데이터를 받아오는 시점에서 ObserveOn으로 UI에 반영하는 패턴을 사용하는 거 같다.

 

그럼 이 외의 두 Operator의 특성을 간단하게 비교해 보자. 

 


 

호출 방법과 적용되는 범위에 대한 특성은 다음 표와 같다.

  ObserveOn SubscribeOn
호출 여러번 할 수 있음 최초 1회 이후에는 무시됨
적용 메서드를 호출한 이후의 코드들에 적용 호출 위치에 상관없이 스트림 전체에 적용

 

ObserveOn은 원하는 곳에서, 원하는 만큼 실행할 수 있다. 그렇기 때문에 위에서 언급한 예시와 같이 SubcribeOn으로 구독을 하고, ObserveOn으로 데이터를 반영하는 것이 가능하다.

 

또, 두 Operator 모두 옵저버 콜백을 호출할 스케줄러를 파라미터로 받고, 스트림으로부터 전달받은 Observable을 반환한다.

 

그럼 마지막으로 직관적인 예시 코드와 로그를 보고 마무리하겠다.

let test = Observable<Int>.create { value in
    value.onNext(1)
    value.onNext(2)
    value.onNext(3)
    value.onNext(4)
    value.onNext(5)
    value.onCompleted()
    return Disposables.create {
        print("실행 긋")
    }
}

test
    // 구독 시작을 백그라운드에서 처리
    .subscribe(on: ConcurrentDispatchQueueScheduler(qos: .default))
    .map {
        // 백그라운드에서 연산
        print("MAP OPERATOR", $0, Thread.current)
        return $0
    }
    // 결과는 메인스레드에서 반영
    .observe(on: MainScheduler.instance)
    .subscribe(onNext: { Int in
        // 구독
        print("SUBSCRIBE", Int, Thread.current)
    })
    .disposed(by: disposeBag)

 

간단하게 1부터 5까지의 수를 방출하는 Observable이고, 이에 대한 구독 시작을 Background에서, 로깅은 Main에서 처리하는 코드이다.

 

위의 코드를 실행하면 다음과 같은 로그를 확인할 수 있다.

 

MAP OPERATOR 1 <NSThread: 0x600001754380>{number = 6, name = (null)}
MAP OPERATOR 2 <NSThread: 0x600001754380>{number = 6, name = (null)}
MAP OPERATOR 3 <NSThread: 0x600001754380>{number = 6, name = (null)}
MAP OPERATOR 4 <NSThread: 0x600001754380>{number = 6, name = (null)}
MAP OPERATOR 5 <NSThread: 0x600001754380>{number = 6, name = (null)}
실행 긋
SUBSCRIBE 1 <_NSMainThread: 0x6000017040c0>{number = 1, name = main}
SUBSCRIBE 2 <_NSMainThread: 0x6000017040c0>{number = 1, name = main}
SUBSCRIBE 3 <_NSMainThread: 0x6000017040c0>{number = 1, name = main}
SUBSCRIBE 4 <_NSMainThread: 0x6000017040c0>{number = 1, name = main}
SUBSCRIBE 5 <_NSMainThread: 0x6000017040c0>{number = 1, name = main}

 

Map 연산을 Background에서 처리하고, 마무리 로그가 찍힌 후 Main에서 받아온 값을 로그로 보여주고 있다.

 

그렇다면 observeOn을 주석처리하면 어떻게 될까?

MAP OPERATOR 1 <NSThread: 0x600001754100>{number = 2, name = (null)}
SUBSCRIBE 1 <NSThread: 0x600001754100>{number = 2, name = (null)}
MAP OPERATOR 2 <NSThread: 0x600001754100>{number = 2, name = (null)}
SUBSCRIBE 2 <NSThread: 0x600001754100>{number = 2, name = (null)}
MAP OPERATOR 3 <NSThread: 0x600001754100>{number = 2, name = (null)}
SUBSCRIBE 3 <NSThread: 0x600001754100>{number = 2, name = (null)}
MAP OPERATOR 4 <NSThread: 0x600001754100>{number = 2, name = (null)}
SUBSCRIBE 4 <NSThread: 0x600001754100>{number = 2, name = (null)}
MAP OPERATOR 5 <NSThread: 0x600001754100>{number = 2, name = (null)}
SUBSCRIBE 5 <NSThread: 0x600001754100>{number = 2, name = (null)}
실행 긋

 

데이터가 방출될 때마다 반영을 반복하는 모습을 보이고 있다. 

 

만약 10개의 네트워크 응답을 가져오는 코드였다면? NASA의 크고 아름다운 천체 이미지를 가져오는 코드였다면??

 

뭐,, 별 수 있나 터지는 거지,,

 

우리 모두 Thread 안전 확인하고 천수 누리도록 합시다.

'개발 > iOS' 카테고리의 다른 글

[SwiftUI] ViewModifier를 이용한 Custom Modifier 활용  (1) 2025.04.17
[UIButton] .addTarget VS .addAction  (0) 2025.02.06