본문 바로가기

RxSwift

RxSwift 사용기

우선 곰튀김님 덕에 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를 통해 비동기함수들을 순차적으로 엮어놓은 형태입니다. 참고

 

Swift CallBack Hell 탈출기 (feat.FireBase)

목차 FireBase 업로드 함수의 문제점 Promise 기초 사용 설명 FireBase와 어떻게 함께 사용할까? 장점 많이 부족합니다. 일단 사용법 위주로 설명드릴 것이고 개념적인 부분은 틀린 점이 많이 있을 것입니다. 피드..

jhmdevdiary.tistory.com

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도 적용하여 더 깔끔한 코드를 작성해보겠습니다.