본문 링크 (Original Link)

예제로 시작하는 RxSwift #4 – 멀티스레딩.

2017.10.14

#

by Łukasz Mróz, translated by pilgwon

titleImage

Rx에 대해서 말할 때 데이터 소스를 UI에 묶는 얘기로 종종 귀결됩니다. 예제에서도 확인할 수 있듯이 우리는 매 시리즈마다 데이터를 UI에 연결하고 있습니다.

이전 편에서 UI 바인딩 외에 또 데이터를 검색하는 것에 대해 알아보았습니다. 서버에서 온 데이터를 가지올 때, 대부분의 경우 어떻게든 그것을 파싱해야 사용할 수 있습니다. 만약 데이터가 충분히 많다면, 매핑하는 작업 자체로도 메모리와 시간을 소비합니다. 특히 그 작업이 메인 스레드에 스케쥴되어 있다면, UI가 멈출 것이고 우리 제품의 유저 경험에 대단히 충격적인 결과를 초래할 것입니다.

3편에서 우리는 오브젝트 매핑에 대해 알아보았습니다. 그리고 “데이터가 메인 스레드에 있다는 것을 확신하기” 때문에 몇몇 연산자에서 MainScheduler.instance를 사용했습니다. 사실은 이것은 스케쥴러(Scheduler)이지 스레드(Thread)가 아닙니다. 그런데 우리는 왜 스레드에 대해 이야기하고 있을까요? 게다가, 우리는 메인 스레드에서 오브젝트를 매핑하지 않아야 한다는 것을 배웠지만, 최근 예제에서 한 거 같습니다. 무슨일일까요?

오늘 글에서 이 모든것들에 대답해드릴 수 있습니다! 1편에서의 당신들을 기억해 보시고 음료 한 잔 하시고 시작해봅시다!

스케쥴러(Schedulers)

스케쥴러에 대한 몇 가지 이론에 대해 알아보는 것으로 시작하겠습니다. Rx로 연산자를 사용할때는 기본적으로 같은 스레드에서 작업됩니다. 스레드를 수동으로 바꾸지 않는 한, 엔트리 포인트는 현재 스레드에서 시작될것이고 같은 스레드에서 dispose될 것입니다.

스케쥴러는 실제로 진짜 스레드는 아닙니다. 하지만 이름이 뜻하는 것과 같이 그것들은 주어진 작업을 스케쥴링 하는 작업을 합니다. 그리고 스케쥴러에는 2가지 종류로, 시리얼(Serial)과 컨커런트(Concurrent)가 있습니다. 아래는 이미 만들어져있는 스케쥴러의 목록입니다:

흥미로운 점은 컨커런트한 큐를 시리얼 스케쥴러 에 보낸다면 RxSwift는 시리얼 큐 로 바꿔줍니다.

반대 경우(시리얼 큐컨커런트 스케쥴러 에 보내는 경우)도 있고 어떤 문제도 일으키지 않지만, 가능하면 피해주세요.

또한 자신만의 커스텀 스케쥴러를 구현하려면 이 문서가 매우 도움이 될 것입니다.

observeOn() 과 subscribeOn()

이 둘은 멀티스레딩에 있어서 가장 핵심이 되는 메소드입니다. 이들을 보면, 이름이 무엇을 하는지 정확히 알려준다고 생각하실 것입니다. 실제로 아주 적은 사람들만이 이 둘 사이의 차이점이 무엇인지 알고 사용할 줄 압니다. 하지만 잠시동안 잊고 친구가 불러서 잠깐동안의 여행을 떠난다고 생각해 봅시다!

흠.. 그정도의 여행은 아니지만, 매우 비슷합니다. 친구인 Emily 가 휴가동안 우리에게 고양이인 Ethan 을 부탁했습니다. 그녀는 돌아왔고 이제 고양이를 그의 엄마에게 돌려줄 때가 왔습니다. 우리는 즐거운 여행을 위해 준비해서 그녀의 집까지 몇 시간동안 운전을 해서 갔습니다!

