RxSwift

[RxSwift] Subject, Relay

yujaehui 2024. 5. 29. 16:00

1. Subject

SubjectObservableObserver의 역할을 동시에 수행할 수 있는 특수한 객체.

이를 통해 Subject는 구독을 통해 값을 방출하고, 외부에서 값을 받아 새로운 이벤트를 방출할 수 있음.

1-1. PublishSubject

PublishSubject 특징

  • 초기에는 아무런 값도 방출되지 않기 때문에 구독 전에 발생한 값은 무시.
  • 구독자가 구독한 시점 이후에 발생하는 next 이벤트만 전달.
  • 구독자가 있는 동안 completed 또는 error 이벤트가 발생하면 모든 구독자에게 전달.
  • 구독이 종료되거나 completed, error 이벤트가 발생하면 더 이상 이벤트가 방출되지 않음.

PublishSubject 예시

import RxSwift

func examplePublishSubject() {
    let subject = PublishSubject<String>()
    let disposeBag = DisposeBag()

    // 구독자 없이 값 방출 (출력 없음)
    subject.onNext("Is anyone listening?")

    // 첫 번째 구독자
    subject.subscribe(onNext: { value in
        print("Subscriber 1: \\\\(value)")
    }).disposed(by: disposeBag)

    // 첫 번째 구독자에게 값 전달
    subject.onNext("1")
    subject.onNext("2")

    // 두 번째 구독자
    subject.subscribe(onNext: { value in
        print("Subscriber 2: \\\\(value)")
    }).disposed(by: disposeBag)

    // 두 구독자에게 값 전달
    subject.onNext("3")
}

 

Subscriber 1: 1
Subscriber 1: 2
Subscriber 1: 3
Subscriber 2: 3

 

PublishSubject 사용 사례

  • 실시간 이벤트
    • 채팅 메시지, 실시간 데이터 업데이트와 같이 구독 시점 이후 발생하는 시간에 민감한 데이터 처리에 유용.
  • 알림 시스템
    • 특정 이벤트가 발생했을 때 이를 구독자에게 전달하는 상황에 사용.

1-2. BehaviorSubject

BehaviorSubject 특징

  • 항상 초기값을 가져야 함.
  • 새로운 구독자는 가장 최근에 방출된 값을 즉시 받게 됨.
  • 구독자가 없더라도 값이 방출될 때마다 저장되어, 나중에 구독하는 구독자에게 최신 값이 전달.

BehaviorSubject 예시

import RxSwift

func exampleBehaviorSubject() {
    let subject = BehaviorSubject(value: "Initial Value")
    let disposeBag = DisposeBag()

    // 첫 번째 구독자 (즉시 초기값 전달)
    subject.subscribe(onNext: { value in
        print("Subscriber 1: \\\\(value)")
    }).disposed(by: disposeBag)

    // 새로운 값 방출
    subject.onNext("First Update")

    // 두 번째 구독자 (최신 값 전달)
    subject.subscribe(onNext: { value in
        print("Subscriber 2: \\\\(value)")
    }).disposed(by: disposeBag)

    // 추가 값 방출
    subject.onNext("Second Update")
}

 

Subscriber 1: Initial Value
Subscriber 1: First Update
Subscriber 2: First Update
Subscriber 1: Second Update
Subscriber 2: Second Update

 

BehaviorSubject 사용 사례

  • 상태 관리
    • 네트워크 요청의 상태(로딩 중, 성공, 실패)를 나타내고, 새로운 구독자가 생길 때마다 최신 상태를 즉시 전달하는 데 유용.
  • UI 초기화
    • ViewController가 나타날 때 최신 데이터를 사용하여 UI를 초기화할 수 있음.

1-3. ReplaySubject

ReplaySubject 특징

  • 생성 시점에 버퍼 크기를 지정하며, 지정된 크기만큼의 이벤트를 저장.
  • 새로운 구독자가 구독할 때 저장된 이벤트를 모두 전달.
  • 메모리를 더 많이 사용하므로 적절한 버퍼 크기 설정 필요.

ReplaySubject 예시

import RxSwift

func exampleReplaySubject() {
    let subject = ReplaySubject<String>.create(bufferSize: 2)
    let disposeBag = DisposeBag()

    // 두 개의 값 방출 (버퍼에 저장)
    subject.onNext("1")
    subject.onNext("2")

    // 세 번째 값 방출 (버퍼에 저장, 가장 오래된 값 삭제)
    subject.onNext("3")

    // 첫 번째 구독자: 가장 최근 두 개의 값 받음
    subject.subscribe(onNext: { value in
        print("Subscriber 1: \\\\(value)")
    }).disposed(by: disposeBag)

    // 네 번째 값 방출
    subject.onNext("4")

    // 두 번째 구독자: 가장 최근 두 개의 값 받음
    subject.subscribe(onNext: { value in
        print("Subscriber 2: \\\\(value)")
    }).disposed(by: disposeBag)
}
Subscriber 1: 2
Subscriber 1: 3
Subscriber 1: 4
Subscriber 2: 3
Subscriber 2: 4

 

