본문 바로가기
iOS/RxSwift

[RxSwift] Subjects

by 0inn 2022. 10. 12.

지난 포스팅에서 Observable에 대해서 알아보았는데요 !

보통의 앱 개발에서 필요한 것은 실시간으로 Observable에 새로운 값을 수동으로 추가하고 subscriber에게 방출하는 방식입니다.

그러므로 Observable이자 Observer인 녀석이 필요하게 되는데 요 녀석을 Subject라고 합니다.

Subject의 종류

Subject = Observable + Observer 와 같이 행동합니다.

Subject는 .next 이벤트를 받고, 이런 이벤트를 수신할 때마다 subscriber에 방출합니다.

RxSwift에는 4가지 타입의 subject가 있습니다.

 

  1. PublishSubject : 빈 상태로 시작하여 새로운 값만을 subscriber에 방출
  2. BehaviorSubject : 하나의 초기값을 가진 상태로 시작하여 새로운 subscriber에게 초기값 또는 최신값을 방출
  3. RelaySubject : 버퍼를 두고 초기화하며 버퍼 사이즈만큼의 값들을 유지하면서 새로운 subscriber에게 방출
  4. Variable : BehaviorSubject를 래핑하고 현재의 값을 상태로 보존합니다. 가장 최신 / 초기 값만을 방출

PublishSubjects

PublishSubject는 구독된 순간 새로운 이벤트 수신을 알리고 싶을 때 용이합니다.

이런 활동은 구독을 멈추거나 .completed, .error 이벤트를 통해 Subject가 완전 종료될 때까지 지속됩니다.

 

 

첫 번째 줄은 subject를 배포한 것으로 두 번째 줄과 세 번째 줄이 subscriber들입니다.

아래로 향하는 화살표들은 이벤트의 방출, 위로 향하는 화살표들은 구독을 선언하는 것을 의미합니다.

첫 번째 subscriber는 1 다음에 구독합니다. 그러므로 1 이벤트는 받지 못하고, 2와 3을 받게 됩니다.

두 번째 subscriber는 같은 원리로 3만 받습니다.

 

코드로 예시를 살펴볼까요 ? 

 example(of: "PublishSubject") {
     let subject = PublishSubject<String>()
     subject.onNext("Is anyone listening?")

     let subscriptionOne = subject
         .subscribe(onNext: { (string) in
             print(string)
         })
     subject.on(.next("1"))
     subject.onNext("2")

     // 1 두 번째 subscriber를 추가
     let subscriptionTwo = subject
         .subscribe({ (event) in
         	// .onNext 이벤트와 방출되므로 nil 확인 필수
             print("2)", event.element ?? event)
         })

     // 2 subject에 3 추가 -> subscriptionOne과 subscriptionTwo에서 총 두 번 출력
     subject.onNext("3")

     // 3 subscriptionOne을 dispose하고 subject에 4 추가
     // -> 출력 : 2) 3 & 2) 4
     subscriptionOne.dispose()
     subject.onNext("4")

     // 4 이벤트 종료 
     // -> 출력 : 2) completed
     subject.onCompleted()

     // 5 이미 종료되었으므로 출력 및 인쇄 X
     subject.onNext("5")

     // 6
     subscriptionTwo.dispose()

     let disposeBag = DisposeBag()

     // 7 subject 완전 종류 후 새로운 subscriber가 생긴다고 다시 subject 작동 X
     // -> 출력 : 3) completed
     subject
         .subscribe {
             print("3)", $0.element ?? $0)
     }
         .disposed(by: disposeBag)

     subject.onNext("?")
 }

 

언제 사용할까 ?

시간에 민감한 데이터를 모델링할 때 ! !

 

BehaviorSubjects

BehaviorSubject는 마지막 .next 이벤트를 새로운 구독자에게 반복한다는 점만 뺀다면 PublishSubject와 유사합니다.

 

 

PublishSubject와 다르게 첫 번째 이벤트가 발생한 후 첫 번째 구독자가 구독을 시작했음에도 직전의 값 1을 받습니다.

두 번째 구독자 역시 직전의 값 2을 받습니다.

 

역시 코드로 예시를 살펴봅시다 ~

 // 1
 enum MyError: Error {
     case anError
 }

 // 2 
 // 이벤트 내 값이 있다면 값 출력하고 에러가 있다면 에러를 nil이라면 event만 출력
 func print<T: CustomStringConvertible>(label: String, event: Event<T>) {
     print(label, event.element ?? event.error ?? event)
 }

 // 3 
 // BehaviorSubject는 초기값을 입력하여 생성
 example(of: "BehaviorSubject") {

     // 4
     // BehaviorSubject는 항상 최신의 값만 방출하기 때문에 초기값 없이 만들 수 없음
	 // 만약 초기값을 줄 수 없는 경우에는 PublishSubject 사용해야 함
     let subject = BehaviorSubject(value: "Initial value")
     let disposeBag = DisposeBag()
 }
 
    // 6
     subject.onNext("X")
 
     // 5
     // subject 구독 후 dispose
     // -> 1) Initial value
     // 6을 입력하면 구독 전 최신값이 "X"로 바뀌므로 출력 값도 바뀜
     // -> 1) X 
     subject
         .subscribe{
             print(label: "1)", event: $0)
         }
         .disposed(by: disposeBag)

     // 7
     subject.onError(MyError.anError)

     // 8 
     // -> 1) anError 2) anError
     subject
         .subscribe {
             print(label: "2)", event: $0)
         }
         .disposed(by: disposeBag)
 }

 