보통 Emily의 집으로 갈 때의 기본 코스는 고속도로(highway)를 타는 것이었습니다. 하지만 오늘은 삶의 활력소를 위해 무언가를 살짝 바꿔보려고 했고 그래서 이차선 고속도로(freeway)로 가기로 정했습니다. 날씨는 몇 시간만 달리면 신선한 공기를 마실 수 있을 정도로 정말 좋습니다. 갑자기 무언가가 머리를 스쳤습니다. ‘이 날씨에는 고속도로(highway)에서 운전하는 것이 최고일 것이다!’ 맑은 공기가 우리에게 악영향을 끼친 바람에 우리는 익숙한 고속도로(highway)로 가기로 결정했습니다. 좋은 음악과 괜찮은 고양이 그리고 아름다운 날씨와 함께 운전을 해서 몇 시간 후에 우리는 에밀리를 만나고 고양이도 전달할 수 있게 되었습니다. 모두가 행복하죠! 그리고 우리는 observeOn()subscribeOn() 에 대해 배웠습니다.

좀 더 명확하게 하자면, 우리는 옵저버블(Observable)입니다. Ethan은 신호(Signal)이고, Emily에게 가는 길은 스케쥴러(Scheduler)이고 Emily는 옵저버(Observer)입니다. Emily는 우리를 구독하고 있고 새로운 신호(고양이)를 받게 될거라 믿고 있습니다. 우리에게는 Emily에게 운전해서 갈 때 정해진 기본 경로가 있습니다. Rx에도 똑같이 디폴트 스케쥴러가 있습니다. 하지만 이번에는 다른 경로(스케쥴러)를 시작점으로 선택했습니다. 기존의 경로와 다른 경로로 여행을 시작 하려고 할 때는 subscribeOn() 메소드를 사용합니다. 만약 subscribeOn()을 사용하면, 여행의 마지막(Emily의 subscribeNext())에 시작한 경로와 같은 경로에 있을거라 확신할 수 없습니다. 그냥 출발 지점만 보증되는 것입니다.

두 번째 메소드인 observeOn() 도 경로를 바꿀 수 있습니다. 하지만 여행의 시작점에 제한되지 않기때문에 언제든 observeOn()을 사용해서 경로를 바꿀 수 있습니다. 둘을 비교해보면, subscribeOn()은 시작점에서만 경로를 변경할 수 있습니다. 이것이 차이점입니다. 대부분의 경우 observeOn()을 사용합니다.

이제 고양이 배달로 돌아가 봅시다. 이 배달을 RxSwift의 수도코드로 적어보면 아래와 같을 것입니다:

catObservable // 1
    .breatheFreshAir() // 2
    .observeOn(MainRouteScheduler.instance) // 3
    .subscribeOn(TwoLaneFreewayScheduler.instance) // 4
    .subscribeNext { cat in // 5
        if cat is Ethan {
            hug(cat)
        }
    }
    .addDisposableTo(disposeBag)

단계별로 알아보겠습니다:

  1. 우리는 Cat 신호를 발생하는 Cat 옵저버블을 구독합니다.

  2. 구독(Rx의 기본 행동)하기 전에 같은 스케쥴러에 있어야 합니다.

  3. 스케쥴러를 MainRouteScheduler 로 변경합니다. 이제 이후의 모든 연산자는 MainRouteScheduler 에서 스케쥴링 됩니다. (당연히 나중에 스케쥴러를 다시 바꾸지 않는다면요.)

  4. 이제 TwoLaneFreewayScheduler 의 체인을 시작합니다. 그러면 breatheFreshAir()TwoLaneFreewayScheduler 위에서 스케쥴링 됩니다. 그리고 observeOn() 을 사용하면 스케쥴러는 또 바뀝니다.

  5. subscribeNext()MainRouteScheduler 에서 스케쥴링 됩니다. 전에 observeOn() 을 추가하지 않았다면 TwoLaneFreewayScheduler 에서 스케쥴링 됐을 것입니다.

요약하자면: subscribeOn() 은 전체 체인의 시작점을 지정하고, observeOn() 은 다음에 어디를 목표로 할 지 지정합니다. 아래의 이미지(reactivex.io에서 제공)는 이 메소드들이 호출됐을 때 어떤 일이 일어날 지에 대해 명백하게 알려줍니다. 파랑 화살표는 subscribeOn() 스케쥴러를 나타내고 주황과 핑크는 observeOn()이 호출되었을 때 두 개의 다른 스케쥴러를 보여줍니다.

