[RxSwift 정리 - 3] Signal, Driver

팔랑이·2025년 6월 5일

iOS/Swift

목록 보기
72/81

[RxSwift 정리 - 2]에 이어서, ViewModel에서 자주 사용했던 Signal, Driver에 대해서 살펴볼 예정이다.

썸네일


Signal / Driver

Signal과 Driver는 UI 작업에 최적화된 Hot Observable이다.
Relay나 일반 Observable과 달리 Main Thread 에서만 동작하도록 제한된 기능을 제공하는, 보다 제한된 Observable이라고 할 수 있다.

왜 필요할까?

UIKit은 thread-safe하지 않기 때문에
UI 관련 작업은 반드시 main thread에서 실행해야 한다.

thread-safe: 여러 스레드가 동시에 접근해도 문제가 발생하지 않는 상태.
UIKit의 대부분의 객체는 thread-safe하지 않아서, 백그라운드 스레드에서 접근하면 크래시 발생 가능.

Relay나 Observable을 UI작업에 사용 시 꼭 observe(on: MainScheduler.instance)를 붙여줘야 했는데, Driver / Signal 에서는 그러지 않아도 된다는 뜻이다.


Signal & Driver

Signal

  • 이벤트 중심(Event Stream)
  • 최신값을 저장하지 않음
  • 한 번만 처리하면 되는 이벤트에 사용 ex) button tap, alert 표시, 네비게이션 이동 트리거 등

-> PublishRelay와 비슷하게 사용한다.

Driver

  • 상태 중심(State Stream)
  • 최신값을 유지함
  • 상태가 업데이트되는 데이터에 사용 ex) 리스트 데이터, 로딩 상태, 선택된 인덱스, 카운트 값

-> BehaviorRelay와 비슷하게 사용한다.


Input–Output 패턴에서 Driver / Signal 활용 예시

ViewController - ViewModel까지의 흐름을 정리한 예시이다.

ViewModel

final class ArchiveViewModel {
    private let ticketsRelay = BehaviorRelay<[Ticket]>(value: [])
    
    struct Input {
        let searchText: Observable<String>
        let deleteTicket: Observable<String>
    }
    
    struct Output {
        let filteredTickets: Driver<[Ticket]>
        let deleteCompleted: Signal<String>
    }
    
    func transform(input: Input) -> Output {
        let filteredTickets = input.searchText
            .map { /* ... */ }
            .asDriver(onErrorJustReturn: [])
        
        let deleteCompleted = input.deleteTicket
            .flatMapLatest { /* ... */ }
            .asSignal(onErrorJustReturn: "")
        
        return Output(
            filteredTickets: filteredTickets,
            deleteCompleted: deleteCompleted
        )
    }
}

Input (Observable로 받음)

  • ViewModel에서는 구체적인 타입에 의존하지 않도록 Observable로 추상화 ❗️
  • searchText: ViewController에서 전달받은 검색어 스트림
  • deleteTicket: ViewController에서 전달받은 삭제 이벤트 스트림

Output (Driver/Signal로 내보냄)

  • filteredTickets: 텍스트 변경에 따라 필터링된 티켓 리스트 상태
    • Observable → Driver로 변환 (.asDriver())
    • ViewController의 tableView가 Observer로서 이 Driver를 구독
    • 구독 즉시 현재 필터링 결과를 받아서 화면에 표시
  • deleteCompleted: 삭제 완료 이벤트
    • Observable → Signal로 변환 (.asSignal())
    • ViewController의 토스트 메서드가 Observer로서 이 Signal을 구독
    • 삭제가 완료된 그 순간에만 토스트 표시 (과거 이벤트 무시)

ViewController

final class ArchiveViewController: UIViewController {
    private let searchTextRelay = BehaviorRelay<String>(value: "")
    private let deleteTicketRelay = PublishRelay<String>()
    
    private func bind() {
        searchBar.rx.text.orEmpty // Observable
            .bind(to: searchTextRelay) // Observer
            .disposed(by: disposeBag)
        
        tableView.rx.itemDeleted // Observable
            .map { self.tickets[$0.row].id } // Observer
            .bind(to: deleteTicketRelay)
            .disposed(by: disposeBag)
        
        let input = ArchiveViewModel.Input(
            searchText: searchTextRelay.asObservable(),
            deleteTicket: deleteTicketRelay.asObservable()
        )
        let output = viewModel.transform(input: input)
        
        output.filteredTickets
            .drive(tableView.rx.items) { _, _, _ in }
            .disposed(by: disposeBag)
        
        output.deleteCompleted
            .emit(onNext: { self.showToast() })
            .disposed(by: disposeBag)
    }
    
    private func showToast() { }
}
  1. Observable → Relay: View의 이벤트를 Input으로 정제
  • searchBar.rx.text.orEmpty : 텍스트 변경 이벤트 스트림(Observable)
    → searchTextRelay에 바인딩 (Observer)
  • tableView.rx.itemDeleted : 삭제 이벤트 스트림(Observable)
    → deleteTicketRelay에 바인딩 (Observer)
  1. Driver / Signal 구독: Output 기반 UI 업데이트
  • filteredTickets.drive(…): 상태가 변경될 때마다 tableView 업데이트
  • deleteCompleted.emit(…): 삭제 이벤트 발생 시 토스트 표시

참고: Drive와 Signal 구독
Drive와 Signal은 각각

  • Driver → drive
  • Signal → emit
    으로 구독해야 한다.
profile
정체되지 않는 성장

0개의 댓글