ReplaySubject 사용 사례

  • 캐시된 데이터 전달
    • 여러 구독자에게 과거 데이터를 캐싱하여 전달할 때 유용.
  • 최근 검색 기록
    • 사용자의 최근 검색어를 저장하고, 새로운 구독자가 구독할 때 과거 검색어를 전달 가능.

1-4. AsyncSubject

AsyncSubject 특징

  • 시퀀스 완료 시 마지막 next 이벤트만 전달.
  • completed 이벤트가 발생해야 구독자에게 값이 전달.
  • error 이벤트가 발생하면 값을 전달하지 않음.

AsyncSubject 예시

import RxSwift

func exampleAsyncSubject() {
    let subject = AsyncSubject<String>()
    let disposeBag = DisposeBag()

    // 첫 번째 구독자
    subject.subscribe(onNext: { value in
        print("Subscriber 1: \\\\(value)")
    }).disposed(by: disposeBag)

    // 값 방출 (아직 전달되지 않음)
    subject.onNext("1")
    subject.onNext("2")

    // 두 번째 구독자
    subject.subscribe(onNext: { value in
        print("Subscriber 2: \\\\(value)")
    }).disposed(by: disposeBag)

    // 시퀀스 완료
    subject.onCompleted()
}
Subscriber 1: 2
Subscriber 2: 2

 

AsyncSubject 사용 사례

  • 단일 결과 처리
    • 비동기 작업이 완료되었을 때 최종 결과만을 필요로 하는 경우에 유용.
    • 예를 들어, 파일 다운로드가 완료되었을 때 다운로드 된 파일의 경로를 전달하는 데 사용할 수 있음.

1-5. PublishSubject vs. BehaviorSubject vs. ReplaySubject vs. AsyncSubject

  PublishSubject BehaviorSubject ReplaySubject AsyncSubject
초기값 없음 필수 (초기값을 설정해야 함) 없음 없음
구독 시점의 값 전달 없음 (구독 이후 발생하는 값만 전달) 최신 값 (구독 시점의 가장 최근 값 전달) 저장된 과거 값 (버퍼 크기만큼 과거 값 전달) 시퀀스가 완료될 때 가장 마지막 값 전달
이벤트 버퍼링 버퍼링 없음 최근 하나의 값만 유지 버퍼 크기만큼 이벤트 저장 시퀀스가 완료될 때 마지막 값만 전달
에러 이벤트 에러 발생 시 모든 구독자에게 전달, 새로운 구독자도 에러 전달 에러 발생 시 모든 구독자에게 전달, 새로운 구독자도 에러 전달 에러 발생 시 모든 구독자에게 전달, 새로운 구독자도 에러 전달 에러 발생 시 마지막 값이 아닌 에러만 전달
완료 이벤트 완료 시 모든 구독자에게 전달, 새로운 구독자도 완료 전달 완료 시 모든 구독자에게 전달, 새로운 구독자도 완료 전달 완료 시 모든 구독자에게 전달, 새로운 구독자도 완료 전달 시퀀스 완료 시 마지막 값 전달
주요 사용 사례 버튼 클릭, 알림 등 구독 시점 이후 발생하는 이벤트 처리 상태 관리, 최신 상태를 유지하고 즉시 전달해야 하는 경우 검색 기록, 캐시된 데이터 등 과거 이벤트 재생 필요 시 비동기 작업 완료 후 최종 결과를 전달해야 하는 경우

 

선택 가이드

  • PublishSubject를 선택할 때
    • 구독 시점 이전에 발생한 이벤트는 중요하지 않고, 구독 이후 발생하는 이벤트만 처리하고자 하는 경우.
    • 예: 버튼 클릭 이벤트 처리, 실시간 알림 등.
  • BehaviorSubject를 선택할 때
    • 상태 관리가 필요하고, 구독자가 구독할 때마다 최신 상태를 즉시 전달해야 하는 경우.
    • 예: 사용자 프로필, 로그인 상태 등 최신 상태를 유지하는 시나리오.
  • ReplaySubject를 선택할 때
    • 구독자가 과거에 발생한 이벤트들을 모두 받아야 하는 경우.
    • 예: 최근 검색 기록, 캐시된 데이터 전달 등.
  • AsyncSubject를 선택할 때
    • 시퀀스가 완료된 후 마지막 값만 필요한 경우.
    • 비동기 작업의 최종 결과만을 전달해야 하는 경우.
    • 예: 파일 다운로드 완료 후 파일 경로 전달, 작업 완료 후 결과 처리.

