우선 곰튀김님 덕에 RxSwift 사용법을 쉽게 터득할 수 있었습니다.
https://www.youtube.com/channel/UCsrPur3UrxuwGmT1Jq6tkQw/playlists 해당 주소를 가서 RxSwift를 사용하는 법과 공부하는 방법을 배우시면 좋을 것 같습니다.
이번에는 CallbackHell을 탈출하기 위해 작성했던 Promise함수들을 RxSwift를 활용한 형태로 변형해보겠습니다.
우선 간단하게 ReactiveX를 알아보겠습니다.
1. ReactiveX
- ReactiveX는 An API for asynchronous programming with observable streams 영어 그대로 비동기 프로그래밍을 위한 Observable 스트림(연속된 데이터 흐름)을 이용하는 API 입니다.
- 비동기 작업을 위해 시퀀스 데이터와 이벤트를 이용하는 방식입니다.
- 하나의 Observable에서 데이터를 생성해서 Emit 했을 때 Observer가 진행할 행동을 미리 정의해놓는 방식으로 코딩하게 됩니다.
2. Observable
- Observable은 흔히 알고 있는 관찰자 패턴으로 내부에 데이터 조회 이후의 프로세스를 작성하여 Observable 객체가 이벤트를 실행시 연산을 진행하여 결괏값(Observable)을 반환하는 패턴을 가집니다.
- 결괏값도 Observable 형태로 Chaining을 통해 적은 타이핑으로 처리가 가능합니다.
- Observable은 Operator를 통해서 더 다양한 패턴을 작성할 수 있습니다.
3. Observer
- Observable안에는 Observer 객체가 내재되어 있습니다.
- Observable을 create할 때 Observer에 각각 .next, .error, .completed, .disposed 등의 이벤트가 발생했을 때 전달할 데이터나 Error등을 정의하게됩니다.
4. 지금까지의 간단한 예제
- 지금까지 설명한 Observable과 Observer를 통해 비동기와 이벤트를 섞어 흐름을 만드는 예시를 만들어보겠습니다.
// Observable 생성 예제
func test(sign: Bool) -> Observable<[Int]> {
return Observable.create { (observer) -> Disposable in // Observable 생성하기
if(sign) {
observer.onNext([1,2,3,4]) // Observable의 observer 객체가 이벤트 발생시 조건에 따라 던질 데이터 정의
}
observer.onError(OneLineReviewError.parsing(description: "make observable fail"))
return Disposables.create() // 데이터 흐름을 언제든 제거하기 위한 Disposebles 생성
}
}
// Observer가 보낸 데이터에 대한 행동 정의(일반적으로 viewDidLoad에 작성)
override func viewDidLoad() {
super.viewDidLoad()
self.test(sign: true).subscribe // Observable을 subscribe 구독을 시작하며
(onNext: { (items) in // Observer에서 일반적으로 데이터를 생성하여 전달하였을때의 로직 작성
print(items)
}, onError: { (err) in // Observer에 에러를 전달하였을 때의 로직 작성
print(err.localizedDescription)
}).disposed(by: disposeBag)
}
5. Operator
- Observable 객체를 생성하고 다양하게 활용할 수 있는 역할을 합니다.
- 단적인 예로, 서로 연관 되어있는 비동기 통신들의 경우 (ex. A 실행 결과값을 통해 B를 진행하는 경우) Operator 중 활용가능한 요소를 통해 Observable을 용도에 맞게 구성할 수 있습니다.
- 대표적으로 Observable을 생성하는 역할과 Observable의 결괏값들을 변형하는 Operator들로 구성되어있습니다.
이제 RxSwift를 통해 제 프로젝트의 비동기 함수들을 어떻게 처리했는지 설명하겠습니다.
1. 함수 구성도 설명 및 지금까지 상태
제 미니 프로젝트의 업로드 함수는 다음과 같은 구성을 가집니다. FireBase Storage를 통해 사진을 업로드하고 이후 업로드된 Storage 이미지의 DownLoad URL을 요청해 받아오고 받아온 URL과 Text, 기타 Data들을 FiraBase DataBase에 업로드합니다.
제 함수의 특징은 비동기적으로 동작하는 함수들이 순차적으로 이루어져야 한다는 것입니다.
사진을 올린 후에야 URL을 요청할 수 있고 URL을 받아온 이후에 DataBase에 업로드를 진행하기 때문입니다.
현재에는 PromiseKit을 통해 Promise를 통해 비동기함수들을 순차적으로 엮어놓은 형태입니다. 참고
2. RxSwift로 적용
가장 처음으로 Observable을 반환하는 함수 형태를 보겠습니다.
func Example(data: [String:String]) {
return Observable.create{ seal in
self.AsyncExample(data: data) { (err, result) in
guard let result = result else { return seal.onError(err!) }
seal.onNext(result)
}
}
}
Observable을 create Operator를 통해 생성하면 클로저의 첫번째 파라미터(seal)로 Observable 데이터를 읽을때 사용하는 subscribe의 옵션을 설정하는 RxSwift.AnyObserver<Self.Element> 형태의 객체가 전달됩니다. 해당 변수를 통해 onNext, onError, onCompleted 등을 설정할 수 있습니다.
이를 통해 작성한 함수는 다음과 같은 형태를 가집니다.
func fbUploadData(data: [String:String], url: String, completed: @escaping (Error?, DatabaseReference?) -> Void) {
var temp_data = data
temp_data["profileImageURL"] = url
let r_data = (temp_data as NSDictionary)
DispatchQueue.global().async {
Database.database().reference().child("footprintPosts").child(self.userInfo.uid).childByAutoId().setValue(r_data, withCompletionBlock: completed)
}
}
func rxUploadData(data: [String:String], url: String) -> Observable<Bool?> {
return Observable.create { seal in
self.fbUploadData(data: data, url: url) { (err, ref) in
guard let ref = ref else { return seal.onError(err!) }
seal.onNext(true)
seal.onCompleted()
}
return Disposables.create()
}
}
func fbUploadImage(_ title: String, _ data: Data, completed: @escaping (StorageMetadata?, Error?) -> Void) {
settingMeta.contentType = "image/jepg"
let r_storage = storage.child(userInfo.uid).child(title + Date.init().description)
recentStorage = r_storage
DispatchQueue.global().async {
r_storage.putData(data, metadata: self.settingMeta, completion: completed)
}
}
func rxUploadImage(_ title: String, _ data: Data) -> Observable<Bool?> {
return Observable.create { seal in
self.fbUploadImage(title, data) { smeta, err in
guard let smeta = smeta else { return seal.onError(err!) }
seal.onNext(true)
}
return Disposables.create()
}
}
func fbGetImageUrl(completed: @escaping (URL?, Error?) -> Void) {
guard let rStorage = recentStorage else { return }
DispatchQueue.global().async {
rStorage.downloadURL(completion: completed)
}
}
func rxGetImageUrl() -> Observable<String?> {
return Observable.create { seal in
self.fbGetImageUrl(){ url, err in
guard let url = url else { return seal.onError(err!) }
seal.onNext(url.absoluteString)
}
return Disposables.create()
}
}
우선 적용전에 제가 사용한 대표적인 Operator를 소개하겠습니다.
Operator: flatMapLatest
- 한 Observable에서 flatMapLatest(Another Observable)을 호출하면 자신을 수행한 이후 다음 Observable을 연산하도록 구성해줍니다.
이를 이용한 저의 업로드 함수의 구조는
rxUploadImage.flapMapLatest{ result in rxGetImageUrl }.flapMapLatest{ url in rxUploadData(url: url, data: data) }
이러한 형태를 가지게 됩니다.
이를 통해 작성한 호출 형태는 다음과 같습니다.
uploadImageOb.flatMapLatest{ b in downLoadUrlOb}
.flatMapLatest{ url in self.fireUtil.rxUploadData(data: postData, url: url!) }
.observeOn(MainScheduler.instance)
.subscribe(onNext: { result in
if result! {
SVProgressHUD.dismiss()
self.performSegue(withIdentifier: "registerEnd", sender: self)
} else {
SVProgressHUD.dismiss()
self.failRegister(message: "업로드실패")
}
}, onError: { err in
SVProgressHUD.dismiss()
self.failRegister(message: err.localizedDescription)
})
.disposed(by: disposeBag)
저의 RxSwift를 적용한 순서를 작성해보았습니다.
Promise 보다 연산자를 통해 간편하게 다양한 처리를 진행할 수 있어 유용한 것 같습니다.
다음에는 RxCoCoa도 적용하여 더 깔끔한 코드를 작성해보겠습니다.
'RxSwift' 카테고리의 다른 글
RxSwift Playground 파헤치기2 - Operator (feat. startWith) 구조 파악하기 (0) | 2021.04.04 |
---|---|
RxSwift Playground 파헤치기1 - Introduction (0) | 2021.03.28 |