RxSwift 를 공부하며 정리한 글 🧑🏻💻
ReativeX : https://reactivex.io/
RxSwift 4시간 끝내기 : https://www.youtube.com/watch?v=w5Qmie-GbiA
Observer Pattern 강의 :
https://www.youtube.com/watch?v=1dwx3REUo34
RxJava 정리글 : https://12bme.tistory.com/570
[RxJava 프로그래밍] - 유동환, 박정준
- RxSwift 는 비동기 프로그래밍을 위한 도구
- RxSwift 함수형 프로그래밍을 가능하게 한다. 그리고 함수형 프로그래밍은 멀티쓰레드 환경에서 데드락이 발생하지 않도록 해준다.
- RxSwift 는 선언형 프로그래밍을 지향한다. 선언형 프로그래밍은 명령형 프로그래밍의 반대말로, 어떤 방법(How)으로 동작하는지가 아니라 프로그래밍할 대상이 무엇(what)인지 알려주는 것을 의미한다.
- 예를 들어, 명령형 프로그래밍에서는 실행할 알고리즘과 동작을 구체적으로 명시한다. 하지만 선언형 프로그래밍은 목표를 명시할 뿐 실행할 알고리즘을 명시하지 않는다.
Observer Pattern 의 이해
Observer Pattern 은 한국어로 감시자 패턴.
감시자들이 한 곳을 계속해서 바라보고 있다. 그곳에서 어떠한 이벤트가 일어났을 때, 이벤트를 바라보고 있던 감시자들이 바로 반응할 수 있는 패턴.
Observer 는 다른 이름으로 Subscriber, Listener 라고도 한다.
만약 이런 옵저버 패턴을 가지지 않는다면, 이벤트를 체크 해야하는 오브젝트들은 매 1초, 1분 마다 계속해서 확인을 해야한다. 이런걸 Polling 이라고 한다.
Polling interval 안에 이벤트가 생겼다 사라지면 알 수도 없다. 하지만 Observer Pattern 을 사용한다면 event 가 일어나는 순간 바라보고 있던 옵저버들이 반응하게 하는 것이 가능하다.
Observer Pattern 의 클래스 구조
- 이벤트를 감지할 Observer 인터페이스를 만든다.
- update() : 이벤트가 일어났을 때 동작할 함수
- Observer1, Observer2 들을 만든다.
- Event를 정의한다. 이벤트 안에는 이 이벤트를 바라보고 있는 Observer1, Observer2 들을 소유하고 있다.
- addObserver() : 옵저버를 추가
- notify() : 옵저저의 update를 호출
Observable Stream 을 가지고 비동기 프로그래밍을 하는 API
MS 사에서 만들었다.
RxJava, RxSwift, RxPython 등 왠만한 언어들에서 지원된다.
RxSwift 를 사용하는 이유는 결국 async 한 프로그래밍을 할 때 편리함을 주기 위해서.
- 곰튀김님의 repository 를 git clone 해오고 공부를 시작했다.
https://github.com/iamchiwon/RxSwift_In_4_Hours
- Xcode 에서 clone 한 프로젝트를 실행했을 때, 시뮬레이터가 등장하지 않는 작은 트러블 이슈가 있었는데 이 방법으로 해결했다.
https://dongminyoon.tistory.com/45
https://reactivex.io/documentation/ko/observable.html
- Observable 의 뜻
[이성의 기능: The Function of Reason] : "Observed" 라는 단어가 관찰을 통해서 얻은 결과를 의미한다면 "Observable" 은 현재는 관찰되지 않았지만 이론을 통해서 앞으로 관찰할 가능성을 의미한다.
- "Every Observable instance is just a sequence"
Observable 은 관찰 가능한 흐름이다. Observer 들이 Observable 을 구독하는 형태다.
- Observable 은 다음 3가지의 이벤트에 반응한다.
- next : 어떤 항목을 배출하는 것, 데이터의 발행을 알린다.
- error : 항목을 배출하다 에러가 발생한 경우. 스트림을 종료시킨다.
- complete : 성공적으로 next 이벤트가 완료된 경우. 스트림을 종료시킨다.
- error, complete 가 발생한 경우 모두 Dispose 가 호출되고 스트림이 종료된다. Dispose (처리하다, 처분하다)
- Observable 타입은 모두 Disposable 타입을 반환하는데, 이 값을 이용해서 스트림을 종료하고 작업을 종료하는 것이 가능해진다.
- 보통 DisposeBag 이라는 레퍼런스 타입을 사용해서 Disposable 변수들을 담아두었다가 한번에 처리를 해준다. 마치 쓰레기통 같은 역할.
( Observable 예제 1 )
// Observable 함수 생성 func fromArray(_ arr: [Int]) -> Observable<Int> { // Observable 을 만들어주며 함수 종료. // Observable 은 Disposable 을 반환한다. return Observable<Int>.create { observer -> Disposable in for element in arr { // 이벤트 발생. 옵저버 패턴에서의 notify() observer.onNext(element) } observer.onCompleted() return Disposables.create() } } fromArray([1, 2, 5, 3, 4]) // 구독. 옵저버 패턴에서의 addObserver() .subscribe { // 이벤트가 next, error, complete 일 때 event in switch event { // 옵저버 패턴에서의 update() case .next(let value): print(value) case .error(let error): print(error) case .completed: print("completed") } } .dispose() // 1 // 2 // 5 // 3 // 4 // completed
( Observable 예제 1 )
- [ 불러오기 / 취소하기 ] 두 개의 버튼이 있다.
불러오기를 누르면 비동기 방식으로 이미지를 호출하고,
취소하기를 누르면 이미지를 호출하던 도중 호출을 취소한다.// Observable 함수 생성 func rxSwiftLoadImage(from imageUrl: String) -> Observable<UIImage?> { return Observable.create { seal in asyncLoadImage(from: imageUrl) { image in // 이벤트 생성 seal.onNext(image) seal.onCompleted() } // Observable 은 Disposable 을 반환 return Disposable.create() } } var dispoable: Disposable? // 불러오기 버튼 @IBAction func onLoadImage(_ sender: Any) { imageView.image = nil disposable = rxswiftLoadImage(from: LARGER_IMAGE_URL) // DispatchQueue.main 같은 역할 .observeOn(MainScheduler.instance) .subscribe({ result in switch result{ case let .next(image): self.imageView.image = image case let .error(err): print(err.localizedDescription) case .completed: break } }) } // 취소하기 버튼 @IBAction func onCancel(_ sender: Any) { disposable?.dispose() }
만약 RxSwift 를 사용하지 않고 날코딩을 했다면?
flag 를 세우고, cancel을 눌렀는지 주기적으로 확인해야하는 로직을 세워야한다.
- Observable 마다 dispose 를 해주지 않고, dispose 를 할 일들이 생길 때마다 DisposeBag 에 담아두었다가 한꺼번에 종료할 수 있다.
- 위 예제에서 DisposeBag 을 활용하는 방법
// DisposeBag 선언 var disposeBag: DisposeBag = DisposeBag() // 불러오기 버튼 @IBAction func onLoadImage(_ sender: Any) { imageView.image = nil disposable = rxswiftLoadImage(from: LARGER_IMAGE_URL) // DispatchQueue.main 같은 역할 .observeOn(MainScheduler.instance) .subscribe({ result in switch result{ case let .next(image): self.imageView.image = image case let .error(err): print(err.localizedDescription) case .completed: break } }) // disposeBag에 담아줌. .disposed(by: disposeBag) } // 취소하기 버튼 @IBAction func onCancel(_ sender: Any) { // DisposeBag 을 새로 선언해주면 자동으로 모두 dispose 된다. disposeBag = DisposeBag() }
연산자 doc : https://reactivex.io/documentation/ko/operators.html
1. just (생성)
Observable.create, next, error 를 생성해줄 필요 없이 값에 대한 Observable 을 바로 생성하고 subscribe 할 수 있다.
@IBAction func exJust1() { Observable.just("Hello World") .subscribe(onNext: { str in print(str) }) .disposed(by: disposeBag) } @IBAction func exJust2() { Observable.just(["Hello", "World"]) .subscribe(onNext: { arr in print(arr) }) .disposed(by: disposeBag) } // ["Hello", "World"]
2. from (생성)
컬렉션 타입에 들어있는 값들을 순차적으로 접근
@IBAction func exFrom1() { Observable.from(["RxSwift", "In", "4", "Hours"]) .subscribe(onNext: { str in print(str) }) .disposed(by: disposeBag) } /* RxSwift In 4 Hours */
3. map (변환)
@IBAction func exMap1() { Observable.just("Hello") // "Hello" 에 "RxSwift" 가 추가됨 .map { str in "\(str) RxSwift" } .subscribe(onNext: { str in print(str) }) .disposed(by: disposeBag) } // Hello RxSwift @IBAction func exMap2() { Observable.from(["with", "곰튀김"]) .map { $0.count } .subscribe(onNext: { str in print(str) }) .disposed(by: disposeBag) } /* 4 3 */
4. filter (필터링)
@IBAction func exFilter() { Observable.from([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) // filter 값이 True 일 때만 밑으로 흘러감 (stream) .filter { $0 % 2 == 0 } .subscribe(onNext: { n in print(n) }) .disposed(by: disposeBag) }
종합
@IBAction func exMap3() { Observable.just("800x600") // "800/600" .map { $0.replacingOccurrences(of: "x", with: "/") } // "https://picsum.photos/800/600/?random" .map { "https://picsum.photos/\($0)/?random" } // URL? .map { URL(string: $0) } // URL이 nil 이면 종료 .filter { $0 != nil } // URL .map { $0! } // Data .map { try Data(contentsOf: $0) } // UIImage .map { UIImage(data: $0) } .subscribe(onNext: { image in self.imageView.image = image }) .disposed(by: disposeBag) }
- Rx 연산자가 어떻게 실행되는지 흐름을 보여주는 그래프
- 마블을 이해할 수 있으면 Operator 를 사용할 수 있게 된다.
구슬 = 데이터
화살표 = 스트림
화살표 끝 작대기 = complete, disposable bag 에서 사라짐
map 을 보면 위에도 화살표가 있고 밑에도 화살표가 있다.
-> 스트림에서 스트림으로 간다는 뜻.구슬이 아닌 이런 번호가 적힌 원들은 드래그로 이동 가능
간혹 설명이 없는 Operator 들이 있는데, 그럴 땐 RxJava 에 가서 설명을 보면 된다.
내려온 구슬은 next, x 표시는 에러다.
- map 은 데이터를 넣으면 데이터가 나오는데, flatmap은 스트림이 나온다.
- 위 예제에서는 함정이 있다. 이미지 호출을 main 스레드에서 하고 있다는 것.
- 그래서 이미지를 호출하는 동안 스크롤을 해보면 스크롤이 동작하지 않는다.
- 다음과 같이 scheduler 를 활용해주면 해결할 수 있다.
- observeOn()
@IBAction func exMap3() { Observable.just("800x600") // [ 동시에 실행할 것이고, 메인쓰레드가 아닌 곳에서 실행 ] // qos 는 우선 순위 .observeOn(ConcurrentDispatchQueueScheduler(qos: .default)) // "800/600" .map { $0.replacingOccurrences(of: "x", with: "/") } // "https://picsum.photos/800/600/?random" .map { "https://picsum.photos/\($0)/?random" } // URL? .map { URL(string: $0) } // URL이 nil 이면 종료 .filter { $0 != nil } // URL .map { $0! } // Data .map { try Data(contentsOf: $0) } // UIImage .map { UIImage(data: $0) } // [ 이미지 호출은 메인 쓰레드에서 실행 ] .observeOn(MainScheduler.instance) .subscribe(onNext: { image in self.imageView.image = image }) .disposed(by: disposeBag) }
- subscribeOn(A)
subscribe 될 때부터 A 에서 코드를 실행하겠다는 뜻.
observeOn(B) 은 그 코드를 작성한 밑에 줄 부터 B 에서 실행을 하겠다는 뜻.
- Side effect : Observable 내부의 코드가 외부 영역에 영향을 미치는 경우. 예를들어 위 예시에서는 subscribe 안에서 imageView 의 image 를 변경하기 때문에 subscribe 코드 부분이 side effect 를 내는 부분이다.
- Observable 에서 side effect 를 낼 수 있게 하는 대표적 Opertor 두 가지
- subscribe()
- do()
@IBAction func exMap3() { Observable.just("800x600") // [ 동시에 실행할 것이고, 메인쓰레드가 아닌 곳에서 실행 ] // qos 는 우선 순위 .observeOn(ConcurrentDispatchQueueScheduler(qos: .default)) // "800/600" .map { $0.replacingOccurrences(of: "x", with: "/") } // "https://picsum.photos/800/600/?random" .map { "https://picsum.photos/\($0)/?random" } // URL? .map { URL(string: $0) } // URL이 nil 이면 종료 .filter { $0 != nil } // URL .map { $0! } // Data .map { try Data(contentsOf: $0) } // UIImage .map { UIImage(data: $0) } // [ 이미지 호출은 메인 쓰레드에서 실행 ] .observeOn(MainScheduler.instance) // [ do 를 사용한 side effect ] .do(onNext: { image in print(image?.size) }) .subscribe(onNext: { image in self.imageView.image = image }) .disposed(by: disposeBag) }
- RxCocoa : RxSwift 에 더해서, iOS 에서 UI 적인 요소들에 Rx 프로그래밍을 할 수 있게 해줌.
[ 실습 ]
E-mail 과 Password 창에 빨간 불이 들어와있다.
키보드 입력을 하다가 valid 한 값이 입력이 되는 순간 빨간 불을 사라지게 한다.
두 개의 빨간 불이 모두 사라질 경우 LOG IN 버튼이 활성화 되도록 한다.
Rx 없이 한다면 ?
- TextField delegate 를 단다.
- delegate 메서드에서 모든 조건을 체크한다.
Rx 를 사용한다면 ?
- UI에서 스트림이 흘러가다가 키보드 입력 이벤트가 발생하면 캐치한다.
- UI 에서 rx 를 적용한 경우
private func bindUI() { // "이제부터 얘가 취급하는 데이터를 비동기로 받겠다." idField.rx.text.subscribe(onNext: { s in print(s) }) .disposed(by: disposeBag) }
-> 키보드에 입력이 생길때마다 비동기로 반응한다.
- idField 와 pwField Rx 로 구현하기
idField.rx.text .filter { $0 != nil} .map { $0! } .map(checkEmailValid) .subscribe(onNext: { b in self.idValidView.isHidden = b }) .disposed(by: disposeBag) // orEmapty 를 활용하면 옵셔널 언래핑 과정이 필요없어짐. pwField.rx.text.orEmpty .map(checkPasswordValid) .subscribe(onNext: { b in self.pwValidView.isHidden = b }) .disposed(by: disposeBag)
- idField 의 결과와 pwField 의 결과를 비동기로 받아들여 LoginButton 구현하기 -> combineLatest 활용.
Observable.combineLatest( idField.rx.text.orEmpty.map(checkEmailValid), pwField.rx.text.orEmpty.map(checkPasswordValid), resultSelector: { s1, s2 in s1 && s2 } ) .subscribe(onNext: { b in self.loginButton.isEnabled = b }) .disposed(by: disposeBag)
- 위의 예제를 응용해서, 이렇게도 코드를 짤 수 있다.
// 심화과정. 이렇게도 코딩할 수 있다. // input : 키보드 입력 let idInputOb = idField.rx.text.orEmpty.asObservable() let idValidOb = idInputOb.map(checkEmailValid) let pwInputOb = pwField.rx.text.orEmpty.asObservable() let pwValidOb = pwInputOb.map(checkPasswordValid) // output : 불릿, 로그인 버튼 enable idValidOb.subscribe(onNext: { b in self.idValidView.isHidden = b }) .disposed(by: disposeBag) pwValidOb.subscribe(onNext: { b in self.pwValidView.isHidden = b }) .disposed(by: disposeBag) Observable.combineLatest(idValidOb, pwValidOb, resultSelector: { $0 && $1 }) .subscribe(onNext: { b in self.loginButton.isEnabled = b }) .disposed(by: disposeBag)
출처 : https://12bme.tistory.com/570
Observable에는 Hot Observable과 Cold Observable이 있다.
- Cold Observable
Observable 을 선언하고 just(), from() 등의 함수를 호출해도 subscribe() 구독을 하지 않으면 데이터를 발행하지 않는다. 다른말로 lazy 한 접근
- Hot Observable
구독자 존재 여부와 관계없이 데이터를 발행한다. 구독자는 Observable 에서 발행하는 데이터를 처음부터 모두 수신한다는 보장이 없다.
- 즉, Cold Observable 은 구독자가 구독하면 준비된 데이터를 처음부터 발행한다. 하지만 Hot Observable 은 구독한 시점부터 Observable 에서 발행한 값을 받는다.
- 예시
- Cold Observable
웹 요청, 데이터베이스 쿼리, 파일 읽기 등- Hot Observable
마우스 이벤트, 키보드 이벤트, 온도, 습도 등
- Hot Observable 은 지상파 방송, Cold Observable 은 넷플릭스
Subject 에 대해 잘 정리한 글 : https://brunch.co.kr/@tilltue/4
- Subject 도 결국은 Observable 이다.
subject 로 observable 을 사용하면 값을 외부에서 넣어줄 수 있게 된다.
- Subject 는 Cold Observable 을 Hot Observable 로 바꿔준다.
- 다음 4가지 subject 중 상황에 맞는 적절한 것을 사용한다.
- AsyncSubject
- PublishSubject
- BehaviorSubject
- ReplaySubject
- BehaviorSubject 활용 예시
// 전역 변수 let idValid: BehaviorSubject<Bool> = BehaviorSubject(value: false) ... // subject observable 을 사용하면 값을 외부에서 넣어줄 수 있게 됨. // just 나 from 은 처음부터 데이터를 만들고 나서 순차적으로 내보내는데, // subject 는 통로만 만들어놓고, 나중에 밖에서 넣어줄 수 있도록 함. // 외부에서 통제하는 observable 인 셈. idValidOb.subscribe(onNext: { b in self.idValid.onNext(b) })
- dipose 처리가 애매하게 되지 않는 Observable 들이 있다.
예를 들어, UI 입력을 받는 Observable 은 계속해서 사용자의 입력을 기다리기 때문에 complete 되지 않아서 메모리 누수의 위험이 발생할 수 있다.
- 그럴 경우엔 [weak self] 를 적절히 활용해준다던지 하는 고려가 필요하다.
- viewWillDisappear 에서 diposeBag 을 비워주는 방법도 있다.
- Pod 'RxViewController'
viewWillAppear 등의 메서드를 rx 로 활용할 수 있게된다.
예를들어, view 를 여러 번 들락날락 하는데, 그 중 1번 들어오면 실행하고, 그 뒤론 실행하고 싶지 않은로직이 있다면 다음과 같은 코드를 작성할 수 있다.@override func viewDidLoad() { super.viewDidLoad() self.rx.viewWillAppear() .take(1) .subscribe(...) }
그 외에 RxOptional, RxExtension 등 다양한 기능들이 있다.
RxSwiftCommunity, 또는 Cocoapods 사이트에서 공부할 수 있다.
출처 : fast campus
- Single, Maybe, Completable 은 좁은 범위의 Observable. 좁은 범위의 Observable 을 사용하는 이유는 코드 가독성을 높이기 위해서다.
- Single 은 Success event, Error event 를 한번만 방출한다.
Success = next + complete
- 정확히 한 가지 요소만 방출할 때 사용한다.
- Single 과 비슷. 성공적으로 complete 되더라도 아무런 값을 방출하지 않음.
- 예를 들어, 사진을 가지고 있는 포토 앨범앱이 있을 때,
새로운 앨범을 생성할 때 Maybe를 사용하면 좋다.
- asMaybe 를 붙여서 만들 수 있다.
- completed 또는 error 만 방출한다. 값은 아무것도 방출하지 않는다.
asMaybe, asSingle 처럼 asCompletable 같은 건 없다.
- completable create 로 생성해야한다.
- 데이터가 자동으로 저장되는 기능을 만들고 싶을 때, 백그라운드 큐에서 비동기로 작업하고, 저장이 되었는지 확인 하는 여부에서 completable 을 사용하면 좋다.