Danny의 iOS 컨닝페이퍼
article thumbnail
Published 2023. 3. 9. 16:24
[iOS/RxSwift] Subject의 종류 RxSwift

Subject

Subject들은 특별하게도

Observable로서 데이터를 방출도 하면서,

Observer로서 데이터를 구독할 수도 있습니다.

 

이러한 특징으로,

Subject는 Observable이 방출하는 데이터의 흐름을 제어할 수 있는 장점이 있습니다.

(예를 들어 Observable이 각각의 이벤트를 방출할 때마다 UI 업데이트하게 되면, 성능이 저하될 수도 있습니다.

특정 조건에만 구현되도록 처리해 주면 이러한 단점을 해결할 수 있습니다.)

 

또한 여러 Observer들이 서로 데이터를 공유할 수 있습니다.

즉, Hot Observable과 같이 동작하죠.

 

참고로 Subject들도 Observable과 마찬가지로 Disposable을 리턴합니다.

즉, 일회성을 갖고 있다는 것이죠.

(Completed 또는 Error 발생 시 dispose가 되므로 새로 구독해야 합니다.)

 

Subject 배우기 앞서 이해를 돕기 위해  

[iOS/RxSwift] Observable, Subscribe 개념 및 사용 방법

[iOS/RxSwift] Observable 연산자

[iOS/RxSwift] Hot & Cold Observable 를 먼저 참고해 주세요.

 

 

PublishSubject

새로운 데이터가 발생했을 때, 해당 데이터만을 구독하는

가장 단순한 형태의 Subject입니다.

 

즉, 구독한 시점부터 이후의 이벤트만 전달합니다!

 

생성 방법은 다음과 같습니다.

// 방출할 데이터 타입을 제너릭으로 표현해줘야 한다!
var publishSubject = PublishSubject<Any>()

 

데이터 전달

기존의 Observable에서의 사용한 이벤트 처리와 동일합니다.

// 이렇게 전달된 데이터는 구독하고 있는 모든 Observer들에게 전달됩니다.
publishSubject.onNext("Hello")
publishSubject.onCompleted()
publishSubject.onError(SomeError.err)

 

구독 방법

기존의 Observable과 동일합니다.

publishSubject.subscribe(onNext: { element in
    print(element)
}).disposed(by: disposeBag)

 

바로 사용해 볼까요?

var publishSubject = PublishSubject<Int>()

publishSubject.onNext(1)
publishSubject.onNext(2)
publishSubject.onNext(3)
publishSubject.onNext(4)

publishSubject.subscribe(onNext: { element in
    print("Observer received value: \(element)")
}).disposed(by: disposeBag)

//

출력 결과가 없네요...

 

그 이유는

PublishSubject는 Hot Observable로서 구독한 이후의 시점부터

방출한 이벤트를 받게 되므로 값이 없는 것이죠!

 

간단하게 Marble 그림으로 이해해 봅시다.

 

Subject의 이벤트 스트림은 이미 진행 중입니다.

즉, 이미 onNext(4)까지 진행된 후 구독을 한 것이므로

아래와 그림같이 어떠한 값도 못 받게 되는 겁니다.

 

그렇다면 구독 순서를 처음으로 바꿔봅시다.

publishSubject.subscribe(onNext: { element in
    print("Observer received value: \(element)")
}).disposed(by: disposeBag)

publishSubject.onNext(1)
publishSubject.onNext(2)
publishSubject.onNext(3)
publishSubject.onNext(4)

// Observer received value: 1
// Observer received value: 2
// Observer received value: 3
// Observer received value: 4

출력이 되고 있네요!

 

이해를 돕기 위해 여기서도 Marble로 나타내자면, 

이와 같이 나타낼 수 있겠죠!

 

간단한 사용 예시

12:00pm에 시작하는 추첨 방송이 있습니다.

앱에서는 시작 10분 전에 알림 문자를 유저에게 보내줍니다.

고객 1은 11:30분에 가입을 해서 11:50에 알림 문자를 받았고,

고객 2는 12:10분에 가입을 했는데 "추첨 10분 전입니다."라는 알람을 받게 되면 문제가 생길 겁니다.

 

이렇게 가입을 구독(subscribe)이라고 생각해 본다면,

PublishSubject 사용 이유에 대해 알 수 있을 겁니다.

 

 

위의 Subject 특징에서

여러 Observer은 서로 데이터를 공유를 한다고 했습니다.

 

어떻게 공유가 되는지 확인을 해봅시다.

var subject = PublishSubject<Int>()

// observer(이벤트 동작)를 만들어 줌
let observer1: (Int) -> Void = { value in
    print("Observer 1 received value: \(value)")
}