1-6. Subject 정리

RxSwift의 Subject는 다양한 비동기 시나리오에서 데이터를 방출하고 수신할 수 있는 중요한 역할을 담당.

각 Subject는 고유한 특성을 가지고 있으며, 다음과 같은 경우에 사용할 수 있음.

  • PublishSubject: 구독 시점 이후에 발생하는 이벤트만 필요한 경우.
  • BehaviorSubject: 구독자가 구독할 때 최신 상태를 즉시 전달해야 하는 경우.
  • ReplaySubject: 과거 이벤트를 캐싱하여 새로운 구독자에게 전달해야 하는 경우.
  • AsyncSubject: 시퀀스가 완료된 후 마지막 값만 필요할 때.

2. Relay

Relay는 내부적으로 Subject를 래핑한 것으로, Subject의 기능을 더 안전하게 사용할 수 있음.

completed나 error 이벤트를 방출하지 않기 때문에, UI 컴포넌트와 같이 지속적인 이벤트를 처리해야 하는 상황에 주로 사용.

2-1. PublishRelay

PublishRelay 특징

  • 초기값을 필요로 하지 않음.
  • 구독자가 구독한 시점 이후에 발생하는 next 이벤트만 전달.
  • completed나 error 이벤트를 방출하지 않음.

PublishRelay 예시

import RxSwift
import RxRelay

func examplePublishRelay() {
    let relay = PublishRelay<String>()
    let disposeBag = DisposeBag()

    // 구독자 없이 값 방출 (출력되지 않음)
    relay.accept("Is anyone listening?")

    // 첫 번째 구독자
    relay.subscribe(onNext: { value in
        print("Subscriber 1: \\\\(value)")
    }).disposed(by: disposeBag)

    // 값 방출
    relay.accept("Hello")
    relay.accept("World")

    // 두 번째 구독자
    relay.subscribe(onNext: { value in
        print("Subscriber 2: \\\\(value)")
    }).disposed(by: disposeBag)

    // 추가 값 방출
    relay.accept("RxSwift")
    relay.accept("Relay")
}
Subscriber 1: Hello
Subscriber 1: World
Subscriber 1: RxSwift
Subscriber 2: RxSwift
Subscriber 1: Relay
Subscriber 2: Relay

 

PublishRelay 사용 사례

  • 버튼 클릭 이벤트
    • UI에서 버튼 클릭과 같은 이벤트 처리에 사용.
  • 실시간 데이터 스트림
    • 채팅 메시지나 실시간 알림과 같이 구독 시점 이후의 데이터 스트림을 처리할 때 유용.

2-2. BehaviorRelay

BehaviorSubject를 래핑한 Relay로, 항상 현재값을 유지하며, 새로운 구독자에게 구독 시점의 현재값을 즉시 전달합니다.

BehaviorRelay는 초기값을 필요로 하며, 구독자가 생길 때마다 최신 값을 전달합니다.

 

BehaviorRelay 특징

  • 초기값 필수.
  • 항상 현재값을 유지하며, 새로운 구독자에게 즉시 전달.
  • completed나 error 이벤트를 방출하지 않음.
  • value 프로퍼티를 통해 현재값을 직접 접근 가능.

BehaviorRelay 예시

import RxSwift
import RxRelay

func exampleBehaviorRelay() {
    let relay = BehaviorRelay(value: "Initial Value")
    let disposeBag = DisposeBag()

    // 첫 번째 구독자 (즉시 초기값을 받음)
    relay.subscribe(onNext: { value in
        print("Subscriber 1: \\\\(value)")
    }).disposed(by: disposeBag)

    // 값 방출
    relay.accept("First Update")

    // 두 번째 구독자 (현재값인 "First Update"를 즉시 받음)
    relay.subscribe(onNext: { value in
        print("Subscriber 2: \\\\(value)")
    }).disposed(by: disposeBag)

    // 추가 값 방출
    relay.accept("Second Update")

    // 현재값 직접 접근
    print("Current Value: \\\\(relay.value)")
}
Subscriber 1: Initial Value
Subscriber 1: First Update
Subscriber 2: First Update
Subscriber 1: Second Update
Subscriber 2: Second Update
Current Value: Second Update

 

BehaviorRelay 사용 사례

  • 상태 관리
    • 앱의 상태(예: 로그인 상태, 사용자 프로필 데이터)를 관리하고, 상태가 변경될 때마다 UI를 업데이트할 때 유용.
  • 폼 데이터 관리
    • 사용자 입력 폼의 현재 값을 유지하고, 필요할 때 접근할 수 있도록 할 때 사용.
  • 설정 데이터
    • 앱의 설정 값이나 환경 설정 데이터를 관리하고, 변경 시 UI에 즉시 반영할 때 유용.

