iOS/RxSwift

RxSwift를 사용한 비동기 프로그래밍 #1

HaningYa 2020. 5. 27. 17:02
728x90

 

iamchiwon/RxSwift_In_4_Hours

RxSwift, 4시간 안에 빠르게 익혀 실무에 사용하기. Contribute to iamchiwon/RxSwift_In_4_Hours development by creating an account on GitHub.

github.com

 


[1교시] 개념잡기 - RxSwift를 사용한 비동기 프로그래밍

동작화면

textview 에 json 데이터를 띄우는 함수

버튼을 누르면 animation이 실행된다.

@IBAction func onLoad() {
        editView.text = ""
        setVisibleWithAnimation(activityIndicator, true)

        let url = URL(string: MEMBER_LIST_URL)!
        let data = try! Data(contentsOf: url)
        let json = String(data: data, encoding: .utf8)
        self.editView.text = json
        
        self.setVisibleWithAnimation(self.activityIndicator, false)
    }

해당 코드로 실행을 하면 시간도 멈추고 Animation도 적용이 안된체 데이터만 들고오게 된다. (동기화가 되어있다.)
--> 데이터를 다운로드 받을때 Time 도 멈추고 

 

Animation 도 나오고 Time 도 멈추지 않으면서 json을 다운받고 UI 업데이트 해주려면 어떻게 해야 할까. (비동기로 만들어 준다.)

--> 다른 쓰레드를 만들어 원하는 작업을 동시에 수행한다.

 

동시에 비동기로 진행하는 DispatchQueue.global.async 로 감싸주고 UI 업데이트 부분은 main 쓰레드에서 처리되도록 감싸준다.

    @IBAction func onLoad() {
        editView.text = ""
        self.setVisibleWithAnimation(self.activityIndicator, true)

        DispatchQueue.global().async {
            
            let url = URL(string: MEMBER_LIST_URL)!
            let data = try! Data(contentsOf: url)
            let json = String(data: data, encoding: .utf8)
            DispatchQueue.main.async {
                self.editView.text = json
                self.setVisibleWithAnimation(self.activityIndicator, false)
            }
            
        }
        
    }

만약 DispatchQueue를 깔끔하게 비동기 처리 부분에 구현하고 싶다면?

 

Json 다운받는 부분을 함수로 빼주고 completion handler를 통해 값을 return 해준다.