let observer2: (Int) -> Void = { value in
    print("Observer 2 received value: \(value)")
}

// 랜덤 숫자를 생성
let randomNum1 = Int.random(in: 1...100)
let randomNum2 = Int.random(in: 1...100)

// 구독 1
subject.subscribe(onNext: observer1)
    .disposed(by: disposeBag)

// 이벤트 1 방출
subject.onNext(randomNum1)

// 구독 2
subject.subscribe(onNext: observer2)
    .disposed(by: disposeBag)

// 이벤트 2 방출
subject.onNext(randomNum2)

// - 이벤트 1 방출 -
//Observer 1 received value: 81
// - 이벤트 2 방출 -
//Observer 1 received value: 23
//Observer 2 received value: 23

// Observer 1, Observer 2의 방출된 이벤트 값이 서로 같은걸 확인할 수 있습니다.
// 이와 같이 서로 같은 데이터를 공유하네요.

 

 

BehaviorSubject

가장 최근(마지막)에 방출 이벤트를 기억하고 있습니다.

새로운 Observer가 구독을 시작하면 이전에 방출된 가장 최신 이벤트를 바로 전달합니다.

 

이전 값을 사용해야 하므로,

BehaviorSubject을 생성할 때는 초기값을 필수로 갖고 있어야 합니다.

 

말로는 이해가 잘 안 되니

Marble 그림으로 이해해 봅시다.

 

생성 방법은 다음과 같습니다.

PublishSubject와 비슷하지만 초기값을 지정해 줘야 합니다.

var behaviorSubject = BehaviorSubject<Any>(value: 0)

 

구독을 해보면

이와 같이 방출한 초기값 "0"을 갖고 있는 것을 확인할 수 있습니다.

behaviorSubject.subscribe(onNext: { element in
    print("Observer 1, received value: \(element)")
}).disposed(by: disposeBag)

// Observer 1, received value: 0

 

새로운 이벤트를 추가해 봅시다.

PublishSubject와 동일하게 추가한 이벤트도 방출이 되네요.

behaviorSubject.onNext(1)
behaviorSubject.onNext(2)

// Observer 1, received value: 1
// Observer 1, received value: 2

 

다시 구독을 해보면,

위의 가장 마지막(최신) 값을 전달하는 걸 확인할 수 있습니다.

behaviorSubject.subscribe(onNext: { element in
    print("Observer 2, received value: \(element)")
})

// Observer 2, received value: 2

 

 

정리를 해보자면

BehaviorSubject는 이전값과 현재값 모두를 알 수 있으므로,

현재의 상태를 저장하고 관찰할 수 있는 곳에서 사용이 적합합니다.

 

사용 예시를 들어보자면

유저 정보가 변경될 때마다

다른 부분에서도 그 변경된 정보를 즉시 반영하고 싶을 때 사용할 수 있습니다.

BehaviorSubject는 가장 마지막(최신) 값을 저장하기 때문에 

다른 부분에서 구독을 하게 되면 마지막 값을 갖고 즉시 업데이트가 가능한 것이죠.

 

 

ReplaySubject

Replay 이름 그대로 "다시 반복"을 할 수 있습니다.

 

이전에 방출한 이벤트들을 버퍼에 저장 후 다음 구독 시

버퍼의 크기만큼 이벤트를 다시 재생할 수 있는 기능입니다.

버퍼(buffer) : Subject가 방출한 이전 이벤트들을 저장해 두는 공간

 

이것도 Marble로 먼저 보시죠.

ReplaySubject는 생성 시점에서 버퍼의 크기를 지정해 줄 수 있습니다.

 

위의 그림과 같이 버퍼의 크기(bufferSize)를 2로 지정해 준 뒤,

구독을 하게 되면 이전의 방출된 2개의 이벤트를 갖고 시작하게 되죠.

 

바로 생성 방법을 알아봅시다.

 

다른 Subject와는 다르게 ReplaySubject에서는

create로 생성해 주고 사용할 버퍼의 크기를 지정해 줍니다.

var replaySubject = ReplaySubject<Int>.create(bufferSize: 2)

 

구독 및 이벤트를 방출해 보겠습니다.

새로 구독 시 버퍼의 크기만큼 이전의 두 개의 이벤트를 갖고 오는 것을 확인할 수 있습니다.

// 구독 1
replaySubject.subscribe(onNext: { element in
    print("Observer 1, received value: \(element)")
}).disposed(by: disposeBag)

// 이벤트 방출
replaySubject.onNext(1)
replaySubject.onNext(2)

// 구독 2
replaySubject.subscribe(onNext: { element in
    print("Observer 2, received value: \(element)")
}).disposed(by: disposeBag)