이 이미지를 이해하셨다면 이제 예제로 갈 수 있습니다!

예제

3편(이 예제에서 꼭 필요한 내용이 있기 때문에 보고 오시는 것을 추천드립니다!)에서 주어진 GitHub의 레포지토리의 이슈를 검색했었습니다. 오늘은 GitHub에서 유저의 이름으로도 검색할 수 있는 예제를 만들어 볼 것입니다. 하지만 이번에는 Alamofire로 네트워크 리퀘스트를 하고 ObjectMapper로 오브젝트를 파싱할 것입니다. 놀라운 RxSwift 커뮤니티 덕분에, Alamofire와 RxSwift를 합친 익스텐션이 있고 이 전 글에서 말했듯이 RxAlamofire라고 부릅니다.

먼저 항상 그랬듯이 프로젝트를 만들겠습니다. 그리고 CocoaPods를 써서 RxAlamofire/RxCocoa(RxSwift, RxCocoa, Alamofire를 디펜던시로 가지고 있는)와 ObjectMapper를 사용할 것입니다. Podfile은 아래와 같습니다:

platform :ios, '8.0'
use_frameworks!

target 'RxAlamofireExample' do

pod 'RxAlamofire/RxCocoa'
pod 'ObjectMapper'

end

이제 코딩 부분으로 넘어가겠습니다: 이미 아시겠지만, 본론으로 들어가기 전에 윤곽을 잡아두는 것은 매우 도움이 되고, 그래서 무엇이 필요하고 어떻게 접근해야 하는지에 대해 생각해 볼 것입니다. 예제의 윤곽은 아래와 같을 것입니다:

  1. UI를 만듭니다. 아마 기본 UISearchBarUITableView 일 것입니다.

  2. 검색바를 관찰하고, 새로운 값이 주어질 때마다 (가능하면) 레포지토리의 배열로 변환합니다. 여기서 네트워크 리퀘스트를 위한 모델을 필요로 합니다.

  3. 테이블 뷰를 새로운 데이터로 업데이트 합니다. 우리는 스케쥴러에 대해 생각해야 하고 UI가 흘러넘치지 않도록 해야 합니다.

1단계 - 컨트롤러와 UI.

다시 UITableViewUISearchBar 부터 시작해보겠습니다. 위의 GIF의 디자인이랑 똑같이 해도 되고, 아니면 자신만의 디자인을 만드셔도 됩니다. 자신에게 가장 어울리는걸로 고르세요!

다음으로 모든 것을 관리할 컨트롤러가 필요합니다. 텍스트 필드를 관찰하는 것부터 시작해서 검색된 레포지토리들을 테이블 뷰에 그리는 역할을 합니다. RepositoriesViewController.swift라는 새로운 파일을 만들어 봅시다. 그리고 모듈을 임포트하고 기본 설정을 준비합니다:

import UIKit
import ObjectMapper
import RxAlamofire
import RxCocoa
import RxSwift

class RepositoriesViewController: UIViewController {

    @IBOutlet weak var tableView: UITableView!
    @IBOutlet weak var searchBar: UISearchBar!

    override func viewDidLoad() {
        super.viewDidLoad()
        setupRx()
    }

    func setupRx() {
    }
}

코드가 Rx에 아주 깊이 의존하고 있기 때문에 또 setupRx() 메소드를 준비해두었습니다.

이제 이전 예제(throttle()distinctUntilChanged() 포함)처럼 검색바의 rx_text 속성의 옵저버블을 만들어봅시다. 하지만 빈 값은 원하지 않기 때문에 이번에는 필터를 추가하겠습니다. 그럴 경우에는 마지막의 리퀘스트를 테이블 뷰에 그대로 둘 것입니다. 그래서 나온 옵저버블은 다음과 같을 것입니다:

class RepositoriesViewController: UIViewController {
    ...
    var rx_searchBarText: Observable<String> {
        return searchBar
            .rx_text
            .filter { $0.characters.count > 0 } // 새로운 filter를 확인하세요
            .throttle(0.5, scheduler: MainScheduler.instance)
            .distinctUntilChanged()
    }
    ...
}