2-3. PublishRelay vs. BehaviorRelay

  PublishRelay BehaviorRelay
초기값 없음 필수
현재값 유지 아니오
새로운 구독자에게 전달되는 값 구독 시점 이후의 next 이벤트만 구독 시점의 현재값 즉시 전달
value 프로퍼티 접근 가능 아니오
사용 사례 이벤트 스트림, 버튼 클릭 등 상태 관리, 현재 값 필요 시 접근

 

선택 가이드

  • PublishRelay를 선택할 때
    • 단순 이벤트 스트림이 필요한 경우.
    • 이벤트를 구독자에게 전달하고, 상태를 유지할 필요가 없는 경우
  • BehaviorRelay를 선택할 때
    • 상태 관리가 필요한 경우.
    • 현재 상태를 유지하고, 새로운 구독자에게 최신 상태를 즉시 전달해야 하는 경우

2-4. Relay 추가 사용 예시

PublishRelay를 이용한 버튼 클릭 이벤트 처리

import UIKit
import RxSwift
import RxCocoa
import RxRelay

class ViewController: UIViewController {
    let disposeBag = DisposeBag()
    let buttonClickRelay = PublishRelay<Void>()

    override func viewDidLoad() {
        super.viewDidLoad()

        // 버튼 생성 및 설정
        let button = UIButton(type: .system)
        button.setTitle("Click Me", for: .normal)
        button.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(button)

        // 버튼 레이아웃 설정
        NSLayoutConstraint.activate([
            button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            button.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])

        // 버튼 클릭 시 Relay에 이벤트 전달
        button.rx.tap
            .bind(to: buttonClickRelay)
            .disposed(by: disposeBag)

        // Relay 구독하여 클릭 이벤트 처리
        buttonClickRelay.subscribe(onNext: {
            print("Button was clicked!")
        }).disposed(by: disposeBag)
    }
}
  • 버튼 클릭 이벤트를 PublishRelay를 통해 처리.
  • 버튼이 클릭될 때마다 buttonClickRelay에 Void 이벤트가 전달되고, 이를 구독하여 콘솔에 메시지를 출력.
  • PublishRelay는 이벤트 스트림을 단순하게 전달하기에 적합.

BehaviorRelay를 이용한 로그인 상태 관리

import RxSwift
import RxRelay

enum LoginState {
    case loggedIn(User)
    case loggedOut
}

struct User {
    let username: String
}

class SessionManager {
    static let shared = SessionManager()

    // BehaviorRelay를 이용하여 현재 로그인 상태를 관리
    private(set) var loginStateRelay = BehaviorRelay<LoginState>(value: .loggedOut)

    private init() {}

    func logIn(username: String) {
        let user = User(username: username)
        loginStateRelay.accept(.loggedIn(user))
    }

    func logOut() {
        loginStateRelay.accept(.loggedOut)
    }
}

 

import RxSwift

let disposeBag = DisposeBag()

// 로그인 상태 구독
SessionManager.shared.loginStateRelay.subscribe(onNext: { state in
    switch state {
    case .loggedIn(let user):
        print("\\\\(user.username) has logged in.")
    case .loggedOut:
        print("User has logged out.")
    }
}).disposed(by: disposeBag)

// 로그인 및 로그아웃 이벤트 발생
SessionManager.shared.logIn(username: "JohnDoe")
// 출력: JohnDoe has logged in.

SessionManager.shared.logOut()
// 출력: User has logged out.
  • BehaviorRelay를 사용하여 로그인 상태를 관리.
  • loginStateRelay는 현재 로그인 상태를 유지하며, 새로운 구독자가 생기면 최신 상태를 즉시 전달.
  • 로그인 및 로그아웃 시점에 따라 BehaviorRelay에 새로운 상태를 accept하여 구독자에게 전달.

장점

  • 현재 상태를 항상 유지하므로, 새로운 구독자가 생겨도 최신 상태를 즉시 확인 가능.
  • 상태 관리가 명확하고 일관되게 이루어 짐.

2-5. Relay 정리

Relay는 RxSwift에서 안전하고 예측 가능한 이벤트 처리와 상태 관리를 위해 강력한 도구.

Relay를 사용함으로써 Subject의 복잡성과 위험성을 줄이고, 더 깔끔한 코드 구조를 유지할 수 있음.

  • PublishRelay: 단순한 이벤트 스트림 처리에 적합하며, 완료나 에러 이벤트가 필요 없는 경우 사용.
  • BehaviorRelay: 상태 관리가 필요한 경우에 유용하며, 현재 상태를 유지하고 새로운 구독자에게 최신 상태를 즉시 전달 가능.