[Swift] RxSwift
RxSwift
반응형 프로그래밍(Reactive Programming)을 Swift에서도 활용할 수 있게 만든 라이브러리이다. 비동기 및 이벤트 기반 코드를 작성할 수 있다.
필요성
1. 비동기 코드의 복잡성 해결
iOS 앱 개발에서는 네트워크 요청, UI 이벤트 처리, 데이터 바인딩과 같은 비동기 작업이 많다. 키보드 상태를 감지하기 위해 NotificationCenter, tableView나 collectionView의 셀을 눌렀을 때의 Delegate, 네트워크 요청에는 GCD, Closure 등의 비동기 처리를 각각 다른 방식으로 처리하였다. 콜백 지옥이 존재할 수도 있고 일관되지 않은 방식으로 처리하다보니 복장성 또한 증가한다.
2. 명령형 프로그래밍의 한계
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
setupUI()
connectUIControls()
createDataSource()
listenForChanges()
}
명령형으로 작성된 코드이다. 각각의 메소드들이 어떠한 역할을 하는지 알 수 없다. 어떠한 순서대로 작동하는지도 역시나 알 수 없다. 메소드 호출 순서를 변경함에 따라 앱이 의도한 바와 다르게 작동할 수도 있다.
3. 선언적 프로그래밍 방식 제공
- 명령형 프로그래밍은 상태 변화를 자유롭게 할 수 있다.
- 함수형 프로그래밍은 side effect를 줄이거나 관리하기 편하다.
- RxSwift는 이 두 가지의 장점을 균형있게 활용할 수 있다.
- 명령형 + 선언형 프로그래밍 = 자유롭게 상태변화가 이루어지면서 추적/예측 가능한 결과 처리
RxSwift의 구성요소
1. Observable<T>
- 시간에 따라 변화하는 데이터를 이벤트 형태로 방출하는 비동기 스트림
- T 형태의 데이터 snapshot을 전달할 수 있는 일련의 이벤트를 비동기적으로 생성함
- 하나 이상의 Observers가 실시간으로 어떤 이벤트에 반응하고 앱 UI를 업데이트 하거나 생성하는지를 처리하고 활용할 수 있도록 함
- Observable<T>(ObservableType 프로토콜)은 세 가지 유형으로만 이벤트를 방출한다.
- next - 최신/다음 데이터를 전달하는 이벤트, T에 해당하는 값을 전달
- completed - 성공적으로 일련의 이벤트들을 종료시키는 이벤트, Observable이 성공적으로 자신의 생명주기를 완료했으며, 추가적으로 이벤트를 생성하지 않음
- error - Observable이 에러를 발생시켰으며 추가적으로 이벤트를 생성하지 않음, Error 값 전달
- next는 계속해서 T에 해당하는 값을 방출할 수 있으며 error와 completed는 이벤트를 방출 후에 종료될 수 있음
enum Event<Element>{
case next(Element)
case error(Swift.Error)
case completed
}
public protocol ObserverType {
func on(_ event: Event<Element>)
}
extension ObserverType {
public func onNext(_ element: Element) {
self.on(.next(element))
}
public func onCompleted() {
self.on(.completed)
}
public func onError(_ error: Swift.Error) {
self.on(.error(error))
}
}
Finite Observables
- 값을 방출한 뒤 completed 또는 error를 통해 종료된다.
- 파일 다운로드
Infinite Observables
- 자연적 또는 강제적으로 종료되어야 하는 Finite Observable과 달리 무한한 시퀀스를 관찰해야 하는 경우가 있다.
- UI 이벤트
Hot Observables
- 생성되지마자 값을 방출할 수 있다.
- 따라서 해당 Observable을 나중에 구독한 Observer는 중간 어디선가부터 값을 전달 받을 수 있다.
- ex) 생방송
- Pulish Subject(Relay)
- Behavior Subject(Relay)
Cold Observables
- Observer가 구독할 때까지 방출을 기다리기 때문에 구독이 발생하면 처음부터 모든 값을 관찰 가능하다.
- ex) 유튜브 영상
- Single
- Just, of, from
2. Operators
- Observable의 이벤트(비동기)를 입력 받아 출력을 생성한다.
- ObservableType, Observable 클래스에는 복잡한 논리를 구현하기 위해 많은 메서드(Operator)가 포함되어 있다.
3. Schedulers
- Rx에서의 스케줄러는 Dispatch queue와 동일한 것이지만 더 강력하고 쓰기 쉽다고 한다.
- 개발자가 직접 스케줄러를 생성하거나 커스텀할 상황은 별로 없을 것.
Observable/Observer
Observable은 위에서 언급한대로 특정한 이벤트가 있을 경우에 방출하는 역할을 한다.
Observer는 Observable이 방출한 값을 입력 받는 역할이다.
"Observable을 구독한다."라고도 말한다.
In ReactiveX an observer subscribes to an Observable.
https://reactivex.io/documentation/observable.html
Observable랑 ObservableType 차이
1. Observable<T>
Observable은 RxSwift에서 가장 기본적인 스트림 타입이다.
데이터를 비동기적으로 방출하는 객체로 subscribe(구독)하면 방출된 값을 받을 수 있다.
let observable = Observable<Int>.just(10) // Observable의 인스턴스
observable.subscribe(onNext: { value in
print(value) // 10
})
2. ObservableType
ObservableType은 RxSwift에서 프로토콜로 정의된 타입이다.
Observable뿐만 아니라 Single, Maybe, Completable 같은 다양한 스트림 타입들도 ObservableType을 준수하고 있다.
즉, ObservableType은 "나는 Observable 같은 동작을 한다."라고 하는 프로토콜이다. Observable, Single, Maybe, Completable 등은 구현체이다.
Observable을 만드는 방법
1. Just
let justObservable1 = Observable<Int>.just(1)
let justObservable2 = Observable.just(1)
justObservable2.subscribe { event in
print(event)
}
// next(1)
// completed
just는 단 한 개의 항목을 방출할 수 있다.
구독자는 1의 값을 next로 받고 성공적으로 이벤트가 방출되었음을 completed로 알린다.
let justObservable = Observable.just([1, 2])
justObservable.subscribe { event in
print(event)
}
// next([1, 2])
// completed
배열을 넣어도 배열 전체가 "한 개의 항목"에 해당하므로 문제 없이 작동한다.
let justObservable = Observable.just(1, 2)
이와 같이 한 개의 항목이 아닐 경우 오류가 발생한다.
2. of
let ofObservable: Observable = Observable.of(1, 2)
ofObservable.subscribe { event in
print(event)
}
// next(1)
// next(2)
// completed
한 개 이상의 항목을 방출 시키고 싶을 땐 of 메소드를 사용할 수 있다.
1을 방출 시키고 그 다음 2를 방출 시킨다.
파라미터의 타입은 동일해야 한다.
3. from
파라미터로 오직 하나의 배열만 받으며 배열의 요소들을 순서대로 하나씩 방출한다.
let fromObservable: Observable = Observable.from([1,2,3])
fromObservable.subscribe { event in
print(event)
}
// next(1)
// next(2)
// next(3)
// completed
4. create
create는 파라미터로 탈출 클로저를 받는다. 이 클로저는 AnyObserver를 인자로 받고 Disposable을 반환한다.
let createObservable = Observable<String>.create { observer in
observer.onNext("1")
observer.onNext("2")
observer.onCompleted()
observer.onNext("3")
return Disposables.create()
}
// next(1)
// next(2)
// completed
실제로 onNext, onCompleted 메소드 등을 실행하고 마지막에 Disposables.create()을 통해 Disposable을 반환한다.
클로저가 끝나기 전에 onCompleted 또는 onError를 1번 호출하면 그 이후에는 어떠한 이벤트도 방출하지 않는다.(dispose 됨)
Traits 로 Observable 만들기
Traits는 일반적인 Observerbale보다 좁은 범위의 Observable로 선택적 사용할 수 있다.
Single, Maybe, Completable 세 가지로 Trait을 사용할 수 있다.
1. Single
- .success(value) 또는 .error를 방출한다.
- .success(value) = .next + .completed
- 사용 : 성공 또는 실패로 확인될 수 있는 1회성 프로세스(ex>데이터 다운로드)
2. Completable
- .completed 또는 .error만 방출하며, 이 외에 어떠한 값도 방출하지 않는다.
- 사용 : 연산이 제대로 완료되었는지만 확인하고 싶을 때 사용한다. (ex> 파일쓰기)
3. Maybe
- Single과 Completable을 섞어 놓은 것
- success(value), .completed, .error 모두 방출 할 수 있다.
- 사용 : 프로세스가 성공, 실패 여부와 더불어 출력된 값도 내뱉을 수 있을 때
Subject
- 하지만 보통의 앱 개발에서 필요한 것은 실시간으로 Observable에 새로운 값을 추가하고 subscriber에게 방출하는 것이다.
- 즉, Observable & Observer 역할을 할 수 있는 것이 Subject이다.
- Subject는 Cold Observable을 Hot하게 변형하는 효과를 얻을 수 있다.
- 구독에 상관 없이 이벤트를 발행할 수 있다.
- .next 이벤트를 수신할 때마다 subscriber에 방출한다.
1. PublishSubject
빈 상태로 시작하여 구독 이후 시점부터 값을 방출한다.
let publishSubject = PublishSubject<String>()
publishSubject.onNext("hello") // #1
publishSubject.subscribe(onNext: {
print($0)
})
publishSubject.onNext("hello 1") // #2
// hello 1
hello 1만 출력된 이유는 구독 이후 시점부터 값을 전달 받을 수 있기 때문이다.
2. BehaviorSubject
하나의 초기값을 가진 상태로 시작하여 subscriber에게 초기값 또는 최신값을 방출한다.
let behaviorSubject = BehaviorSubject<String>(value: "hello")
behaviorSubject.subscribe(onNext:{
print($0)
})
// hello
let behaviorSubject = BehaviorSubject<String>(value: "")
behaviorSubject.onNext("hello")
behaviorSubject.subscribe(onNext:{
print($0)
})
behaviorSubject.onNext("hello~~~~")
// hello
// hello~~~~
초기값이 subscriber에게 유효한 값이 되며, 최초 구독 시 subject의 가장 최신 값을 받을 수 있다.
3. ReplaySubject
n개의 이벤트를 저장하고 구독하면 구독된 시점과 상관 없이 저장된 이벤트를 모두 전달한다.
버퍼를 사용하여 이벤트를 저장하기 때문에 이미지나 배열 같이 메모리를 크게 차지하는 값들은 메모리에 부하를 줄 수 있다.
let replaySubject = ReplaySubject<String>.create(bufferSize: 2)
replaySubject.onNext("1")
replaySubject.onNext("2")
replaySubject.subscribe(onNext: {
print($0)
})
replaySubject.onNext("3")
// 1
// 2
// 3
let replaySubject = ReplaySubject<String>.create(bufferSize: 2)
replaySubject.onNext("1")
replaySubject.onNext("2")
replaySubject.onNext("3")
replaySubject.subscribe(onNext: {
print($0)
})
replaySubject.onNext("4")
// 2
// 3
// 4
버퍼 크기가 2이기 때문에 가장 먼저 발생한 이벤트인 "1"이 지워진다.
Disposable / Dispose
구독(subscribe)는 사실 Disposable을 리턴한다.
Disposable은 구독을 해제할 때 사용하는 인스턴스이다.
그동안 위 예시에서는 Disposable이 리턴되는 것에 대한 처리를 하지 않았다.
구독이 이루어졌으면 구독을 언제 취소할 것인지 정확히 명시해야 할 필요가 있다. (정상적으로 구독이 해제되지 않으면 메모리 누수 위험)
let replaySubjectDisposable = replaySubject.subscribe(onNext: {
print($0)
})
replaySubject.onNext("4")
replaySubjectDisposable.dispose()
위 예제에서 replaySubject의 리턴 값을 저장할 replaySubjectDisposable 변수를 만들고 이벤트를 수신 받고 싶지 않을 때 .dispose() 호출 시 구독이 해제된다.
DisposeBag
실제 프로젝트 단위에서는 많은 구독이 이루어질테고 일일히 관리하는 것은 효율적이지 못하기 때문에 DisposeBag을 활용할 수 있다.
let replaySubject = ReplaySubject<String>.create(bufferSize: 2)
let disposeBag = DisposeBag()
replaySubject.subscribe(onNext: {
print($0)
}).disposed(by: self.disposeBag)
ViewController에서 작성 했다면 ViewController가 deinit 될 때 disposeBag도 메모리가 해제되면서 disposeBag이 가진 Disposable들이 모두 구독 해제가 된다.
구독이 이루어졌을 때 Disposable 인스턴스를 처리하지 않으면 Xcode에서 경고를 알려주기 때문에 쉽게 인지할 수 있다.
Relay
Relay는 Subject의 Wrapper 클래스이다.
Subject는 RxSwift을 import 해야 하고 Relay는 RxCocoa를 import하면 사용할 수 있다.
Relay는 completed와 error를 받지 않은 것이 Subject와의 차이점이다.
그래서 dispose되기 전까지 계속 작동하기 때문에 종료 없이 지속되는 UI 작업하기에 적합하다
PublishRelay
PublishSubject를 Wrapping하였다.
let publishRelay = PublishRelay<Int>()
publishRelay.accept(1)
publishRelay.subscribe({ event in
print(event)
}).disposed(by: self.disposeBag)
publishRelay.accept(2)
// next(2)
BehaviorRelay
BehaviorSubject를 Wrapping하였다.
let behaviorRelay = BehaviorRelay<Int>(value: 1)
behaviorRelay.accept(2)
behaviorRelay.subscribe({ event in
print(event)
}).disposed(by: self.disposeBag)
behaviorRelay.accept(3)
// next(2)
// next(3)
https://github.com/fimuxd/RxSwift/tree/master