그리고 그것을 RepositoryViewController 의 변수로 추가할 것입니다. 이제 Observable<[Repository]> 로 변환된 새로운 옵저버블과 UITableView 사이의 연결을 추가해야 합니다. 이전에도 해본 내용입니다!

2단계 - 네트워크 모델과 오브젝트 매핑

맞습니다, 네트워크 모델입니다! 하지만 그 전에, 우리는 오브젝트를 매핑하는 것을 설정해놓아야 합니다. 이번에는 다른 매퍼를 사용할 것입니다. 많은 좋은 선택이 있고 가장 좋아하는 것으로 사용하면 됩니다. ObjectMapper 는 훌륭한 매퍼이고 설정하는 방법은 Mapper 라이브러리 설정하는 법과 비슷합니다. Repository.swift 라는 새로운 파일을 만들겠습니다. 그리고 Repository 오브젝트를 위한 매핑 설정을 구현해보겠습니다:

import ObjectMapper

class Repository: Mappable {
    var identifier: Int!
    var language: String!
    var url: String!
    var name: String!

    required init?(_ map: Map) { }

    func mapping(map: Map) {
        identifier <- map["id"]
        language <- map["language"]
        url <- map["url"]
        name <- map["name"]
    }
}

완벽합니다! 여기까지가 본론으로 들어가기 위한 준비였습니다…

이제 컨트롤러도 가지고 있고, Repository 오브젝트를 위한 모델도 가지고 있습니다… 맞습니다 이제 네트워크 모델 차례입니다!

모델을 특정한 Observable<String> 를 사용해서 초기화하고 Observable<[Repository]> 를 반환하는 메소드를 구현할 것입니다. 이제 그것을 RepositoriesViewController 의 뷰에 연결할 수 있습니다. RepositoryNetworkModel.swift 의 초기 구현은 아래와 같을것입니다:

import ObjectMapper
import RxAlamofire
import RxCocoa
import RxSwift

struct RepositoryNetworkModel {

    private var repositoryName: Observable<String>

    private func fetchRepositories() -> Driver<[Repository]> {
        ...
    }

}

첫눈엔 꽤 캐쥬얼 해 보입니다. 하지만 더 자세히 보면, 실제로 Observable<[Repository]> 를 반환하지 않고 대신에 Driver<[Repository]> 를 반환하고 있습니다. 이 Driver 친구는 무엇이고 왜 저를 계속 따라다니는 걸까요? 😤

음… 우리는 오늘 스케쥴러에 대해 얘기하고 있습니다. 그리고 데이터를 UI에 묶으려고 할 때, 언제나 MainScheduler 를 사용하려고 합니다. 이것이 기본적으로 Driver 의 역할입니다. Driver 는 “좋아 친구, 나는 메인 스레드에 올라갈테니 걱정말고 나를 바인딩해!” 라고 말하는 변수(Variable) 라고 볼 수 있습니다. 이렇게 하면 바인딩할 때 오류가 발생하지 않고 연결을 안전하게 수행 할 수 있습니다.

구현 방법은 어떨까요? 이전에 사용했던 flatMapLatest() 부터 시작하고 Observable<String>Observable<[Repositories]> 로 변경해보겠습니다:

struct RepositoryNetworkModel {
    ...
    private func fetchRepositories() -> Driver<[Repository]> {
        return repositoryName
            .flatMapLatest { text in
                return RxAlamofire
                    .requestJSON(.GET, "https://api.github.com/users/\(text)/repos")
                    .debug()
                    .catchError { error in
                        return Observable.never()
                    }
            }
            .map { (response, json) -> [Repository] in
                if let repos = Mapper<Repository>().mapArray(json) {
                    return repos
                } else {
                    return []
                }
            }
    }
    ...
}

흠, 우선 그것은 다르게 보이고, flatMapLatest() 에 더해서 다른 map() 도 볼 수 있습니다. 하지만 실제론 무서워하지 않아도 됩니다. flatMapLatest() 에서 보통의 네트워크 리퀘스트를 하고, 만약 어떤 에러라도 있다면 Observable.never() 를 사용해서 파이프라인을 멈춥니다. 그러면 Alamofire 의 리스폰스를 Observable<[Repository]> 로 매핑하는 작업을 합니다. 우리는 map()flatMapLatest() 안에서(catchError() 이후에) 연결할수도 있지만, 나중에 flatMapLatest() 의 바깥에서 필요해서 그렇게 한 것이고 지금은 그저 선호도의 차이입니다.

