[RxSwift] 개념 정리

김상우·2022년 4월 19일
5
post-thumbnail

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 함수형 프로그래밍을 가능하게 한다. 그리고 함수형 프로그래밍은 멀티쓰레드 환경에서 데드락이 발생하지 않도록 해준다.
  • RxSwift 는 선언형 프로그래밍을 지향한다. 선언형 프로그래밍은 명령형 프로그래밍의 반대말로, 어떤 방법(How)으로 동작하는지가 아니라 프로그래밍할 대상이 무엇(what)인지 알려주는 것을 의미한다.
  • 예를 들어, 명령형 프로그래밍에서는 실행할 알고리즘과 동작을 구체적으로 명시한다. 하지만 선언형 프로그래밍은 목표를 명시할 뿐 실행할 알고리즘을 명시하지 않는다.

Observer Pattern

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를 호출

ReactiveX

  • Observable Stream 을 가지고 비동기 프로그래밍을 하는 API

  • MS 사에서 만들었다.

  • RxJava, RxSwift, RxPython 등 왠만한 언어들에서 지원된다.

  • RxSwift 를 사용하는 이유는 결국 async 한 프로그래밍을 할 때 편리함을 주기 위해서.


Git clone

  • Xcode 에서 clone 한 프로젝트를 실행했을 때, 시뮬레이터가 등장하지 않는 작은 트러블 이슈가 있었는데 이 방법으로 해결했다.

-> https://blog.goodgods.com/474


Observable

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가지의 이벤트에 반응한다.
    1. next : 어떤 항목을 배출하는 것, 데이터의 발행을 알린다.
    2. error : 항목을 배출하다 에러가 발생한 경우. 스트림을 종료시킨다.
    3. 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을 눌렀는지 주기적으로 확인해야하는 로직을 세워야한다.


DisposeBag

  • 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()
}


Operator

연산자 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)
}

Marbels

https://rxmarbles.com/

  • Rx 연산자가 어떻게 실행되는지 흐름을 보여주는 그래프
  • 마블을 이해할 수 있으면 Operator 를 사용할 수 있게 된다.

구슬 = 데이터
화살표 = 스트림
화살표 끝 작대기 = complete, disposable bag 에서 사라짐

  • map 을 보면 위에도 화살표가 있고 밑에도 화살표가 있다.
    -> 스트림에서 스트림으로 간다는 뜻.

  • 구슬이 아닌 이런 번호가 적힌 원들은 드래그로 이동 가능

  • 간혹 설명이 없는 Operator 들이 있는데, 그럴 땐 RxJava 에 가서 설명을 보면 된다.

  • 내려온 구슬은 next, x 표시는 에러다.

  • map 은 데이터를 넣으면 데이터가 나오는데, flatmap은 스트림이 나온다.

Scheduler

  • 위 예제에서는 함정이 있다. 이미지 호출을 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

  • Side effect : Observable 내부의 코드가 외부 영역에 영향을 미치는 경우. 예를들어 위 예시에서는 subscribe 안에서 imageView 의 image 를 변경하기 때문에 subscribe 코드 부분이 side effect 를 내는 부분이다.
  • Observable 에서 side effect 를 낼 수 있게 하는 대표적 Opertor 두 가지
    1. subscribe()
    2. 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 실습

  • 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)

Hot & Cold Observable

출처 : 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

Subject 에 대해 잘 정리한 글 : https://brunch.co.kr/@tilltue/4

  • Subject 도 결국은 Observable 이다.
    subject 로 observable 을 사용하면 값을 외부에서 넣어줄 수 있게 된다.
  • Subject 는 Cold ObservableHot Observable 로 바꿔준다.
  • 다음 4가지 subject 중 상황에 맞는 적절한 것을 사용한다.
    1. AsyncSubject
    2. PublishSubject
    3. BehaviorSubject
    4. ReplaySubject
  • BehaviorSubject 활용 예시
// 전역 변수
let idValid: BehaviorSubject<Bool> = BehaviorSubject(value: false)
...

// subject observable 을 사용하면 값을 외부에서 넣어줄 수 있게 됨.
// just 나 from 은 처음부터 데이터를 만들고 나서 순차적으로 내보내는데,
// subject 는 통로만 만들어놓고, 나중에 밖에서 넣어줄 수 있도록 함.
// 외부에서 통제하는 observable 인 셈.
idValidOb.subscribe(onNext: { b in
	self.idValid.onNext(b)
})

Unfinished Observable

  • dipose 처리가 애매하게 되지 않는 Observable 들이 있다.
    예를 들어, UI 입력을 받는 Observable 은 계속해서 사용자의 입력을 기다리기 때문에 complete 되지 않아서 메모리 누수의 위험이 발생할 수 있다.
  • 그럴 경우엔 [weak self] 를 적절히 활용해준다던지 하는 고려가 필요하다.
  • viewWillDisappear 에서 diposeBag 을 비워주는 방법도 있다.

RxViewController

  • Pod 'RxViewController'

viewWillAppear 등의 메서드를 rx 로 활용할 수 있게된다.
예를들어, view 를 여러 번 들락날락 하는데, 그 중 1번 들어오면 실행하고, 그 뒤론 실행하고 싶지 않은로직이 있다면 다음과 같은 코드를 작성할 수 있다.

@override func viewDidLoad() {
	super.viewDidLoad()
  	
  	self.rx.viewWillAppear()
  		.take(1)
  		.subscribe(...)
}

그 외에 RxOptional, RxExtension 등 다양한 기능들이 있다.
RxSwiftCommunity, 또는 Cocoapods 사이트에서 공부할 수 있다.


Single

출처 : fast campus

  • Single, Maybe, Completable 은 좁은 범위의 Observable. 좁은 범위의 Observable 을 사용하는 이유는 코드 가독성을 높이기 위해서다.

  • Single 은 Success event, Error event 를 한번만 방출한다.
    Success = next + complete
  • 정확히 한 가지 요소만 방출할 때 사용한다.

Maybe

  • Single 과 비슷. 성공적으로 complete 되더라도 아무런 값을 방출하지 않음.
  • 예를 들어, 사진을 가지고 있는 포토 앨범앱이 있을 때,
    새로운 앨범을 생성할 때 Maybe를 사용하면 좋다.
  • asMaybe 를 붙여서 만들 수 있다.

Completable

  • completed 또는 error 만 방출한다. 값은 아무것도 방출하지 않는다.
    asMaybe, asSingle 처럼 asCompletable 같은 건 없다.
  • completable create 로 생성해야한다.
  • 데이터가 자동으로 저장되는 기능을 만들고 싶을 때, 백그라운드 큐에서 비동기로 작업하고, 저장이 되었는지 확인 하는 여부에서 completable 을 사용하면 좋다.
profile
안녕하세요, iOS 와 알고리즘에 대한 글을 씁니다.

0개의 댓글