Scheduler

스케줄러 개념

  • 작업을 어느 문맥(컨텍스트)에서 실행할지 결정한다.
  • 다루기 어려운 스레드 개념을 스케줄러를 통해 구현 가능하게 해준다.
  • 필요한 작업을 다른 스케줄러에서 실행하여 비동기로 동작할 수 있도록 한다.
  • 별도로 스케줄러를 지정하지 않으면 현재 스레드에서 동작을 실행한다.

RxSwift 스케줄러 종류

MainScheduler

  • UI를 변경하거나 높은 수준의 작업을 위한 스케줄러이다.
  • 시간이 오래 걸리는 작업(네트워킹 등)은 여기서 실행되면 안된다.
  • UI를 새로 그리거나 업데이트할 때에는 여기서 실행하여야 한다.

SerialDispatchQueueSchedular

  • Serial 비동기 큐를 위해서 사용한다.
  • 시리얼이기 때문에 작업의 순서가 보장되므로 네트워킹 등에 적합하다.

ConcurrentDispatchQueueSchedular

  • Concurrent Queue로 동작하는 스케줄러이다.
  • Serial을 막지 않도록 여러 개의 작업이 한꺼번에 실행되어야 할 경우 유용하다,
  • 작업 완료 보장을 위해서는 concat()등과 같이 사용한다.

OperationQueueScheduler

  • Concurrent 스케줄러와 비슷하나 DispatchQueue가 아닌 OperationQueue에서 동작한다.
  • OperationQueue에서는 DispatchQueue보다 많은 옵션을 제공한다.
  • concurrent로 한꺼번에 수행할 작업의 개수를 정할 수 있다.

TestScheduler

  • 테스팅을 위한 스케줄러이다.
  • 테스트를 위해 작업시간을 조정하여 옵저버블의 값이 언제 발행될지 조정할 수 있다.

스케줄링 연산자

subscribeOn()

  • 이벤트를 발행하기 위한 옵저버블이 만들어지는 스케줄러를 지정한다.
  • 옵저버블이 만들어지는 부분에 대한 지정이므로 체이닝 위아래로 영향을 미친다.

observeOn()

  • 발행되는 이벤트를 처리하는 작업에 대해 어느 스케줄러에서 수행될지를 지정한다.
  • 발행되는 이벤트를 사용하는 부분에 대한 지정이므로 체이닝 아래쪽으로 영향을 미치게 되고, 결과적으로 항상 subscribeOn()에 대해 우선된다.
  • 대표적으로 데이터를 받아와 UI를 그리는 작업은 항상 메인 스레드에서 실행되어야 하기 때문에 .observeOn(Mainscheduler.instance)를 붙여준다.

let globalScheduler = ConcurrentDispatchQueueScheduler(qos: .default)
let mainScheduler = MainScheduler()

print("최상위 쓰레드: \(Thread.isMainThread ? "Main" : "Global")")
Observable<Int>.deferred {
    print("옵저버블 생성: \(Thread.isMainThread ? "Main" : "Global")")
    return Observable.just(100)
}
.map { num -> Int in
    print(" Map 연산자: \(Thread.isMainThread ? "Main" : "Global")")
    return num
}
.observe(on: mainScheduler) // 아래쪽 - subscribe() 스케줄러 지정
.subscribe(on: globalScheduler) // 전체 - Observable 생성(defer), map() 스케줄러 지정
.subscribe(onNext: { num in
    print("구독: \(Thread.isMainThread ? "Main" : "Global")")
}).disposed(by: self.disposeBag)

/* 결과

최상위 쓰레드: Main
옵저버블 생성: Global
Map 연산자: Global
구독: Main

*/

콜백 지옥


func createRelation(email: String, completion: @escaping (_: String) -> Void) {
    let url = URL(string: "https://api.eoflow.com/relations/v2")
    AF.request(.post, url!, parameters: ["peerEmail": email])
        .validate()
        .responseJSON { response in
            switch response.result {
            case .success(let data):
                guard let json = data as? [String: Any], let peerId = json["peerId"] as? String else { return }
                completion(peerId)
            case .failure(let error):
                print(error)
            }
        }
}

func getPhoto(peerUserID: String, completion: @escaping (_: String) -> Void) {
    let url = URL(string: "https://api.eoflow.com//relations/v2/peer-photo/\(peerUserID)")
    AF.request(url!, method: .get)
        .validate()
        .responseJSON { response in
            switch response.result {
            case .success(let data):
                guard let json = data as? [String: Any], let img = json["img"] as? String else { return }
                completion(img)
            case .failure(let error):
                print(error)
            }
        }
}

func createAndGetPhoto(email: String) {
    self.createRelation(email: email, completion: { id in
        self.getPhoto(peerUserID: id, completion: { img in
            self.setPhoto(img: img)
        })
    })
}

func setPhoto(img: String) {
    let image = UIImage(data: img.data(using: .utf8)!)
}

Rx를 이용한 콜백지옥 벗어나기


func rxCreateRelation(email: String?) -> Observable<String?> {
    guard let email = email else { return Observable.just(nil) }
    let url = URL(string: "")
    return Session().rx.request(.post, url!, parameters: ["peerEmail": email])
        .validate()
        .responseJSON()
        .map { response -> String? in
            switch response.result {
            case .success(let data):
                guard let json = data as? [String: Any], let peerId = json[""] as? String else { return nil }
                return peerId
            case .failure:
                return nil
            }
        }
}

func rxGetPhoto(peerUserID: String?) -> Observable<String?> {
    guard let peerUserID = peerUserID else { return Observable.just(nil) }
    let url = URL(string: "https://api.eoflow.com//relations/v2/peer-photo/\(peerUserID)")
    return Session().rx.request(.get, url!)
        .validate()
        .responseJSON()
        .map { response -> String? in
            switch response.result {
            case .success(let data):
                guard let json = data as? [String: Any], let img = json["img"] as? String else { return nil }
                return img
            case .failure:
                return nil
            }
        }
}

func rxCreateAndGetPhoto(email: String) {
    let source = PublishSubject<String>()
    source
        .concatMap { email in self.rxCreateRelation(email: email) }
        .concatMap { peerUserID in self.rxGetPhoto(peerUserID: peerUserID) }
        .subscribe(onNext: { img in
            guard let img = img else { return }
            self.setPhoto(img: img)
        }).disposed(by: self.disposeBag)
    
    source.onNext(email)
}