// 이벤트 방출
replaySubject.onNext(3)
replaySubject.onNext(4)

// 구독 3
replaySubject.subscribe(onNext: { element in
    print("Observer 3, received value: \(element)")
}).disposed(by: disposeBag)

// 구독 1
// Observer 1, received value: 1
// Observer 1, received value: 2

// 구독 2
// Observer 2, received value: 1
// Observer 2, received value: 2
// Observer 1, received value: 3
// Observer 2, received value: 3
// Observer 1, received value: 4
// Observer 2, received value: 4

// 구독 3
// Observer 3, received value: 3
// Observer 3, received value: 4

알아보기 쉽게 출력 순서를 조금 바꾼다면

위의 Marble과 같은 출력 값을 갖는 것을 확인할 수 있습니다.

// Observer 1, received value: 1
// Observer 1, received value: 2
// Observer 1, received value: 3
// Observer 1, received value: 4

// Observer 2, received value: 1
// Observer 2, received value: 2
// Observer 2, received value: 3
// Observer 2, received value: 4

// Observer 3, received value: 3
// Observer 3, received value: 4

 

사용 시 주의점이 있습니다.

 

보통 onCompleted나 onError가 발생하면 자동으로 dispose가 호출됩니다.

 

하지만 ReplaySubject에서는 dispose가 발생하더라도,

새로 구독하게 되면 이전 버퍼값을 구독자에게 넘겨주게 됩니다.

(주의! onCompleted, onError 발생 후,

새로 구독하더라도 다음 onNext는 더 이상 전달하지 않습니다.)

var replaySubject = ReplaySubject<Int>.create(bufferSize: 2)

replaySubject
    .debug()
    .subscribe(onNext: { element in
    print("Observer 1, received value: \(element)")
}).disposed(by: disposeBag)

replaySubject.onNext(1)
replaySubject.onNext(2)
replaySubject.onCompleted()

replaySubject.subscribe(onNext: { element in
    print("Observer 2, received value: \(element)")
}).disposed(by: disposeBag)

// 이벤트를 방출해도 더 이상 전달되지 않는다. 
replaySubject.onNext(3)
replaySubject.onNext(4)

// 구독 1
// Subjects: (__lldb_expr_322) -> subscribed
// Subjects: (__lldb_expr_322) -> Event next(1)
// Observer 1, received value: 1
// Subjects: (__lldb_expr_322) -> Event next(2)
// Observer 1, received value: 2
// Subjects: (__lldb_expr_322) -> Event completed
// Subjects: (__lldb_expr_322) -> isDisposed

// 구독 2
// Observer 2, received value: 1
// Observer 2, received value: 2

DisposeBag을 사용하면 뷰컨트롤러가 해제될 때, 자동 해제되지만, 

 

만약 내부에서 버퍼를 지우고 싶다면

새로운 ReplaySubject을 생성해 기존의 ReplaySubject를 대체하거나,

새로 ReplaySubject을 생성해 주는 방법 등을 고려해 볼 수 있습니다.

(아쉽게도 직접 버퍼를 관리하는 메서드는 없는 것 같습니다.)

var replaySubject = ReplaySubject<Int>.create(bufferSize: 2)

replaySubject
    .debug()
    .subscribe(onNext: { element in
    print("Observer 1, received value: \(element)")
}).disposed(by: disposeBag)

replaySubject.onNext(1)
replaySubject.onNext(2)

// 새로운 ReplaySubject로 대체
replaySubject = ReplaySubject<Int>.create(bufferSize: 2)

replaySubject.subscribe(onNext: { element in
    print("Observer 2, received value: \(element)")
}).disposed(by: disposeBag)

replaySubject.onNext(3)
replaySubject.onNext(4)

// Subjects: (__lldb_expr_334) -> subscribed
// Subjects: (__lldb_expr_334) -> Event next(1)
// Observer 1, received value: 1
// Subjects: (__lldb_expr_334) -> Event next(2)
// Observer 1, received value: 2

 

또한, ReplaySubject 생성할 때는 버퍼 크기에 유의하여 설계를 해야 합니다.

(버퍼를 지나치게 크게 설정하면 메모리 문제가 발생할 수 있습니다.)

 

사용 예시로는

버퍼의 크기만큼(2개 이상) 최근에 방출된 이벤트를 어디서 사용할 수 있을까요?

검색창과 같이, 10개의 최근 검색어를 더 보여 주고 싶을 경우 사용할 수 있을 것 같네요.

 

 

참고

RxSwift Study - Github

 

 

반응형
profile

Danny의 iOS 컨닝페이퍼

@Danny's iOS

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!