이직 준비 때문에 한 동안 글이 없었네요ㅠㅠ 오늘부터 다시 차근차근 RxSwift를 정리해보려고 합니다!
다시 원래 컨셉으로...
이번 시간은 정말 유용하게 또 많이 사용되는 Subject와 Relay에 대해서 정리해보자. 결론부터 말하자면, Subject는 Observable이자 Observer다. Relay는 이런 Subject가 onCompleted, onError에 의해 이벤트 스트림이 종료되지 않도록 Wrapping된 Subject다. 차근차근 자세히 알아보자.
Observable은 다른 Observable을 구독하지 못한다. 마찬가지로 Observer도 다른 Observer에게 이벤트를 전달하지 못한다. 하지만 Subject는 다른 Observable에게 이벤트를 받아서 구독자에게 전달할 수 있다.
예를 들어 보자. 사용자의 아이디를 입력받고 해당 아이디가 DB에 있는지 확인한 뒤, 그 여부를 UI에 바인딩하는 경우를 살펴보자. 이 경우 비지니스 로직이 ViewModel안에 위치해야 하기 때문에 ViewModel안에서 DB의 Validatation 여부를 체크해야 하고 또한 여기서 방출되는 결과를 ViewController에서 다시 방출해야 한다. 이 경우 Observable과 Observer의 역할을 동시에 감당할 무엇이 필요하다.
class RxSwiftViewController: UIViewController {
var disposeBag: DisposeBag = DisposeBag()
let viewModel = ViewModel()
let UserIDTextField = UITextField()
func bindViewModel() {
UserIDTextField.rx.text.orEmpty
.bind(to: viewModel.inputUserId)
.disposed(by: disposeBag)
viewModel.resultInValidation
.bind(onNext: { result in
if !result {
self.errorAlertControllerShow()
}
})
.disposed(by: disposeBag)
}
}
class ViewModel {
var disposeBag = DisposeBag()
// input
let inputUserId = PublishSubject<String>()
//output
let resultInValidation = PublishSubject<Bool>()
func bindingInOutput() {
inputUserId
.map(checkUserIDFromDB(id:))
.bind(to: resultInValidation)
.disposed(by: disposeBag)
}
func checkUserIDFromDB(id: String) -> Bool {
return true
}
}
아래와 같이 Subject는 이벤트를 방출하기도, 이벤트를 받아 처리하기도 한다. 필자는 Subject를 ViewModel을 input, output으로 구성할 때 자주 사용했고, 어떤 값을 Rx적으로 저장할 때 많이 사용했다.
이런 Subject에는 4가지 종류가 있는데, 각 Subject마가 특징이 있으니 꼭 알아보고 써야 한다.
PublishSubject
Subject로 전달되는 새로운 이벤트를 구독자로 전달한다. 이 subject는 비어있는 상태로 생성된다. 바꿔말하면 subject가 생성되는 시점에는 내부에 아무런 이벤트가 저장되어 있지 않다. 그래서 생성 직후에 observer가 구독을 시작하면, 아무런 이벤트도 전달되지 않는다. 코드와 함께 알아보자.
Publish Subject는 subject로 전달되는 이벤트를 observer에게 전달하는 가장 기본적인 형태다.
let subject = PublishSubject<String>()
위와 같이 표현하면 문자열이 포함된 Next 이벤트를 받아서 다른 observer에게 전달할 수 있다. 하지만 막상 구독해보면 아무런 이벤트도 전달되지 않는다.
subject.onNext("Hello")
subject.subscribe { print(">>1", $0) }
o1.disposed(by: disposeBag)
// 출력되지 않음
이렇게 추가로 전달하면, 그제서야 전달된다. 이 부분이 PublishSubject의 특징이다. 그리고 onCompleted 이벤트를 전달하면, 그럼 모든 구독자에게 completed가 전달되고 더 이상 추가적인 이벤트를 전달하지 않는다. 이후에 구독하게 되는 이벤트들도 마찬가지다.
subject.onNext("RxSwift")
subject.onCompleted()
BehaviorSubject
생성시점에 초기값을 구현하고 구독시 최신 이벤트를 저장해두었다가 전달한다. PublishSubject와 Subject로서의 기능을 동일하다. 하지만 초기값을 설정한다는 점과 최신 이벤트를 저장해두고 구독시 전달한다는 점이 다르다. 코드와 함께 살펴보자.
BehaviorSubject를 생성할 때는 하나의 값을 전달한다.
let b = BehaviorSubject<Int>(value: 0)
BehaviorSubject를 구독하고 있는 Observer로 Next 이벤트가 전달되고 생성자로 전달한 값도 전달된다. BehaviorSubject를 생성하면 내부에 넥스트 이벤트가 하나 만들어지고 여기에는 생성자로 전달했던 값이 전달된다. 새로운 구독자가 추가되면 저장되어있는 Next 이벤트가 전달된다.
b.subscribe { print("BehaviorSubject1>>", $0) }
.disposed(by: disposeBag)
BehaviorSubject에 Next 이벤트를 전달하면 옵저버로 Next 이벤트가 전달된다.
b.onNext(1)
만약 이 시점에 새로운 옵저버가 추가되면 BehaviorSubject를 어떤 이벤트를 전달할까? 그렇다 1이 최근 값이므로 1이 전달된다.
b.subscribe { print("BehaviorSubject2>>", $0) }
.disposed(by: disposeBag)
마지막으로 onComplete이벤트가 전달되면 다른 PublishSubject와 마찬가지로 더 이상 이벤트를 전달하지 않는다.
ReplaySubject
하나 이상의 최신 이벤트를 버퍼에 저장하고 구독시 버퍼에 있는 모든 이벤트를 전달한다. BehaviorSubject 역할을 하되 두 개 이상의 이벤트를 저장하는데 사용하고 싶다면 이 Subject를 사용해보자. 코드와 함께 살펴보자.
ReplaySubject를 생성할 때는 bufferSize를 함께 설정해줘서 몇 개의 이벤트까지 저장할건지 설정해 준다.
let r = ReplaySubject<Int>.create(bufferSize: 3)
이렇게 하면 8, 9, 10이 buffer에 저장되어 차례로 방출된다.
(1...10).forEach { r.onNext($0) }
r.subscribe { print("Observer 1 >>", $0) }
.disposed(by: disposeBag)
만약 새로운 이벤트가 전달되면, 다른 Subject와 마찬가지로 즉시 구독자에게 전달된다.
// 11을 전달
r.onNext(11)
하지만 구독을 다시한 Observer에게는 최신의 3개의 값을 갱신해서 전달한다. 버퍼는 메모리에 저장되기 때문에 항상 메모리 사용량에 주의해야 한다. 필요이상으로 큰 버퍼 사용하지 말자!
// 9, 10, 11을 전달한다
r.subscribe { print("Observer 2 >>", $0) }
.disposed(by: disposeBag)
마지막으로 onCompleted, onError 이벤트를 전달하면 기존 이벤트에는 completed가 전달되지만, 새롭게 구독하는 Observer에게는 기존 버퍼에 담겨있는 녀석을 방출하고 종료된다.
AsyncSubject
위의 세 Subject는 구독되는 즉시 이벤트를 전달한다. 하지만 AsyncSubject는 Completed 이벤트가 전달되기 전까지 어떤 이벤트도 전달하지 않는다. Completed 이벤트가 전달되면, 즉시 최근 전달된 Next 이벤트 중 하나를 전달한다. 코드로 확인해보자.
AsyncSubject를 선언하고 이벤트를 전달해보자.
let as = AsyncSubject<Int>()
as.subscribe { print($0) }
.disposed(by: disposeBag)
as.onNext(1)
as.onNext(2)
as.onNext(3)
여기까지만 구현하면 아무 것도 전달되지 않는다. 하지만 onCompleted 이벤트를 전달하는 순간, 최근 이벤트 하나가 전달된다. 그리고 Completed 이벤트가 전달되고 종료된다. Error 이벤트가 전달되면 Next 이벤트가 전달되지 않고 Error 이벤트만 전달되고 종료한다.
as.onCompleted()
// next(3)
Relay는 Subject와 달리 Next 이벤트만 받고 completed와 error는 주고 받지 않는다. 그래서 앞에서 사용한 Subject와 달리 종료되지 않는다. 구독자가 dispose되기까지 계속 이벤트를 처리한다. 그래서 주로 종료없이 지속되는 UI 이벤트를 처리하는데 사용된다. Relay에 이벤트를 전달하기 위해서는 onNext()가 아니라 accept 메소드를 사용한다.
Relay는 RxCocoa에서 제공하는 기능이다. RxCocoa를 import해야 한다.
RxCocoa는 두 가지 Relay를 제공한다.
1. PublishRelay는 PublishSubject를 Wrapping한 것이고,
2. BehaviorRelay는 BehaviorSubject를 Wrapping한 것이다.
PublishRelay
빈 생성자로 PublishSubject와 동일하게 생성한다. 아래처럼 동일하게 구독하고 이벤트를 전달하면 된다.
let pr = PublishRelay<Int>()
pr.subscribe { print("1: \($0)") }
disposed(by: disposeBag)
pr.accept(1)
BehaviorRelay
BehaviorSubject와 마찬가지로 하나의 값을 생성자로 전달한다.
let br = BehaviorRelay<Int>(value: 0)
br.subscribe { print("1: \($0)") }
disposed(by: disposeBag)
br.accept(1)
BehaviorRelay의 특징! 중 하나가 value라는 속성에 접근해서 저장하고 있는 값을 리턴받을 수 있다.
읽기 전용이고 저장되어 있는 값을 바꾸지는 못한다. 새로운 값으로 바꾸고 싶다면 accept 메소드를 사용해 새로운 값을 전달하면 된다.
print(br.value)
// 1