맞습니다, 위의 코드는 컴파일되지 않을테니(Driver 를 반환할거라 예상했는데 Observable 을 반환하기 때문에) 더 깊이 알아보겠습니다. 어떻게 Observable<[Repository]>Driver<[Repository]> 로 바꿀까요? 그건 매우 쉬운 일입니다. 어떤 Observable 이든 asDriver() 연산자를 사용하면 Driver 로 바꿀 수 있습니다. 이 경우엔, .asDriver(onErrorJustReturn: []) 를 사용할 것이고 이 연산자는 만약 체인에서 에러가 발생했을 때(하지만 이전에 우리가 막아두었기 때문에 거의 일어나지 않을 것입니다), 빈 배열을 반환합니다. 여기까지 입니다! 작동되는 코드는 다음과 같습니다:

struct RepositoryNetworkModel {
    ...
    private func fetchRepositories() -> Driver<[Repository]> {
        return repositoryName
            .flatMapLatest { text in
                return RxAlamofire
                    .requestJSON(.GET, "https://api.github.com/users/\(text)/repos")
                    .debug()
                    .catchError { error in
                        return Observable.never()
                    }
            }
            .map { (response, json) -> [Repository] in
                if let repos = Mapper<Repository>().mapArray(json) {
                    return repos
                } else {
                    return []
                }
            }
            .asDriver(onErrorJustReturn: []) // 우리가 MainScheduler에 있다는 것을 확인해줍니다
    }
    ...
}

완벽합니다! 보면 아시겠지만 observeOn() 이나 subscribeOn() 을 건드리지도 않았지만 이미 스케쥴러를 2번이나 변경했습니다! 첫번째는 throttle()을 이용한 방법이고, 이번에는 우리가 MainScheduler에 있다는 것을 확인하게 해주는 asDriver() 입니다. 그리고 이것은 시작에 불과합니다. 일단 코드는 작동하고, 마지막으로 해야 할 일은 레포지토리들을 뷰 컨트롤러의 RepositoryNetworkModel 에 연결하는 일입니다. 하지만 그 전에 그 메소드를 다른 것으로 바꿔봅시다. 왜냐하면 그 방법은 사용할 때마다 새롭게 파이프라인을 만들기 때문입니다. 대신에, 저는 속성을 사랑하고 싶습니다. 그러나 계산된 속성은 아닙니다. 왜냐하면 결과가 메서드와 동일하기 떄문입니다. 대신에, lazy var 를 만들어서 메소드에게 레포지토리들을 검색하도록 하겠습니다. 이 방법에서는 여러개의 시퀀스를 만드는 것을 피할 것입니다. 또한 우리는 속성이 아닌 모든것들을 숨겨야 할 것이고, 이 모델을 사용하는 모두가 올바른 Driver 속성을 받는다는 것을 확실히 해야 합니다. 이 해결책의 유일한 단점은 Struct에 init을 명시적으로 입력해야 한다는 사실입니다. 하지만 이것은 공정 거래라고 생각합니다. 그래서 지금 모델의 완성형은 아래와 같을 것입니다:

struct RepositoryNetworkModel {

    lazy var rx_repositories: Driver<[Repository]> = self.fetchRepositories()
    private var repositoryName: Observable<String>

    init(withNameObservable nameObservable: Observable<String>) {
        self.repositoryName = nameObservable
    }

    private func fetchRepositories() -> Driver<[Repository]> {
        return repositoryName
            .flatMapLatest { text in
                return RxAlamofire
                    .requestJSON(.GET, "https://api.github.com/users/\(text)/repos")
                    .debug()
                    .catchError { error in
                        return Observable.never()
                    }
            }
            .map { (response, json) -> [Repository] in
                if let repos = Mapper<Repository>().mapArray(json) {
                    return repos
                } else {
                    return []
                }
            }
            .asDriver(onErrorJustReturn: []) // 우리가 MainScheduler에 있다는 것을 확인해줍니다
    }
}