*@escaping은 써줘야 한다. (optional 일 경우는 default 이므로 명시안해도 된다.

 func downloadJson(_ url : String, _ completion: @escaping (String?) -> Void) {
        DispatchQueue.global().async {
            let url = URL(string: MEMBER_LIST_URL)!
            let data = try! Data(contentsOf: url)
            let json = String(data: data, encoding: .utf8)
            DispatchQueue.main.async {
                completion(json)
            }
        }
    }

@IBAction func onLoad() {
        editView.text = ""
        self.setVisibleWithAnimation(self.activityIndicator, true)
        
        downloadJson(MEMBER_LIST_URL) { json in
            self.editView.text = json
            self.setVisibleWithAnimation(self.activityIndicator, false)
        }
    }

 

Swift에서 이렇게 비동기 적으로 쓰면 된다.

 

하지만 계속 이런 방식으로 비동기 부분을 개발 할 경우 아래와 같이 여러번의 중첩된 비동기 처리에서 어떤게 먼저 리턴될지

 

순서를 보장할 수 없게된다. --> 계속 뎁스가 생기면서 관리가 힘들어 진다.

@IBAction func onLoad() {
        editView.text = ""
        self.setVisibleWithAnimation(self.activityIndicator, true)
        
        downloadJson(MEMBER_LIST_URL) { json in
            self.editView.text = json
            self.setVisibleWithAnimation(self.activityIndicator, false)
            
            self.downloadJson(MEMBER_LIST_URL) { json in
                self.editView.text = json
                self.setVisibleWithAnimation(self.activityIndicator, false)
            }
            
            self.downloadJson(MEMBER_LIST_URL) { json in
                self.editView.text = json
                self.setVisibleWithAnimation(self.activityIndicator, false)
            }
            
            self.downloadJson(MEMBER_LIST_URL) { json in
                self.editView.text = json
                self.setVisibleWithAnimation(self.activityIndicator, false)
            }
            
            self.downloadJson(MEMBER_LIST_URL) { json in
                self.editView.text = json
                self.setVisibleWithAnimation(self.activityIndicator, false)
            }
        }
    }

Completion handler 말고 return 값으로 처리하고 싶다!

쉽게 만들어 주는 PromiseKit, Bolt,  RxSwift 유틸리티를 사용하면 된다.

class 나중에생기는데이터<T> {
	private let task: (@escaping (T) -> Void) -> Void
    init(task: @escaping (@escaping (T) -> Void) -> Void) {
    	self.task = task
    }
    func 나중에오면(_ f: @escaping (T) -> Void){
    	tast(f)
    }
}


func downloadJson(_ url : String -> 나중에생기는데이터<String?>) 
	return 나중에 생기는 데이터(){ f in
    	f(json)
    }
    
let json:나중에생기는데이터<String?> = downloadJson(MEMBER_LIST_URL)
json.나중에오면 { json in
	UI뷰 업데이트
    }

 

  • Observable : 나중에 생기는 데이터 class
  • Observable.create() : 나중에 생기는 데이터 만들때
  • Subscribe : 나중에 오면
  • onNext : f에 바로 전달하는게 아닌  onNext로 전달
  • event : subscribe 하면 나중에 event 가 온다.
  • Disposable 로 작업 취소(create 했으면 Disposable 를 리턴 해야함)
    disposable.dispose() 로 작업을 바로 취소할 수 있다.

RxSwift를 적용한 코드

func downloadJson(_ url : String)-> Observable<String?> {
        return Observable.create() { f in
            DispatchQueue.global().async {
                let url = URL(string: MEMBER_LIST_URL)!
                let data = try! Data(contentsOf: url)
                let json = String(data: data, encoding: .utf8)
                
                DispatchQueue.main.async {
                    f.onNext(json)
                }
            }
            return Disposables.create()
        }
    }
 @IBAction func onLoad() {
        editView.text = ""
        self.setVisibleWithAnimation(self.activityIndicator, true)
        downloadJson(MEMBER_LIST_URL)
            .subscribe { event in
                switch event{
                case let .next(json):
                    self.editView.text = json
                    self.setVisibleWithAnimation(self.activityIndicator, false)
                    break
                case .completed:
                    break
                case .error(_):
                    break
                }
        }
    }
  • 데이터가 전달 될때는 .next
  • 데이터가 다 전달 됬을 때는 .completed

RxSwift 란 비동기적으로 생기는 데이터를 completion 같은 closure 로 전달하는게 아니라 return 값으로 전달하기 위해서 만들어진 유틸리티이다.

 

RxSwift의 Observable 을 class로 표현하면

class Observable<T>{
    private let task : (@escaping (T) -> Void) -> Void
    init(task: @escaping (@escaping (T) -> Void) -> Void){
        self.task = task
    }
    func subscribe(_ f: @escaping (T) -> Void){
        task(f)
    }
}

 

 

비동기적으로 발생하는 데이터를 리턴값으로 사용하고 subscribe 메소드로 사용한다.

 

* 클로져 내에있는 self들이 순환참조 될 수 있다.

{ event in
                switch event{
                case let .next(json):
                    self.editView.text = json
                    self.setVisibleWithAnimation(self.activityIndicator, false)
                    break
                case .completed:
                    break
                case .error(_):
                    break
                }
        }

이땐 f.onComplete 를 통해 클로져를 벗어나면서 해결할 수 있다.


RxSwift 사용방법 2개

  • 비동기로 생기는 데이터를 Observable로 감싸서 리턴하는 방법
    --> downloadJson 함수
  • Observable로 오는 데이터를 받아서 처리하는 방법
    --> downloadJson.subscribe 클로져

1. 비동기로 생기는 데이터를 Observable로 감싸서 리턴하기

  1. Observable.create() 한다.
  2. onNext로 데이터를 받는다.
  3. 데이터 다 받으면 onComplete()
  4. 마지막에 Disposables.create()
func downloadJson(_ url : String)-> Observable<String?> {
        
        //1. 비동기로 생기는 데이터를 Observable 로 감싸서 리턴하는 방법
        return Observable.create(){emitter in
            emitter.onNext("Hello")
            emitter.onNext("World")
            emitter.onCompleted()
            
            return Disposables.create()
        }
    }

 

그럼 함수의 event 에 Hello 와 World 가 오니까 최종적으로 World 만 찍혀있다.

 

URLSession task로 처리하면

  func downloadJson(_ url : String)-> Observable<String?> {
        
        //1. 비동기로 생기는 데이터를 Observable 로 감싸서 리턴하는 방법
        return Observable.create(){emitter in
            let url = URL(string: MEMBER_LIST_URL)!
            let task = URLSession.shared.dataTask(with: url){(data,_,err) in
                //err는 nil 이여야 하닌데 아닐땐 onError
                guard err == nil else{
                    emitter.onError(err!)
                    return
                }
                //데이터가 제대로 왔다면 onNext로 Json 접근
                if let data = data , let json = String(data: data, encoding: .utf8){
                    emitter.onNext(json)
                }
                //다 받았을 경우 끝
                emitter.onCompleted()
            }
            task.resume()
            
            //중간에 cancel 하면 task를 Cancel 한다.
            return Disposables.create() {
                task.cancel()
            }
        }
    }

 

URLSession 자체가 Main 쓰레드가 아닌 다른 쓰레드에서 처리되기 때문에 함수 호출 부분인 downloadJson 의 subscribe 부분 또한 URLSession과 같은 쓰레드에서 동작한다. 그래서 Main 쓰레드에서 UI 를 업데이트 하지 않기 때문에 에러가 발생한다.

 

이 부분을 main 쓰레드에서 실행해야한다.

  1. Observable을 create 한다. 
  2. 적절한 시점에 emitter의 err를 호출하거나 onNext로 데이터를 받거나 onComplete 으로 종료시킨다.
  3. 취소되었을때 수행해야되는 코드가 있다면 Dispsables.create(){ 코드구현 } 한다.

 

Observable 의 생명주기

  1. Create
  2. Subscribe
  3. onNext
    -----------끝----------
  4. onCompleted / onError
  5. Disposed

Observable은 subscribe 부분에서 실행이 된다.

downloadJson() 만으로 실행되지 않고 downloadJson().subscribe() 해야 실행이 된다.

 

동작이 끝난 Observable 은 재사용하지 못한다.

let ob = downloadJson(MEMBER_LIST_URL)
let disp = ob.subscribe ...
disp.dispose() //동작이 다 끝나고 없어짐
ob.동작 해도 수행안됨
ob.subscribe을 다시 해야함

 

debug()로 생명주기 확인

downloadJson(MEMBER_LIST_URL)
        .debug()
            .subscribe

디버그 찍어주면 subscribe에서 어떤 데이터가 왔는지 확인할 수 있다.

2020-05-27 23:45:07.460: ViewController.swift:93 (onLoad()) -> subscribed
2020-05-27 23:45:08.890: ViewController.swift:93 (onLoad()) -> Event next(Optional("데이터들"))
2020-05-27 23:45:08.949: ViewController.swift:93 (onLoad()) -> Event completed
2020-05-27 23:45:08.949: ViewController.swift:93 (onLoad()) -> isDisposed

 


2. Observable로 오는 데이터를 받아서 처리하는 방법

  1. observable 을 만든다.
  2. subscribe로 이벤트를 받는다.
  3. 이벤트의 next err complete 을 처리한다
  4. 필요에 따라 취소시킨다.
let observable = downloadJson(MEMBER_LIST_URL)
        let disposable = observable.subscribe{ event in
            switch event{
            case .next(let json):
                break
            case .error(let err):
                break
            case .completed:
                break
            }
            
        }
        disposable.dispose() // 로 필요에 따라 취소시킬 수 있다.

Sugar API - RxSwift를 더욱 쉽게 쓸 수 있는 API

굉장히 많다.

  • Observable.just
    - 어차피 데이터 하나 보낼 꺼면 간단하게 하자
    - dispose 되었을 때 따로 처리가 없을 때
func downloadJson(_ url : String)-> Observable<String?> {
        return Observable.just("Hello World")
        
//        return Observable.create { emitter in
//            emitter.onNext("Hello world")
//            emitter.onCompleted()
//            return Disposables.create()
//        }
    }

* 근데 데이터 두개 보내고 싶을 땐 : 배열을 써라

func downloadJson(_ url : String)-> Observable<[String?]> {
        return Observable.just(["Hello", "World"])
        
//        return Observable.create { emitter in
//            emitter.onNext("Hello world")
//            emitter.onCompleted()
//            return Disposables.create()
//        }
    }

* 근데 배열 한꺼번에 말고 하나씩 보내고 싶을 때 : from 

func downloadJson(_ url : String)-> Observable<String?> {
        return Observable.from(["Hello", "World"])
        
//        return Observable.create { emitter in
//            emitter.onNext("Hello world")
//            emitter.onCompleted()
//            return Disposables.create()
//        }
    }

  • subscribe 도 한줄로 onNext만 받아서 처리할 수 있다.
 _ = downloadJson(MEMBER_LIST_URL)
            .subscribe(onNext : {print($0)})
            
            
            
 _ = downloadJson(MEMBER_LIST_URL)
            .subscribe(
                onNext : {print($0)},
                onError: {print("error")}
                onCompleted: {print("complete")}
        )

 

  • Dispatch.main 으로 감싸주기 귀찮을 때
downloadJson(MEMBER_LIST_URL)
            .observeOn(MainScheduler.instance)
            .subscribe ( onNext: {json in
                self.editView.text = json
                self.setVisibleWithAnimation(self.activityIndicator, false)
        })

  • just from 생성을 할때 제공하는 suger
  • next, onError 처리할 때 제공하는 suger
  • observe 에서 subscribe로 데이터가 전달되는 중간에 데이터를 바꿔치기하는 operator suger
    - map, filter, 등등 겁나많다.
downloadJson(MEMBER_LIST_URL)
            .map{json in json?.count ?? 0}	// json이 count로 바껴서 내려오고 
            .filter{cnt in cnt > 0} 		// 필터에서 0보다 큰 값만 걸러지고 
            .map{"\($0)"}				// String 으로 바뀐 다음
            .observeOn(MainScheduler.instance)	// Main Thread 로 전환된다.
            .subscribe ( onNext: {json  in
                self.editView.text = json
                self.setVisibleWithAnimation(self.activityIndicator, false)
        })

[Operators 문서]

 

ReactiveX - Operators

Introduction Each language-specific implementation of ReactiveX implements a set of operators. Although there is much overlap between implementations, there are also some operators that are only implemented in certain implementations. Also, each implementa

reactivex.io

 

문서 보는법: 그림을 통해 데이터의 변환이나 operator 의 동작을 이해해라.

Just

 

ReactiveX - Just operator

In Swift, this is implemented using the Observable.just class method. The parameter, whether a tuple (i.e. (1, 2, 3)) or an array (i.e. [1,2,3]) is produced as one emission. Sample Code let source = Observable.just(1, 2, 3) source.subscribe { print($0) } l

reactivex.io

예를들어

  • 빨간 동그라미는 데이터 이다.
  • 데이터를 Just 에 넣으면
  • Observable이 생기고 (직선) 빨간데이터가 전달되고 complete 된다(세로 작대기)

[FROM]

 

ReactiveX - From operator

In RxJava, the from operator can convert a Future, an Iterable, or an Array. In the case of an Iterable or an Array, the resulting Observable will emit each item contained in the Iterable or Array. Sample Code Integer[] items = { 0, 1, 2, 3, 4, 5 }; Observ

reactivex.io

from

  • array가 from 에 전달되면
  • observable 이 생성되고 순서대로 전달이 끝나면 complete 된다.

[MAP]

 

ReactiveX - Map operator

RxJS implements this operator as map or select (the two are synonymous). In addition to the transforming function, you may pass this operator an optional second parameter that will become the “this” context in which the transforming function will execu

reactivex.io

map

  • observable 과 observable 사이에서 쓸 수 있다.
  • 변환 공식에 따라 바뀐 후 전달된다.

 

ReactiveX - ObserveOn operator

To specify on which Scheduler the Observable should invoke its observers’ onNext, onCompleted, and onError methods, use the observeOn operator, passing it the appropriate Scheduler. Sample Code /* Change from immediate scheduler to timeout */ var source

reactivex.io

  • 직선의 색은 Thread 의 종류를 나타낸다.
  • ObserveOn하면 Thread 가 바뀐다.
  • 파랑 쓰레드에서 observeOn(주황색) 하면 주황 쓰레드로 바뀐다.
  • 주황으로 바뀐후 map 을 해도 쓰레드의 종류는 바뀌지 않는다.
  • subscribeOn(파란색) 은 맨 처음 쓰레드에 영향을 준다. (첫째줄)
  • observeOn(핑크)로 다시 핑크 쓰레드로 바뀐다.
downloadJson(MEMBER_LIST_URL)
            .map{json in json?.count ?? 0}
            .filter{cnt in cnt > 0}
            .map{"\($0)"}
            .observeOn(MainScheduler.instance) // 메인쓰레드로 바뀌고
            .subscribeOn(ConcurrentDispatchQueueScheduler(qos:.default)) //default
            .subscribe ( onNext: {json  in
                self.editView.text = json
                self.setVisibleWithAnimation(self.activityIndicator, false)
        })
  • default qos를 가지는 Thread에서 처음부터 실행이 된다.
  • observeOn에서 Main Thread 가 바뀌고 나서 subscribe가 Main Thread 에서 실행된다.
  • subscribeOn의 위치는 어디든 상관없다. (처음 동작의 쓰레드를 지정하기 때문에)

 

728x90