RxSwift 를 배워보자
동기와 비동기. 어플리케이션 프로그래밍에서 빠질 수 없는 내용이다.
애플에서는 GCD : Grand Central Dispatch
일명 DispatchQueue 를 이용해 비동기 방식을 구현한다.
이 블로그에서 배운 토대로 설명을 간단하게 하자면
Sync 와 Async 에는 Concurrent (병렬) 과 Serial (직렬) 이 존재한다.
Concurrent 은 메인 쓰레드의 여러 Task들을 수많은 Thread에 나눠서 할당하고 처리한다.
Serial 은 메인쓰레드의 여러 Task들을 한 쓰레드에 할당하여 처리한다.
즉, Serial 방식을 여러 Thread에 사용하면 Concurrent 가 된다.
Sync 와 Async 를 구분하자면,
Url ( https:// appshawn.com ) 에서 받아온 이미지를 UIImageView에 띄운다고 가정하자.
Sync 한 코드 ( 그대로 이미지뷰에 띄우면 된다. ) 로 적으면,
Url 에서 이미지를 받아오는 0.x 초 동안은 쓰레드가 멈춰있다.
어떠한 기능도 이루어지지 않는다.
Async 한 코드 ( DispatchQueue.global.async ) 로 적으면,
Url에서 이미지를 받아오는 Task 가 다른 쓰레드에 할당되어 MainThread에서는 다른 Task를 처리할 수 있게된다.
한 가지 알아둬야 할 점이 있는데, 무작정 global DispatchQueue 안에 코드를 적다보면, UIImageView 는 꼭 main 쓰레드로 처리해주라는 오류 메세지가 뜬다. (쓰레드관련 에러는 보라색 에러 메세지를 띄운다) UIView나 UI와 관련된 것들은 꼭 main 쓰레드로 처리해줘야한다.
이번에 내가 앱스토어에 등록한 입사미 어플리케이션
입사미 에서는 동기, 비동기 방식을 제대로 설계하며 만들지 않았다.
다만, KingFisher이라는 라이브러리를 이용했는데
앞으로 다른 어플리케이션을 만들게 되면 sync, Async 를 생각하면서 설계를 해야겠다는 생각이 든다. ( 많이 빡셀것 같다 )
입사미 어플리케이션 에서는 KingFisher을 이용했지만
이번엔 PromiseKit을 간단하게 알아보려고 한다.
(JavaScript 에서도 유용하게 사용된다고 한다.)
1. 이미지 불러오기
RxSwift로 가는길에 있는 PromiseKit 을 보자면,
func promiseLoadImage(from imageUrl: String) -> Promise<UIImage?> {
return Promise<UIImage?>() { seal in
asyncLoadImage(from: imageUrl) { image in
seal.fulfill(image)
}
}
}
PromiseKit 으로 이미지를 받아오는 함수이다.
이미지를 받아, Promise 를 리턴한다.
2. 이미지 로드
이미지를 로드하는 함수를 보자
@IBAction func onLoadImage(_ sender: Any) {
imageView.image = nil
promiseLoadImage(from: LARGER_IMAGE_URL)
.done { image in
self.imageView.image = image
}.catch { error in
print(error.localizedDescription)
}
}
위에 promiseLoadImage 에서 Promise 를 리턴하면, .done 뒤에 클로저를 실행하고 에러가 발생하면 .catch 를 실행한다.
아주 간단한 방식으로 Async하게 이미지를 불러온다.
1. 이미지 불러오기
이번엔 RxSwift에서 이미지를 불러오는 함수를 살펴보자.
func rxswiftLoadImage(from imageUrl: String) -> Observable<UIImage?> {
return Observable.create { seal in
asyncLoadImage(from: imageUrl) { image in
seal.onNext(image)
seal.onCompleted()
}
return Disposables.create()
}
}
PromiseKit과 매우 유사한 것을 알 수 있다.
마찬가지로 Seal에 이미지를 넣어 Promise가 아닌 Observable을 리턴한다.
fulfill() 이 아닌, onNext 함수로 이미지를 전달한다.
2. 이미지 로드
이미지를 로드하는 함수를 살펴보자
@IBAction func onLoadImage(_ sender: Any) {
imageView.image = nil
_ = rxswiftLoadImage(from: LARGER_IMAGE_URL)
.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
}
})
}
.done() 이 아니라 .subscribe()로 실행한다.
PromiseKit 이나 RxSwift나
Async한 동작을 간단하게 구현할 수 있게 해주는 유틸리티이다.
그렇다면 어떤 차이점이 있을까.
우리가 RxSwift를 배워야 하는 이유는 무엇일까?
ReactiveX 공식홈페이지를 살펴보면
Docs 안에 다섯가지 문서들이 있다.
이것들을 우리가 공부해야한다.
3. Disposable
먼저 Disposable에 대해 알아보자면,
간단하게 불러오기를 클릭했을 때 이미지가 나오는 프로그램을 코딩해보자.
// MARK: - IBOutlet
@IBOutlet var imageView: UIImageView!
@IBOutlet var countLabel: UILabel!
// MARK: - IBAction
@IBAction func onLoadImage(_ sender: Any) {
imageView.image = nil
_ = rxswiftLoadImage(from: LARGER_IMAGE_URL)
.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) {
// TODO: cancel image loading
}
// MARK: - RxSwift
func rxswiftLoadImage(from imageUrl: String) -> Observable<UIImage?> {
return Observable.create { seal in
asyncLoadImage(from: imageUrl) { image in
seal.onNext(image)
seal.onCompleted()
}
return Disposables.create()
}
}
아까 했던 것 처럼 rxswiftLoadImage 에서 옵저버블을 리턴해서 .subscribe로 띄워주면 된다.
그런데 취소하기를 눌러 현재 진행되고 있는 Observable을 멈추고 싶다면?
disposable을 이용하면 된다.
var disposable: Disposable?
@IBAction func onLoadImage(_ sender: Any) {
imageView.image = nil
disposable = rxswiftLoadImage(from: LARGER_IMAGE_URL)
.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) {
// TODO: cancel image loading
disposable?.dispose()
}
밖에 disposable이라는 Disposable? 를 만들고,
func rxswiftLoadImage 로 만들어진 객체를 disposable로 선언해준다.
onCancel 을 누르면 disposable.dispose() 해주면 된다.
하지만, 어플리케이션들을 생각해보면 한개의 disposable만 있을 것 같지는 않다. 다수의 disposable을 처리하기위한 방안으로 DisposeBag이 있다.
DisposeBag 을 이용해서 여러 Disposable을 dispose 해보자.
4. DisposeBag
var disposebag: DisposeBag = DisposeBag()
@IBAction func onLoadImage(_ sender: Any) {
imageView.image = nil
let disposable = rxswiftLoadImage(from: LARGER_IMAGE_URL)
.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.insert(disposable)
}
@IBAction func onCancel(_ sender: Any) {
// TODO: cancel image loading
disposebag = DisposeBag()
}
disposebag 객체를 만들고, disposable을 insert 해준다.
disposebag 안에 disposable 들을 dispose 할 때,
.dispose() 가 없는 것을 확인 할 수 있다.
재 선언 ( disposebag = DisposeBag() )을 해주면 전부 dispose 가 된다.