swift

RxSwift: ReactiveX

kimyounggyun 2022. 7. 28. 20:08

ReactiveX

ReactiveX is a library for composing asynchronous and event-based programs by using observable sequences.

ReactiveX는 observable를 사용해 비동기 및 이벤트 기반 프로그램을 구성하는 라이브러리이다.

It extends the observer pattern to support sequences of data and/or events and adds operators that allow you to compose sequences together declaratively while abstracting away concerns about things like low-level threading, synchronization, thread-safety, concurrent data structures, and non-blocking I/O.

옵저버 패턴을 확장하여 데이터와 이벤트의 시퀀스를 지원하며 low-level threading, synchronization, thread-safety, concurrent data structres 및 non-blocking I/O 같은 문제를 추상화하면서 선언적으로 시퀀스를 함께 구성할 수 있는 연산자를 추가했다.

Swift의 비동기

Swift에서 비동기로 얻어 온 값을 전달하려면 completion handler를 사용한다.

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

closure로 전달되니 completion handler가 여러 개 중첩되면 코드가 너무 복잡해지는 단점이 있다.

downloadJson(URL1) { json in
    self.editView.text = json
    self.setVisibleWithAnimation(self.activityIndicator, false)

    self.downloadJson(URL2) { json in
        self.editView.text = json
        self.setVisibleWithAnimation(self.activityIndicator, false)

        self.downloadJson(URL3) { json in
            self.editView.text = json
            self.setVisibleWithAnimation(self.activityIndicator, false)

            self.downloadJson(URL4) { json in
                self.editView.text = json
                self.setVisibleWithAnimation(self.activityIndicator, false)
            }
        }
    }
}

값을 completion handler가 아닌 함수의 반환 값으로 전달받아 사용하면 편할 것 같은 생각이 든다.

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

let json = downloadJson(URL)

RxSwift는 "비동기로 생긴 데이터를 completion handler가 아닌 함수의 반환 값으로 전달하여 사용할 수 있을까?"라는 생각을 실현한 라이브러리이다.

위 코드에서 json은 메인 스레드가 아닌 다른 스레드에서 실행된 나중에 생기는 데이터이다. 만약 나중에 생기는 데이터를 함수의 반환 값으로 사용할 수 있으면 어떨까? 그러기 위해서 나중에 생기는 데이터 클래스를 만들어보자.

나중에생기는데이터 클래스는 기존의 completion handler를 멤버 변수로 갖는다.

나중에생기면 메서드의 매개 변수로 들어가는 클로저는 비동기적으로 나중에 생기는 값을 가지고 수행할 이벤트이다.

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

기존의 함수를 completion handler가 아닌 나중에생기는데이터 클래스를 반환하는 함수로 바꾸자. json 데이터가 비동기적으로 생성되면 수행할 이벤트를 나중에생기는데이터의 멤버 변수 task의 매개 변수로 보내 json을 비동기적으로 생성하는 task 클로저에서 호출할 수 있도록 하는 코드이다. Observer pattern과 유사한 흐름인 것을 알 수 있다.

func downloadURL(url: String) -> 나중에생기는데이터<String?> {
    return 나중에생기는데이터 { f in
        DispatchQueue.global().async {
            let url = URL(string: url)!
            let data = try! Data(contentsOf: url)
            let json = String(data: data, encoding: .utf8)
            DispatchQueue.main.async {
                f(json)
            }
        }
    }
}

let jsonObserver: 나중에생기는데이터<String?> = downloadURL(URL)

jsonObserver.나중에생기면 { json in
    print(json)
}

여기서 나중에생기는데이터 클래스를 Observable로 나중에생기면 메서드를 subscribe로 바꾸면 RxSwift이 된다.

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)
    }
}

func downloadURL(url: String) -> Observable<String?> {
    return Observable { fn in
        let url = URL(string: url)!
        let data = try! Data(contentsOf: url)
        let json = String(data: data, encoding: .utf8)
        DispatchQueue.main.async {
            fn(json)
        }
    }
}

let json: Observable<String?> = downloadURL(URL)

json.subscribe { json in
    print(json)
}

참고