
RxSwift 와 Combine 을 하면서 공부는 해봤지만 실제 코드에는 적용하기 어려웠을 두 개의 operator 를 이번에 사용해보게 되면서 포스팅을 하기로 마음 먹었습니다. 이미 수많은 글에 이와 관련된 글이 많지만, 제가 직접 포스팅해보고 싶어서 하게 됐습니다.
결론부터 이야기하자면...
어떤 걸로 시험을 해볼까 하다가, 그냥 간단하고 언제나 사용하는 랜덤 숫자 뽑기 기능으로 비교를 해봤습니다.
로 먼저 진행을 하겠습니다.
먼저 Output 으로 받는 그 쓰레드 자체가 어딘지 궁금했습니다.
왜냐하면 보통 쓰레드 관련된 고민은 보통 Input 과 Output 의 쓰레드이기 때문이죠. 그 중간의 과정은 알아서 해주겠지 라는 생각이 아무래도 크죠. (다른 머리 아픈것도 많기 때문에...)
struct ThreadCheckView: View {
@State var number: Int = 0
private let generator = RandomNumberGenerator()
var body: some View {
VStack {
Button("Generate") {
generator.generateNewNumber()
}
.padding()
Text("\(number)")
.onReceive(generator.subject, perform: { data in
number = data
})
Button("Complete") {
generator.completeGenerator()
}
}
}
}
final class RandomNumberGenerator {
private var subscriptions = Set<AnyCancellable>()
let subject = PassthroughSubject<Int, Never>()
init() {
subject
.subscribe(on: DispatchQueue.global())
.sink { randomNumber in
print("🔥 \(randomNumber) has been generated. With thread of \(Thread.current)")
}
.store(in: &subscriptions)
}
func generateNewNumber() {
subject.send(Int.random(in: -10...10))
}
func completeGenerator() {
subject.send(completion: .finished)
}
}


결과를 보실까요??
우리는 subscribe 를 Main Thread 가 아닌 곳에서 했지만?
Receive 된 Thread 를 보면 Main Thread 이죠.
어라라!
이래서 공식 다큐에서 정의를 살펴보면...

Output 에 관련된 게 아니라, upstream 의 operation 에 관련된 것이라고 써져 있습니다.
⭐️ 아하 그렇다면 receive 를 할때의 operation 과는 관련이 없구나! 라는 걸 알 수 있죠
그렇다면? 내부에서 벌어지는 일들에 대해서 조금 더 뜯어봅시다. 과연 이 말처럼 중간 과정의 upstream 친구들은 이 말처럼 다른 쓰레드에서 돌아갈까요?
handleEvents() 메서드를 사용해보시죠
final class RandomNumberGenerator {
private var subscriptions = Set<AnyCancellable>()
private let subscriber: AnySubscriber<Int, Never>
let subject = PassthroughSubject<Int, Never>()
init() {
subscriber = AnySubscriber(receiveSubscription: { subscription in
print("------------------------\n📬 Subscription has been received. - Thread of \(Thread.current)")
subscription.request(.unlimited)
}, receiveValue: { value in
print("☀️ \(value) has been received. - Thread of \(Thread.current)")
return .none
}, receiveCompletion: { _ in
print("🖌️ Completed - Thread of \(Thread.current)\n-------------")
})
subject
.handleEvents(receiveSubscription: { _ in
print("-: Received Subscription - Thread of \(Thread.current)")
}, receiveOutput: { _ in
print("-:: Received Output - Thread of \(Thread.current)")
}, receiveCompletion: { _ in
print("-::: Received Completion - Thread of \(Thread.current)")
}, receiveCancel: {
print("-:::: Cancelled - Thread of \(Thread.current)")
}, receiveRequest: { _ in
print("-::::: Received Request - Thread of \(Thread.current)")
})
.subscribe(on: DispatchQueue.global())
.subscribe(subscriber)
}
func generateNewNumber() {
print("\n\(#function) called. Thread of \(Thread.current)")
subject.send(Int.random(in: -10...10))
}
func completeGenerator() {
print("\n\(#function) called. Thread of \(Thread.current)")
subject.send(completion: .finished)
}
}
를 추적해보는 거죠.
그럼 결과는 ?

보시면 Complete 와 Output 을 받았을 때를 제외하곤 모두 Main 쓰레드가 아닌 다른 쓰레드에서 실행된다는 점을 볼 수 있죠.
여기서 중요한 점은 그러면 왜? 다른 downstream 은 Main 쓰레드에서 실행되죠?
그건 우리가 SwiftUI 의 View 내부에서 이 함수를 실행시키기 때문이죠.
이 SwiftUI 의 View 는 @MainActor 이기 때문에 기본적으로 Main 쓰레드에서 실행이 되죠!
오 좋아요 어느정도 이해가 되셨죠?
이제는 그러면 Cancel 일 때는 어떤지도 해봅시다.
func cancelSomeCancellable() {
let someCancellable = [20].publisher
.handleEvents(receiveRequest: { _ in
print("Received Cancel - \(Thread.current)")
})
.delay(for: 50, scheduler: DispatchQueue.main)
.subscribe(on: DispatchQueue.global())
.sink { someValue in
print("Value: \(someValue)")
}
DispatchQueue.main.asyncAfter(deadline: .now()+2) {
someCancellable.cancel()
}
}
간단하게 함수를 View 내부에 넣어봤습니다.
2초 뒤에 cancel 을 시켜버렸는데요.


네~ Cancel Operation 도 subscribe 에서 정한 쓰레드에서 발생하는 걸 볼 수 있죠!
그럼면 이제 receive(on:) 에 대해서 알아보죠!

-> "receive elements from publisher" 라고 되어 있습니다. 바로 Output 과 complete 입니다. 아마 failure() 도 똑같이 여기서 적용될 겁니다.
기존의 함수에서 subscribe(on:) 을 receive(on:) 을 바꿔보죠. 그리고 실행을 해보겠습니다. 결과는??


이제 Subscription 받는 것, Request 를 받는 것 등 upstream 에 관련된 것들은 모두 Main 쓰레드에서 실행되고,
우리가 원한 Output 과 Complete 는 다른 쓰레드에서 발생하는 것을 볼 수 있죠.
그러면 Cancel 은 어떨까요?


upstream 의 operation 이라는 걸 바로 알려줍니다. Main 쓰레드에서 실행이 됐군요!
그러면 이제 초기값이 있는 Subject 로 이어나가보겠습니다.

이렇게 Subject 의 종류를 바꿨구요.
각각 subscribe(on:), receive(on:) 에 각각 DispatchQueue.global 을 적용했습니다.


차이가 보이시나요??
기본적으로 위에서 본 대로 똑같이 작동되지만!
첫 초기값의 쓰레드는 조금 다르게 실행되는 걸 볼 수 있습니다.
Subscription 의 Thread 와 같은 Thread 에서 실행되는 것 같죠?
아마도..!
처음 Subscription 이 전달될 때 같은 쓰레드로 전달이 되는 거 같다.
(이거 한번 더 찾아봐서 포스팅을 써봐야겠다..!)
암튼 이렇게 해서 포스팅을 마무리합니다...
생각보다 이렇게 뜯어보는 데에 더 재미를 느끼고 있네요!
재밌따!
그리고 틀린 점 지적은 항상 환영합니다! 미리 감사합니다! 😊