언제 사용할까 ?

뷰를 가장 최신의 데이터로 미리 채울 때 ! !

예를 들어 유저 프로필 화면의 컨트롤을 BehaviorSubject에 바인드할 수 있는데 이렇게 하면 앱이 새로운 데이터를 가져오는 동안 최신 값을 사용해 화면을 미리 그려놓을 수 있겠죠 ~

 

RelaySubjects

RelaySubject는 생성시 선택한 특정 크기까지 방출하는 최신 요소를 일시적으로 캐시하거나 버퍼합니다.

그 다음 해당 버퍼를 새 구독자에게 방출합니다.

 

 

첫 번째 줄이 subject, 아래에 있는 애들이 구독자들입니다.

여기서 subejct의 버퍼 사이즈는 2입니다.

첫 번째 구독자는 subject와 함께 구독하므로 subject의 값들을 그대로 갖습니다. 

두 번째 구독자는 subject가 두 개의 이벤트를 받은 후 구독하였지만 버퍼 사이즈 2만큼의 값을 받을 수 있습니다.

 

여기서 중요한 점 !

RelaySubject에서 사용되는 버퍼들은 메모리를 가지고 있다는 점입니다.

이미지나 arrray 같이 메모리를 크게 차지하는 값들은 메모리에 엄청난 부하를 주겠죠 ㅠ

 

코드로 살펴볼게요 ~

 example(of: "ReplaySubject") {

     // 1 
     // 버퍼 사이즈 2 가지는 RelaySubject 생성
     let subject = ReplaySubject<String>.create(bufferSize: 2)
     let disposeBag = DisposeBag()

     // 2
     // 1, 2, 3 세 개의 요소들 subject에 추가
     subject.onNext("1")
     subject.onNext("2")
     subject.onNext("3")

     /* 3
        해당 subject에 대한 두 개의 구독자 생성
        -> 1) 2 
     	   1) 3 
           2) 2 
           2) 3
     */
     subject
         .subscribe {
             print(label: "1)", event: $0)
         }
         .disposed(by: disposeBag)

     subject
         .subscribe {
             print(label: "2)", event: $0)
         }
         .disposed(by: disposeBag)
         
     subject.onNext("4")
     
     subject.onError(MyError.anError)

     subject.subscribe {
         print(label: "3)", event: $0)
         }
         .disposed(by: disposeBag)

	/* Prints:
  	1) 4
 	2) 4
    -> 4가 추가되기 전 이미 구독중이기 때문에 4만 출력
  	1) anError
  	2) anError
  	3) 3
  	3) 4
    -> 새로운 구독자이므로 마지막 2개의 요소 출력
  	3) anError
 	*/
    
	 // 에러 이벤트만 받게 됨
     subject.dispose()
   
 }

 

언제 사용할까 ?

BehaviorSubject처럼 최근의 값 외에 더 많은 것을 보여주고 싶을 때 ! !

예를 들어 검색창과 같이 최근 5개의 검색어를 보여주고 싶을 때 사용하면 좋겠네요 ..

 

Variables

Variable은 BehaviorSubject를 래핑하고 이들의 현재값을 State로 가집니다. 이 때, 현재값은 value 프로퍼티로 알 수 있습니다.

value 프로퍼티를 Variable의 새로운 요소로 가지기 위해선 일반적인 subject나 observable과는 다른 방법으로 추가해야 합니다. 즉, onNext(_:)를 사용할 수 없습니다.

또 하나의 특성은 에러가 발생하지 않을 것임을 보증하기 때문에 .error 이벤트를 variable에 추가할 수 없습니다.

variable은 할당 해제되었을 때 자동적으로 완료되기 때문에 수동적으로 .completed를 할 필요 또한 없습니다.

 

코드로 살펴봅시다 . . !

 example(of: "Variable") {

     // 1 
     // 초기값 가지는 variable 생성
     let variable = Variable("Initial value")
     let disposeBag = DisposeBag()

     // 2
     // value 추가
     variable.value = "New initial value"

     // 3
     // 구독하기
     variable.asObservable()
         .subscribe {
             print(label: "1)", event: $0)
         }
         .disposed(by: disposeBag)

     /* Prints:
      1) New initial value
     */  
     
     // 4
     variable.value = "1"

     // 5
     variable.asObservable()
         .subscribe {
             print(label: "2)", event: $0)
         }
         .disposed(by: disposeBag)

     // 6
     variable.value = "2"

     /* Prints:
      1) 1
      2) 1
      1) 2
      2) 2
     */
     
     // .error 나 .completed 이벤트 추가 불가능
 }

 

언제 사용할까 ?

variable은 유동적이므로 observable처럼 구독할 수 있고, subject처럼 새로운 .next 이벤트를 받을 때마다 반응하도록 구독할 수 있습니다.

또한, 업데이트 구독없이 그냥 현재값을 확인하고 싶을 때 일회성으로 적용될 수 있습니다 . . !

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

[RxSwift] Combining Operators  (0) 2022.11.18
[RxSwift] Transforming Operators  (0) 2022.11.17
[RxSwift] Filtering Operators  (0) 2022.10.12
[RxSwift] Observable  (0) 2022.10.11
[RxSwift] Hello, RxSwift !  (1) 2022.10.11