훌륭합니다! 이제 데이터를 뷰 컨트롤러에 연결해보겠습니다. 우리가 Driver 를 테이블 뷰에 바인딩하려고 할 때, bindTo (이전에 사용했던) 대신에 drive() 연산자를 사용할 것이지만 문법을 포함한 모든 것들이 bindTo 와 같습니다. 데이터를 테이블 뷰에 바인딩하는 것 외에도, 다른 구독을 만들어서 0개의 레포지토리를 가질 때마다 경고창을 보여줄 것입니다.

RepositoryViewController 클래스의 완성형은 아래와 같습니다:

class RepositoriesViewController: UIViewController {

    @IBOutlet weak var tableViewBottomConstraint: NSLayoutConstraint!
    @IBOutlet weak var tableView: UITableView!
    @IBOutlet weak var searchBar: UISearchBar!
    let disposeBag = DisposeBag()
    var repositoryNetworkModel: RepositoryNetworkModel!

    var rx_searchBarText: Observable<String> {
        return searchBar
            .rx_text
            .filter { $0.characters.count > 0 }
            .throttle(0.5, scheduler: MainScheduler.instance)
            .distinctUntilChanged()
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        setupRx()
    }

    func setupRx() {
        repositoryNetworkModel = RepositoryNetworkModel(withNameObservable: rx_searchBarText)

        repositoryNetworkModel
            .rx_repositories
            .drive(tableView.rx_itemsWithCellFactory) { (tv, i, repository) in
                let cell = tv.dequeueReusableCellWithIdentifier("repositoryCell", forIndexPath: NSIndexPath(forRow: i, inSection: 0))
                cell.textLabel?.text = repository.name

                return cell
            }
            .addDisposableTo(disposeBag)

        repositoryNetworkModel
            .rx_repositories
            .driveNext { repositories in
                if repositories.count == 0 {
                    let alert = UIAlertController(title: ":(", message: "No repositories for this user.", preferredStyle: .Alert)
                    alert.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil))
                    if self.navigationController?.visibleViewController?.isMemberOfClass(UIAlertController.self) != true {
                        self.presentViewController(alert, animated: true, completion: nil)
                    }
                }
            }
            .addDisposableTo(disposeBag)
    }
}

이 코드에서 유일하게 새로운 것은 driveNext() 연산자이지만, 추측할 수 있듯이 이것은 그냥 Driver 를 위한 subscribeNext 입니다.

2단계도 끝입니다! 다음 단계로 넘어가시죠!

3단계 - 멀티스레딩 최적화

이제 추측하셨듯이, 사실 우리가 했던 모든 것들이 MainScheduler 위에서 작동했습니다. 왜그럴까요? 체인이 searchBar.rx_text 에서 시작하고 이것이 MainScheduler 위에 있다는 것이 보증되기 때문입니다. 그리고 모든 것들이 기본으로 현재 스케쥴러에서 작동하기 때문에 UI 스레드가 압도당할 수 있습니다. 어떻게 예방할까요? 리퀘스트와 매핑을 하기 전에 백그라운드 스레드로 변경하고 UI는 메인스레드에서 업데이트하면 됩니다:

struct RepositoryNetworkModel {
    ...
    private func fetchRepositories() -> Driver<[Repository]> {
        return repositoryName
            .observeOn(ConcurrentDispatchQueueScheduler(globalConcurrentQueueQOS: .Background))
            .flatMapLatest { text in // .Background 스레드, 네트워크 리퀘스트
                return RxAlamofire
                    .requestJSON(.GET, "https://api.github.com/users/\(text)/repos")
                    .debug()
                    .catchError { error in
                        return Observable.never()
                    }
            }
            .observeOn(ConcurrentDispatchQueueScheduler(globalConcurrentQueueQOS: .Background))
            .map { (response, json) -> [Repository] in // 다시 .Background 스레드로 돌아가서 오브젝트 매핑
                if let repos = Mapper<Repository>().mapArray(json) {
                    return repos
                } else {
                    return []
                }
            }
            .asDriver(onErrorJustReturn: []) // 우리가 MainScheduler에 있다는 것을 확인해줍니다
    }
    ...
}

그런데 왜 observeOn() 을 같은 방법으로 두 번이나 사용하는 걸까요? 왜냐하면 위의 코드에서 requestJSON 이 시작한 스레드에서 데이터를 반환하는지 모르기 때문입니다. 그래서 우리는 매핑을 위해 백그라운드 스레드에 있는지 계속 확인해야 합니다.

