[Day 49] Reactive Programming
RxSwift를 사용하여 Reactive Rrogramming하기! Part.3
📌 Scheduler
Scheduler
는 Obsevable
연산자들이 동작하는 실행 Context(Thread)
를 정의하는 객체에요.
Rx에서의 연산은 기본적으로 동기적이고, 현재 스레드에서 실행되지만, subscribe(on:)
과 obseve(on:)
을 사용하면 명시적으로 실행 Thread를 제어가 가능해요
예시를 한번 볼게요
1
2
3
4
5
6
7
8
9
10
let mainScheduler = MainScheduler.instance
let backgroundScheduler = ConcurrentDispatchQueueScheduler(qos: .background)
Observable<Int>.just(100)
.subscribe(on: backgroundScheduler) // Observable 시작은 백그라운드에서
.map { $0 * 10 } // 여전히 백그라운드에서 실행
.observe(on: mainScheduler) // 이 시점부터 메인 스레드로 이동
.filter { $0.isMultiple(of: 20) } // 메인 스레드에서 실행
.subscribe(onNext: { print($0) }) // 메인 스레드에서 결과 처리
.disposed(by: disposeBag)
- subscribe(on:) → 무거운 작업(예: 네트워크 요청, 파일 읽기 등)은 백그라운드에서 처리하고
- observe(on:) → UI 작업은 무조건 메인 스레드에서 해야 하니까 그 전에 바꿔주는 거에요.
subscribe(on:) 과 observe(on:)의 차이
| | subscribe(on:) | observe(on:) | | — | — | — | | 목적 | Observable이 생성되고 실행되는 Thread를 결정 | 이벤트가 처리되는 Thread를 지정 | | 시점 | Observable Chain 가장 처음 영향 | 호출 이후 모든 Chain 영향 | | 예시 | .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .background)) | .observeOn(MainScheduler.instance) |
주요 Scheduler 종류
| Scheduler | 설명 | 주로 쓰는 용도 | | — | — | — | | MainScheduler.instance
| 메인 스레드(UI Thread) | UI 업데이트 | | ConcurrentDispatchQueueScheduler
| GCD의 백그라운드 큐 | 네트워크 요청, 디스크 작업 등 | | SerialDispatchQueueScheduler
| GCD의 직렬 큐 | 순서 보장해야 하는 작업 | | OperationQueueScheduler
| OperationQueue 기반 | 복잡한 백그라운드 작업 | | ImmediateSchedulerType
| 지연 없이 즉시 실행 | 테스트용 혹은 단순 연산 | | CurrentThreadScheduler
| 현재 스레드에서 실행 | 지연 없이 현재 흐름에서 처리 |
📌 Traits
Traits는 특정한 제약과 의미를 부여한 Observable의 추상화된 형태에요 모든 Traits는 asObservable() 연산을 통해 Observable로 변환이 가능하다!
먼저 간단하게 Traits에 대해 뭐가 있는지 살펴 보자
Traits | 설명 | 용도 | 이벤트 |
---|---|---|---|
Single | 하나의 Element | Data가 1회만 발생할 때 (네트워크 API, DB 쿼리) | success(Element) failure(Error) |
Completable | Completed 또는 Error 방출 | Data 없이 완료 여부만 필요할 때 | completed error(Error) |
Maybe | 하나의 Element 또는 Completed, Error 방출 | Optional한 데이터 흐름 | success(Element) completed error(Error) |
Infallible | 하나의 Element 또는 Completed 방출 | Error가 없는 이벤트 흐름 | next(Element) completed |
Single
1
2
3
4
5
6
7
8
9
10
11
Single<UIImage>.create { single in
downloadImage { result in
switch result {
case .success(let image): // success(Element)
single(.success(image))
case .failure(let error): // failure(Error)
single(.failure(error))
}
}
return Disposables.create()
}
success(Element)
나failure(Error)
중 하나만 발생!
Complettable
1
2
3
4
5
6
Completable.create { completable in
saveToDatabase { success in
success ? completable(.completed) : completable(.error(MyError.db)) // completed or error(Error)
}
return Disposables.create()
}
- 데이터는 모르겠고, 성공했는지, 실패했는지만 알고 싶을떄!
Maybe
1
2
3
4
5
6
7
8
Maybe<String>.create { maybe in
if isValidUser {
maybe(.success("UserName"))
} else {
maybe(.completed) // 아무 값도 없이 종료
}
return Disposables.create()
}
1
2
3
4
5
6
7
8
9
func loadUserSettings() -> Infallible<Settings> {
return Infallible.create { infallible in
loadSettings {
infallible(.next($0))
infallible(.completed)
}
return Disposables.create()
}
}
- error가 절대 없는 상황에서 사용!
- 값만 전달하고 성공/완료로 끝나는 단순한 스트림
- 에러 처리를 생략하고 싶은 UI 데이터 바인딩
📌 Subject and Relay
Subject와 Relay는 RxSwift와 RxCocoa에서 핵심적으로 사용되는 Hot Obsevable이에요.
Subject와 Relay는 Observable이면서 Observer의 역할
을 한답니다.
Subject (RxSwift) | Relay (RxRelay) | |
---|---|---|
오류 방출 | ✅ | ❌ |
스트림 완료 | ✅ | ❌ |
종류 | PublicshSubject BehaviorSubject ReplaySubject AsyncSubJect | PublishRelay BehaviorRelay |
각각의 예시 코드와 정의를 알아볼까요??
PublicshSubject
구독 이후에 발생되는 이벤트만 전달하며, 이전 이벤트는 기억하지 않아요.
1
2
3
4
5
6
7
8
9
10
let publishSubject = PublishSubject<Int>()
publishSubject
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)
publishSubject.on(.next(1))
publishSubject.on(.next(2))
publishSubject.on(.next(3))
publishSubject.on(.completed)
일반적인 이벤트 처리나 단발성 알림에 적합해요
BehaviorSubject
반드시 초기값이 필요하며, 가장 최근의 이벤트 하나를 저장하고, 구독시 즉시 전달합니다.
1
2
3
4
5
6
7
8
9
10
let behaviorSubject = BahaviorSubject(value: 0)
behaviorSubject
.subscribe(onnext: {
print($0)
})
.disposed(by: disposeBag)
behaviorSubject.on(.next(1))
behaviorSubject.on(.next(2))
behaviorSubject.on(.next(3))
behaviorSubject.on(.completed)
상태를 계속 추적하고 최신 상태를 공유할 때 적합해요
ReplaySubject
지정 된 버퍼 크기 만큼의 이전 이벤트를 저장하고, 구독시 버퍼에 저장된 모든 이벤트를 즉시 전달해요
1
2
3
4
5
6
7
8
9
10
let replySubject = ReplaySubject<Int>.create(bufferSize: 2)
replySubject.on(.next(1))
replySubject.on(.next(2))
replySubject
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)
replySubject.on(.next(3))
replySubject.on(.completed)
값의 히스토리를 유지해야 할 때 적합해요
AsyncSubJect
completed 이벤트 발생시, 가장 마지막 값을 전달해요
1
2
3
4
5
6
7
8
9
10
let asyncSubject = AsyncSubject<Int>()
asyncSubject
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)
asyncSubject.on(.next(1))
asyncSubject.on(.next(2))
asyncSubject.on(.next(3))
asyncSubject.on(.completed)
작업이 끝난 뒤 최종 결과만 필요할 경우 적합해요
PublishRelay
PublishSubject에서 에러와 완료 이벤트를 제거한 형태에요
1
2
3
4
5
6
7
8
9
let publishRelay = PublishRelay<Int>()
publishRelay
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)
publishRelay.accept(1)
publishRelay.accept(2)
publishRelay.accept(3)
UI이벤트 처리처럼 에러가 나면 안되는 상황에 적합해요
BehaviorRelay
BehaviorSubject에서 에러와 완료를 제거한 형태로, 현재 상태 값을 저장하고 구독시 전달해요
1
2
3
4
5
6
7
8
9
let behaviorRelay = BehaviorRelay(value: 0)
behaviorRelay
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)
behaviorRelay.accept(1)
behaviorRelay.accept(2)
behaviorRelay.accept(3)
마찬가지로 UI이벤트 처리처럼 에러가 나면 안되는 상황에 적합해요
📌 RxCocoa
RxSwift를 UIKit, AppKit등 Apple의 UI프레임워크에 통합하기 위한 확장 라이브러리
에요
Binder | ControlEvent | ControlProperty | |
---|---|---|---|
방향 | 단방향(State → UI) | 단방향(UI → State) | 양뱡향 |
타입 | State | Event | State + Evnet |
목적 | UI 값 바인딩 | UI 이벤트 수신 | UI 값 바인딩 UI 이벤트 수신 |
예시 | 모든 UI Property @dynamicMemberLookup으로 자동 생성 | UIButton.rx.tap UIScrollerView.rx.didScroll UICollectionView.rx.itemSeleted | UITextFiedld.rx.text UISwitch.rx.isOn UISlider.rx.value UIDatePicker.rx.date |
간단한 사용자 정의 Binder, ControlEvent, ControlProperty 를 만들어 볼까요?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class MyButton: UIControl {
..
}
extension Reactive where Base: MyButton {
var tap: ControlEvent<Void> {
controlEvent(.touchUpInside)
}
}
class MyToggleSwitch: UIControl {
var isOn: Bool = false
...
}
extension Reactive where Base: MyToggleSwitch {
var isOn: ControlProperty<Bool> {
controlProperty(editingEvents: .valueChanged) { toggleSwitch in
toggleSwitch.isOn
} setter: { toggleSwitch, isOn in
toggleSwitch.isOn = isOn
}
}
}
맺으며: 춘식이와 함께하는 스마트한 개발
오늘도 오류 내며 자란 춘식이였습니다. 🐾 더 열심히 노력 해서 좋은 iOS Developer가 되자!
혹시 이 글을 보시는 분들 중 더 유용한 팁이 있다면 댓글로 많이 알려주세요!