RxSwift - BehaviorSubject vs BehaviorRelay
안녕하세요. 그린입니다 🍏
이번 포스팅은 RxSwift에서 BehaviorSubject와 BehaviorRelay의 차이에 대해 학습해보겠습니다 🙋🏻
RxSwift를 사용하여 상태를 관리할 때 두 개념 모두 자주 사용되는데요.
두가지 모두 Subject입니다.
어떤 차이를 가지고 있고, 어떤 상황에서 어떤것을 쓰는게 좋을지에 대해 알아볼께요.
BehaviorSubject
BehaviorSubject는 현재 값을 유지하며, 새롭게 구독하는 Subscriber에게 항상 최신 값을 방출하는 Subject입니다.
또한 초기 및 기존 값이 존재하기 때문에 UI 상태 관리 시 유용하게 사용될 수 있죠.
특징으로는 다음과 같습니다.
1️⃣ 초기값 지정
2️⃣ 구독 시 최신 값을 즉시 수신
3️⃣ .onNext(value), .onError(error), onCompleted() 호출 가능
4️⃣ .error 혹은 .completed 시 추가적인 이벤트 방출 없음
한번 사용 예시를 간단히 볼까요?
import RxSwift
let disposeBag = DisposeBag()
let subject = BehaviorSubject(value: "초기값")
subject.subscribe { event in
print("Subscriber 1: \(event)")
}.disposed(by: disposeBag)
subject.onNext("새로운 값")
subject.subscribe { event in
print("Subscriber 2: \(event)")
}.disposed(by: disposeBag)
// Subscriber 1: next(초기값)
// Subscriber 1: next(새로운 값)
// Subscriber 2: next(새로운 값)
이렇게 Subscriber가 두개 존재하고 subject를 모두 옵저빙해요.
이때 출력 결과를 보면, Subscriber2가 구독 시 최신 존재하는 2의 값을 받는것이죠.
이와 비슷한 BehaviorRelay를 살펴볼까요?
BehaviorRelay
BehaviorRelay는 BehaviorSubject를 래핑한 형태로 RxCocoa에서 제공합니다.
큰 차이점으로는 .onError()와 .onCompleted()를 지원하지 않는다는 것이죠.
이 차이점은 UI 바인딩에 더 적합하도록 설계된것이에요.
그렇기에 RxCocoa에서 제공하겠죠!?
특징으로는 다음과 같아요.
1️⃣ .onError(), .onCompleted() 없음
2️⃣ 내부적으로 BehaviorSubject를 사용하지만, 구독이 종료되지 않음
3️⃣ .onNext(value) 대신, .accept(value)를 사용해 새 값 방출
4️⃣ UI 상태 관리를 위한 최적의 선택
사용 예시도 볼까요?
import RxSwift
import RxCocoa
let disposeBag = DisposeBag()
let relay = BehaviorRelay(value: "초기값")
relay.subscribe { event in
print("Subscriber 1: \(event)")
}.disposed(by: disposeBag)
relay.accept("새로운 값")
relay.subscribe { event in
print("Subscriber 2: \(event)")
}.disposed(by: disposeBag)
// Subscriber 1: next(초기값)
// Subscriber 1: next(새로운 값)
// Subscriber 2: next(새로운 값)
BehaviorSubject와 사용과 흐름이 동일하죠?
그럼 이걸 일목요연하게 정리해서 BehaviorSubject와 BehaviorRelay는 어떤 차이가 있을까요?
BehaviorSubject vs BehaviorRelay
특성 | BehaviorSubject | BehaviorRelay |
초기값 설정 | O | O |
최신값 제공 | O | O |
.onNext 호출 여부 | O | X (.accept 사용) |
.onError 지원 | O | X |
.onCompleted 지원 | O | X |
UI 바인딩 적합성 | ? (에러 시 종료) | O |
여기서 살펴볼것이 UI 바인딩 적합성인데요.
BehaviorSubject는 물음표로 두었어요.
MVVM에서 ViewModel의 데이터 바인딩 시 BehaviorRelay는 절대 종료되지 않는 스트림을 제공하기에 UI 바인딩에 매우 적합해요.
(왜냐하면, completed, errorr가 없어서 스트림이 강제 종료되지 않습니다.)
예를들어, 테이블 뷰 사용을 볼 수 있는데요.
BehaviorRelay로 보다 안전하게 값 업데이트가 가능합니다.
만약 BehaviorSubject를 사용해 한 번 에러가 발생하거나 완료되면 데이터를 갱신할 수 없는 상태가 될 위험이 있어요.
그래서, BehaviorSubject를 물음표로 두었어요.
반면, 다시 언급하지만 BehaviorRelay는 종료 개념 자체가 없기에 항상 안전하게 데이터 업데이트가 가능합니다.
// BehaviorSubject
let subject = BehaviorSubject(value: ["초기 데이터"])
// onError를 호출하면 이후 데이터 바인딩이 중단됨
subject.onError(NSError(domain: "테스트 오류", code: -1, userInfo: nil))
subject.subscribe(onNext: { data in
print("데이터 업데이트: \(data)")
}).disposed(by: disposeBag)
// 새 데이터 추가
subject.onNext(["새로운 데이터"]) // ❌ 에러로 인해 실행되지 않음
// BehaviorRelay
let relay = BehaviorRelay(value: ["초기 데이터"])
// 새로운 데이터 추가
relay.accept(["새로운 데이터"]) // ✅ 정상적으로 UI 업데이트됨
accept도 내부적으로 onNext를 호출하지만 onCompleted나 onError를 막아둔 안전한 방식을 취하고 있어요.
BehaviorRelay를 이용해 테이블 뷰와 아래와 같이 안전히 연결해주죠.
let dataRelay = BehaviorRelay(value: ["초기 데이터"])
// 테이블뷰와 바인딩
dataRelay.bind(to: tableView.rx.items(cellIdentifier: "cell")) { _, item, cell in
cell.textLabel?.text = item
}.disposed(by: disposeBag)
// 데이터 추가
dataRelay.accept(dataRelay.value + ["새로운 데이터"]) // ✅ UI 업데이트 정상 동작
결과적으로 테이블뷰의 데이터는 동적으로 변경되며, 새로운 데이터가 추가될 때 UI가 자동으로 업데이트되어야 하는것이 중요해요.
이때 BehaviorRelay는 스트림이 종료되지 않고, 값이 변경될 때만 UI를 갱신하기에 테이블뷰에서 사용하기가 적합하죠.
이 외에도 BehaviorRelay는 싱글턴 패턴과 결합해 앱 전역적인 상태를 관리할 때도 사용하기 유용할 수 있습니다.
그럼 정리해서 언제 어떤것을 적절히 사용하는게 좋을까요?
When use it?
여러가지 기준이 있을 수 있겠지만 크게 네가지로 잡아봤습니다.
1️⃣ UI 상태 관리 위주 - BehaviorRelay
2️⃣ 상태 변경 감지가 필요하지만, 에러 및 완료 이벤트는 불필요 - BehaviorRelay
3️⃣ 특정 시점에 Subject 종료되어야 함 - BehaviorSubject
4️⃣ Observable이 에러를 방출해야 함 - BehaviorSubject
이런것처럼 대부분의 UI 바인딩에서는 사실상 BehaviorRelay를 사용하면 되지만, 스트림을 종료해야 하거나 에러 및 완료 처리를 구분받아 이에 적절히 UI 처리를 해야한다면 BehaviorSubject를 사용하는것이 적절해보입니다 😃
마무리
두 가지 모두 상태를 관리하는 훌륭한 도구입니다.
이에 프로젝트의 요구사항에 맞춰 적절한 도구를 선택하는것이 개발자의 몫일것 같네요!
레퍼런스