이제 백그라운드 스레드에서 매핑하게 되었고 결과는 UI 스레드에 배달되었습니다. 무언가를 더 요청할 수 있을까요?! 당연하죠! 우리는 유저가 네트워크 리퀘스트가 진행되고 있다는 것을 알게 해주고 싶습니다. 그러기 위해 스피너라고 잘 알려진 UIApplication.sharedApplication().networkActivityIndicatorVisible 속성을 사용할 것입니다. 하지만 우리가 리퀘스트나 매핑하는 도중에 UI를 업데이트하려 하기 때문에, 스레드를 다루는 일에 조심해야 합니다. 또한 doOn() 라고 불리는 멋진 메소드를 사용하여 특정 이벤트(.Next, .Error 등)에서 원하는대로 할 수 있습니다. flatMapLatest() 전에 스피너를 보여주고 싶어 한다고 칩시다. doOn 은 그것을 할 수 있게 해줍니다. 그리고 액션이 수행되기 전에 MainScheduler 로 변경해야 합니다.

레포지토리를 검색하는 전체 코드는 다음과 같습니다:

struct RepositoryNetworkModel {

    lazy var rx_repositories: Driver<[Repository]> = self.fetchRepositories()
    private var repositoryName: Observable<String>

    init(withNameObservable nameObservable: Observable<String>) {
        self.repositoryName = nameObservable
    }

    private func fetchRepositories() -> Driver<[Repository]> {
        return repositoryName
            .subscribeOn(MainScheduler.instance) // 우리가 MainScheduler 위에 있다는 것을 확인
            .doOn(onNext: { response in
                UIApplication.sharedApplication().networkActivityIndicatorVisible = true
            })
            .observeOn(ConcurrentDispatchQueueScheduler(globalConcurrentQueueQOS: .Background))
            .flatMapLatest { text in // .Background 스레드, 네트워크 리퀘스트
                return RxAlamofire
                    .requestJSON(.GET, "https://api.github.com/users/\(text)/repos")
                    .debug()
                    .catchError { error in
                        return Observable.never()
                    }
            }
            .observeOn(ConcurrentDispatchQueueScheduler(globalConcurrentQueueQOS: .Background))
            .map { (response, json) -> [Repository] in // 다시 .Background 스레드로 돌아가서 오브젝트 매핑
                if let repos = Mapper<Repository>().mapArray(json) {
                    return repos
                } else {
                    return []
                }
            }
            .observeOn(MainScheduler.instance) // UI 업데이트를 위해 MainScheduler로 변경
            .doOn(onNext: { response in
                UIApplication.sharedApplication().networkActivityIndicatorVisible = false
            })
            .asDriver(onErrorJustReturn: []) // 우리가 MainScheduler에 있다는 것을 확인해줍니다
    }
}

끝입니다! 이제 우리는 파싱할 때 스레드에 대해 상관하지 않아도 되는 이유를 알고있습니다: Moya-ModelMapper의 익스텐션이 우리를 위해 스케쥴러를 변경해줍니다.

완성된 앱은 다음과 같이 작동할 것입니다:

이번 편은 다른 편보다 좀 더 길었고 예제도 쉽지 않았는데 참고 따라와주신 것에 감사드립니다.

항상 그렇듯이, 피드백과 아이디어, 개선 사항 등을 트위터, 이메일 또는 댓글로 알려주세요. 여러분의 메세지는 항상 감사하는 마음을 가지게 만듭니다! ✌️

또한 저는 RxSwift의 리소스를 향상시키고 있고 여기서 확인 가능 합니다. 그리고 구독(😎) 을 해서 RxSwift와 다른 시리즈들에 대한 최신 정보를 받으세요.

전체 소스 코드는 Droids on Roids의 깃헙에서 볼 수 있고 여기서 다른 RxSwift 예제들을 확인할 수 있습니다!

RxSwift에 관련된 다른 게시글을 읽어보세요 (영어 원문)

» RxSwift by Examples #3 – Networking.

RxSwift에 관련된 다른 게시글을 읽어보세요 (한글 번역) (일부 번역 예정입니다.)

» 예제로 시작하는 RxSwift #3 – 네트워킹.