스파르타코딩 클럽/본 캠프

51. 스파르타 코딩클럽 [본캠프 - 온보딩 51일차]

UDDT 2025. 5. 14. 19:21

51일차 - 강의 주차

앱 개발 심화 주차 - RxSwift

 

1.  RxSwift란? (feat. ReactiveX)

     RxSwift는  ReactiveX에 포함된 개념이다.

    그렇다면, ReactiveX란 뭘까?

    

    ReactiveX는 옵저버블 스트림으로 비동기 프로그래밍을 하기 위한 API다

   ReactiveX(Rx)는 마이크로소프트 사에서 만든 라이브러리(비동기 프로그래밍과 옵저버 패턴을 쉽게 구현할 수 있도록 돕는 라이브러리)

   반응형 프로그래밍(데이터의 변화에 반응하는 프로그래밍)이라고도 하며, ReactiveX를 적용한 Swift 라이브러리가 바로 RxSwift

 

   Rx는 iOS 뿐만 아니라 안드로이드, 서버 개발자 등 다양한 분야의 개발자들이 애용하는 라이브러리이기 때문에

  Rx 개념을 가지고 있으면 다른 플랫폼의 개발자와도 함께 소통할 수 있음(언어가 달라도 공학적 사고 공유 가능)

- 공학적 사고를 공유한다는 것?
 iOS 개발자 : "Rx를 사용해서 버튼을 클릭했을 때 서버에 데이터를 요청하려고요
                       성공적으로 데이터 응답을 받으면 버튼의 색상이 녹색으로, 실패하면 버튼의 색상이 빨간색으로 바뀌게 하려고요
                        이때 버튼의 중복 클릭 방지를 위해서 throttle도 적용하려고 하는데, OO님 같으면 어떻게 작성하실건가요?"

사용하는 언어는 다르더라도, 각각의 플랫폼에는 버튼이 있고, 서버와 통신을 한다는 대전제는 동일하기 때문에 소통 가능

2.  RxSwift를 왜 공부해야할까?

     - 취업 및 이직 관련

     최근 대부분의 회사들이 RxSwift를 우대사항으로 포함하고 있는 추세

     MVVM과 함께 사용하기 적절한 라이브러리이기 때문

     ReactorKit, RIBs를 우대사항으로 하는 기업들도 있는데, 이 기술들도 내부적으로는 RxSwift를 활용함

 

     - 간결한 코드 작성이 가능

       비동기 코드를 간결하게 작성할 수 있음

       복잡한 로직 구현을 깔끔하게 작성할 수 있음

 

     * 애플 자체 라이브러리인 Combine과는 어떤 차이가 있어?

  RxSwift Combine
iOS 지원 버전 iOS 9.0 이상 iOS 13.0 이상
지원 시작일 2014년 2019년
타 플랫폼에서 지원 여부 O X
자료 비율 상대적으로 많음 상대적으로 적음

3.  Observer & Observable

이미지 참고 (https://refactoring.guru/ko/design-patterns/observer)

 

   - 옵저버 패턴 : 어떤 객체의 상태가 변화할 때 그를 관찰하는 구독자들에게 이벤트를 전달시켜주는 디자인 패턴

                          이벤트를 발행하는 객체 : Publisher(=Observable, = Subject)

                          구독자(관찰자) : Observer

      * 이벤트가 발행되면 곧바로 구독자가 이벤트를 받아보고, 그에 맞는 행동을 취하게 됨

        그런데, 여기서 구독자가 Publisher에게 계속 상태가 변했는지 물어보는 것보다는

       이벤트가 발생했을 때 Publisher가 구독자에게 바로 전달해주는 방식이 더 유리할 것임

       또 Publisher가 이벤트를 발행했다 하더라도 아무한테나 무작위로 정보를 스팸성으로 전송하는 것보다는,

       구독자들에게만 정확하게 전송하는 것이 좋을 것

      

        이벤트를 발행하는 주체는 구독자가 누구인지 타입으로 알 필요가 없고,

      구독자들만 이벤트를 발행하는 주체를 알고 있는 느슨한 결합 구조로 구성되어 있음

 

      iOS에서는 보통 View가 Data를 구독하고, Data의 변화가 일어나면 그 Data를 View에 적용시키는 구조로 많이 활용됨

 

     - Observable : 옵저버 패턴의 데이터 발행 주체

                              관측 가능한 대상이며, 이벤트와 데이터를 방출하는 클래스

                              Observable이 이벤트(값)을 방출하면,

                              이를 구독하던 관찰자(구독자)들이 그 값에 즉각적으로 반응하여 개발자가 정의해 놓은 로직을 수행함

                              Observable을 구독하는 것을 subsribe라고 하며, 이를 구독하고 관찰하는 관찰자를 Observer라고 함

                              그리고 구독을 해제하는 것을 dispose라고 함

       결론적으로, Observable은 데이터를 방출하는 스트림!(stream)

       * 데이터는 계속 흐르고, 언제 방출될지 모름(3초 뒤 던 1초 뒤던 방출)

 

   Observable에서 데이터를 방출할 때는 상태와 함께 방출됨

   * 상태: onNext / onError / onCompleted

      - onNext : 정상적인 데이터 방출 상태 (데이터가 함께 방출됨)

      - onError: 에러 방출 상태(에러가 함께 방출됨)

      - onCompleted: 옵저버블의 방출 종료. (데이터나 에러 없이 종료되었다는 사실만 인지)

 

    위의 그림에서처럼, Observable은 데이터의 흐름이기 때문에 시간적인 개념이 포함됨

 

4.  기본적인 Observable

     1. Observable.create()로 Observable 생성하기

import Foundation
import RxSwift

struct MyError: Error {}

let observable = Observable<String>.create { observer in
    observer.onNext("Apple")
    observer.onNext("Banana")
    observer.onNext("Cake")
    observer.onError(MyError())

    // Disposable(구독해제가 가능한) observable 객체를 만들었다~
    return Disposables.create()
}

 

     2. observable.subscribe()로 데이터 받기

// 방출하는 데이터를 받아서 print
observable.subscribe(onNext: { data in
    print("onNext: \(data)")
}, onError: { error in
    print("onError: \(error)")
}, onCompleted: {
    print("onCompleted")
}, onDisposed: {
    print("onDisposed")
    // 구독을 해제하면 가방에 찌꺼기를 담을게
}).disposed(by: disposeBag)

/* 결과
onNext: Apple
onNext: Banana
onNext: Cake
onError: MyError()
onDisposed
*/

 

     3. Observable.just()로 Observable 생성하기 (간단하게 데이터 방출을 할 때)

import Foundation
import RxSwift

struct MyError: Error {}

// 구독을 하고 남은 찌꺼기들을 담는 가방
let disposeBag = DisposeBag()

let observable = Observable.just("Uddt")   // Uddt를 단 한번 방출하는 옵저버블

observable.subscribe(onNext: { data in
    print("onNext: \(data)")
}).disposed(by: disposeBag)

// onNext: Uddt

 

     4. Observable.of()로 Observable 생성하기 (여러 개의 데이터를 방출할 때)

import Foundation
import RxSwift

struct MyError: Error {}

// 구독을 하고 남은 찌꺼기들을 담는 가방
let disposeBag = DisposeBag()

let observable = Observable.of("red", "blue", "yellow")

observable.subscribe(onNext: { data in
    print("onNext: \(data)")
}).disposed(by: disposeBag)

/* 결과
onNext: red
onNext: blue
onNext: yellow
*/

 

     5. Observable.from()로 Observable 생성하기 (배열에서 각 요소를 순차적으로 방출할 때)

import Foundation
import RxSwift

struct MyError: Error {}

// 구독을 하고 남은 찌꺼기들을 담는 가방
let disposeBag = DisposeBag()

let observable = Observable.from([1, 2, 3, 4, 5])

observable.subscribe(onNext: { data in
    print("onNext: \(data)")
}).disposed(by: disposeBag)

/* 결과
onNext: 1
onNext: 2
onNext: 3
onNext: 4
onNext: 5
*/

 

     6. Observable.interval()로 Observable 생성하기 (일정한 시간 주기로 데이터를 방출하고자할 때)

import Foundation
import RxSwift

struct MyError: Error {}

// 구독을 하고 남은 찌꺼기들을 담는 가방
let disposeBag = DisposeBag()

// 임의의 쓰레드 생성
let scheduler = SerialDispatchQueueScheduler(qos: .default)

let observable = Observable<Int>.interval(.seconds(1), scheduler: scheduler)    // take(5)로 다섯번으로 설정
    .take(5)

observable.subscribe(onNext: { data in
    print("onNext: \(data)")
}).disposed(by: disposeBag)

// commandLineTool 특성상 코드 실행이 종료되면 프로그램도 종료하기 때문에 세팅
let input = readLine()


/* 결과
onNext: 0
onNext: 1
onNext: 2
onNext: 3
onNext: 4
*/

+ Subscribe

    Observable은 선언하기만하고 구독하지 않으면 존재 가치가 퇴색됨

    Observable은 Observer가 구독을 해서 데이터를 사용해야만 의미가 있는 것

  

   - Cold Observable & Hot Observable

     Cold Observable : 구독을 했을 때 데이터가 흐르기 시작하는 Observable

     예) 넷플릭스는 구독자가 재생을 눌러야만 영상이 시작함

     Hot Observable : 구독과 무관하게 데이터가 흐르는 Observable

     예) TV는 시청자가 TV를 켜지 않아도 방송 송출이 진행되고 있음

 

     Observable.creat / Observable.just / Observable.of / Observable.from은 모두 subscribe를 호출했을 때 방출하므로

    Cold Observable으로 볼 수 있음!

 

+ DisposeBag

   구독 후에는 disposed(by: disposeBag)을 호출해서 구독 해제를 명시해야 함

   subscribe를 수행하면 Disposable 객체가 되며, 이를 DisposeBag 안에 담으면 DisposeBag이 메모리에서 해제될 때 구독도 해제함

5.  Trait (Single / Maybe / Completable)

    특별한 상황에 맞게 제공되는 Observable

   

    1. Single : 오직 하나의 값만을 방출하는 Observable

       - onSuccess : Observable의 onNext와 같은 개념

       - onFailure : Observable의 onError와 같은 개념

       - onCompleted는 없음

       * 하나의 값을 방출하거나, 에러를 방출하면 곧바로 스트림 종료

import Foundation
import RxSwift

struct MyError: Error {}

// 구독을 하고 남은 찌꺼기들을 담는 가방
let disposeBag = DisposeBag()

let single = Single<Int>.create { observer in
    observer(.success(100))
    return Disposables.create()
}

single.subscribe(onSuccess: { data in
    print("onSuccess: \(data)")
}, onFailure: { error in
    print("onFailure: \(error)")
}).disposed(by: disposeBag)
// 결과 onSuccess: 100

let single2 = Single<Int>.create { observer in
    observer(.failure(MyError()))
    return Disposables.create()
}

single2.subscribe(onSuccess: { data in
    print("onSuccess: \(data)")
}, onFailure: { error in
    print("onFailure: \(error)")
}).disposed(by: disposeBag)
// 결과 onFailure: MyError()

 

  single 안에 데이터를 2개 방출하게 하더라도, 방출되는 데이터는 처음에 방출된 데이터 하나로 끝

let single3 = Single<Int>.create { observer in
    observer(.success(100))
    observer(.success(200))
    return Disposables.create()
}

single3.subscribe(onSuccess: { data in
    print("onSuccess: \(data)")
}, onFailure: { error in
    print("onFailure: \(error)")
}).disposed(by: disposeBag)

// 결과 onSuccess: 100

 

    2. Maybe : 아마도 값(또는 에러)을 방출할 수도 있고, 아닐 수도 있는 Observable

        * onSuccess / onError / onCompleted 중 하나의 값만 방출

         (Single은 onSuccess / onFailure)

import Foundation
import RxSwift

struct MyError: Error {}

// 구독을 하고 남은 찌꺼기들을 담는 가방
let disposeBag = DisposeBag()

let maybe1 = Maybe<Int>.create { observer in
    observer(.success(100))
    return Disposables.create()
}

maybe1.subscribe(onSuccess: { data in
    print("onSuccess: \(data)")
}, onError: { error in
    print("onError: \(error)")
}, onCompleted: {
    print("onCompleted")
}).disposed(by: disposeBag)
// 결과 onSuccess: 100

let maybe2 = Maybe<Int>.create { observer in
    observer(.error(MyError()))
    return Disposables.create()
}

maybe2.subscribe(onSuccess: { data in
    print("onSuccess: \(data)")
}, onError: { error in
    print("onError: \(error)")
}, onCompleted: {
    print("onCompleted")
}).disposed(by: disposeBag)
// 결과 onError: MyError()

let maybe3 = Maybe<Int>.create { observer in
    observer(.success(100))
    observer(.success(200))
    return Disposables.create()
}

maybe3.subscribe(onSuccess: { data in
    print("onSuccess: \(data)")
}, onError: { error in
    print("onError: \(error)")
}, onCompleted: {
    print("onCompleted")
}).disposed(by: disposeBag)
// 결과 onSuccess: 100

let maybe4 = Maybe<Int>.create { observer in
    observer(.completed)
    return Disposables.create()
}

maybe4.subscribe(onSuccess: { data in
    print("onSuccess: \(data)")
}, onError: { error in
    print("onError: \(error)")
}, onCompleted: {
    print("onCompleted")
}).disposed(by: disposeBag)
// 결과 onCompleted

 

    3. Completable : 값을 뱉지 않고 무언가 완료되는 시점만 알고 싶을 때

         - onCompleted: Observable의 onCompleted와 같은 개념

         - onError: Observable의 onError과 같은 개념

        * 로딩이 완료되었다 등의 완료시점만 알고 싶을 때 활용 가능

import Foundation
import RxSwift

struct MyError: Error {}

// 구독을 하고 남은 찌꺼기들을 담는 가방
let disposeBag = DisposeBag()

let completable1 = Completable.create { observer in
    observer(.completed)
    return Disposables.create()
}

completable1.subscribe(onCompleted: {
    print("onComplted")
}, onError: { error in
    print("onError: \(error)")
}).disposed(by: disposeBag)
// 결과 onComplted

let completable2 = Completable.create { observer in
    observer(.error(MyError()))
    return Disposables.create()
}

completable2.subscribe(onCompleted: {
    print("onComplted")
}, onError: { error in
    print("onError: \(error)")
}).disposed(by: disposeBag)
// 결과 onError: MyError()

 

6.  Operator(map / zip / merge / flatMap)

     Operator의 종류는 아주 많지만, 그 중 선별해서 학습

    Operator의 원리를 이해하려면 마블 다이어그램(Marble Diagram)을 이해하는 것이 좋음

- 화살표, 구슬의 간격  : 시간의 흐름
- 연산자 박스: rx operator
- 연산자 박스 기준 상단 화살표 : input 스트림
- 연산자 박스 기준 하단 화살표 : output 스트림
- 마블(구슬) : 방출되는 값
- X 표시 : 에러
- | 표시 : 스트림 종료

 

    하단의 사이트에서 다양한 마블 다이어그램을 확인할 수 있다 (zip, filter 등 참고 가능)

    https://rxmarbles.com/

 

RxMarbles: Interactive diagrams of Rx Observables

 

rxmarbles.com

   

    1. map :  스트림에서 방출되는 값의 변형을 일으킴

import Foundation
import RxSwift

struct MyError: Error {}

// 구독을 하고 남은 찌꺼기들을 담는 가방
let disposeBag = DisposeBag()

let observable = Observable<Int>.create { observer in
    observer.onNext(1)
    observer.onNext(2)
    observer.onNext(3)
    return Disposables.create()
}

observable
    .map { $0 * 10 }.subscribe(onNext: {
        print("onNext: \($0)")
    }).disposed(by: disposeBag)

/* 결과
onNext: 10
onNext: 20
onNext: 30
*/

 

    2. zip :  두개의 스트림에서 나오는 값을 한 쌍씩 묶어서 방출

 

import Foundation
import RxSwift

struct MyError: Error {}

// 구독을 하고 남은 찌꺼기들을 담는 가방
let disposeBag = DisposeBag()

let observableA = Observable<Int>.create { observer in
    observer.onNext(1)
    observer.onNext(2)
    observer.onNext(3)
    observer.onNext(4)
    return Disposables.create()
}

let observableB = Observable<String>.create { observer in
    observer.onNext("A")
    observer.onNext("B")
    observer.onNext("C")
    return Disposables.create()
}

Observable.zip(
    observableA,
    observableB
).subscribe(onNext: { data in
    print("onNext: \(data)")
}).disposed(by: disposeBag)
/* 결과
onNext: (1, "A")
onNext: (2, "B")
onNext: (3, "C")
짝이 없기 때문에 4는 방출되지 않았음
*/

  

    3. merge :  두개의 스트림에서 나오는 값을 하나의 스트림에서 방출한 것처럼 합침

import Foundation
import RxSwift

struct MyError: Error {}

// 구독을 하고 남은 찌꺼기들을 담는 가방
let disposeBag = DisposeBag()

let observableA = Observable<Int>.interval(.seconds(2), scheduler: SerialDispatchQueueScheduler(qos: .default))
    .take(3)
    .map{ "A -> \($0) 번째 방출"}

let observableB = Observable<Int>.interval(.seconds(5), scheduler: SerialDispatchQueueScheduler(qos: .default))
    .take(3)
    .map{ "B -> \($0) 번째 방출"}

Observable.merge(
    observableA,
    observableB
).subscribe(onNext: { value in
    print("onNext: \(value)")
}).disposed(by: disposeBag)

let input = readLine()
/* 결과
onNext: A -> 0 번째 방출
onNext: A -> 1 번째 방출
onNext: B -> 0 번째 방출
onNext: A -> 2 번째 방출
onNext: B -> 1 번째 방출
onNext: B -> 2 번째 방출
*/

 

    4. flatMap :  스트림에서 방출된 값의 변형을 일으킴(단, operator 안에 또 다시 스트림이 들어온다는 것이 중요함)

                         스트림에서 방출된 값을, flatMap 안에 정의된 스트림으로 다시 흐르게 함

       * 스트림의 연산자로써 스트림을 넣었는데, 두개의 스트림이 한 개의 스트림으로 펼쳐진 결과가 나옴

 

import Foundation
import RxSwift

struct MyError: Error {}

// 구독을 하고 남은 찌꺼기들을 담는 가방
let disposeBag = DisposeBag()

let observable = Observable<Int>.interval(.seconds(2), scheduler: SerialDispatchQueueScheduler(qos: .default))
    .take(5)

let fruitDictionary = [
    0: "사과",
    1: "바나나",
    2: "오렌지",
    3: "멜론"
]

func getFruitObservable(_ number: Int) -> Observable<String> {
    Observable.create { observer in
        guard let fruit = fruitDictionary[number] else {
            observer.onError(MyError())
            return Disposables.create()
        }
        observer.onNext(fruit)
        return Disposables.create()
    }
}

observable
    .flatMap { data in
        return getFruitObservable(data)
    }.subscribe(onNext: { data in
        print("onNext: \(data)")
    }, onError: { error in
        print("onError: \(error)")
    }).disposed(by: disposeBag)

/* 결과
onNext: 사과
onNext: 바나나
onNext: 오렌지
onNext: 멜론
onError: MyError()
*/

let input = readLine()

 

   그외 더 공부하면 좋을 오퍼레이터 

  • concat
  • combineLatest
  • withLatestFrom
  • share

7.  Scheduler (Main / ConcurrentDispatchQueue / SerialDispatchQueue)

     Scheduler : Observable 스트림에서 스레드를 지정할 수 있도록 돕는 도구

     예) Observable 구독을 통해 UI 작업을 처리하게 된 경우, 그 작업은 메인 스레드에서 동작하도록 명시하는 게 좋음

 

     1. MainScheduler : 메인 스레드에서 작업을 실행하는 스케줄러

         * 주로 UI 업데이트와 관련된 작업을 실행할 때 사용

     2. ConcurrentDispatchQueueScheduler : 비동기 작업을 위한 DispatchQueue 기반의 스케줄러(qos 설정도 가능)

        * 여러 스레드에서 동시 작업을 처리하고자할 때 사용

     3. SerialDispatchQueueScheduler : 특정 DispatchQueue에서 직렬 작업을 실행하는 스케줄러

        * 동시성이 필요없는 작업을 특정 스레드에서 순서대로 처리할 때 유용

 

      - subscribeOn & observeOn

         subscribeOn : 스트림이 시작되는 곳의 스레드를 지정

         observeOn : 다운스트림의 스레드를 지정

 

      1) subscribeOn

Observable.just("Apple")
    .map { $0 + " + Banana" }
    .subscribe(onNext: { print($0) })

 

    위의 코드의 스트림은 다음과 같이 진행된다

    just > map > subscribe

   이 코드를 subscribeOn으로 작성하면, 

Observable.just("Apple")
    .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .background))
    .map { $0 + " + Banana" }
    .subscribe(onNext: { print($0) })

 

    스트림의 시작점인 just부터 map > subscribe 모두 백그라운드 스레드에서 실행된다
    (ConcurrentDispatchQueueScheduler로 지정해줬으니까)

 

 

      2) observeOn

Observable.just("Apple")
    .observeOn(MainScheduler.instance)
    .map { $0 + " + Banana" }
    .subscribe(onNext: { print($0) })

 

     subscribeOn과 다르게 observeOn으로 작성하면,

    observeOn 이후에 실행되는 코드만 해당 스케줄러가 실행하게 된다

    따라서 just는 기본 스레드에서 실행하고

    map > subscribe만 메인 스레드에서 실행하게 되는 것이다

 

    

 

파란색 부분 : input stream
operator를 4번 사용
operator 내부 도형 각각의 색상이 다름(각각 다른 스레드에서 돌아가고 있다)
observeOn > map > subscribeOn > observeOn

[흐름]
파란색 스레드에서 작업이 되던 것들이
observeOn을 만나고 주황색 스레드에서 작업을 시작함
스레드 변경이 되지 않고 쭉 진행되다가,
subscribeOn은 어떤 순서에 등장했던, 항상 첫번째(근본) 스레드를 지정하는 것
이후 observeOn을 한번 더 만나고 분홍색 스레드로 작업을 시작함
observable
    .map { someFunc1($0) }                 // 1
    .map { someFunc2($0) }                 // 2
    .subscribe(on: customScheduler)        // 3(스트림 시작 스레드)
    .observe(on: MainScheduler.instance)   // 4(스레드 변경)
    .subscribe(onNext: { result in         // 5(이 구독은 Main스레드에서 처리)
        print("onNext: \(result)")
        print(Thread.current)              // 6(스레드 확인)
    }).disposed(by: disposeBag)

 

 

 

8.  Subject

     Subject는 Observable과 Observer의 역할을 모두 수행할 수 있음

     * Observable : 값을 방출 시키는 입장

     * Observer : 값을 받는 입장

     

    1. BehaviorSubject : 초기값을 가지며, 구독을 시작하면 가장 최근에 방출되었던 값을 받으며 구독을 시작

첫번째 구독 시점에서는 분홍색이 초기값, 두번째 구독 시점에서는 녹색이 초기값

   

import Foundation
import RxSwift

struct MyError: Error {}

let disposeBag = DisposeBag()

let subject = BehaviorSubject(value: 0) // 기본 값을 방출할 수 있음

subject.subscribe(onNext: { data in
    print("OnNext: \(data)")
}).disposed(by: disposeBag)

subject.onNext(20)

subject.onNext(30)

/* 결과
OnNext: 0
OnNext: 20
OnNext: 30
*/

    

    2. PublishSubject : 초기 값을 가지지 않으며, 구독을 시작했어도 가장 최근에 방출된 값을 받지 않음

        * 구독 이후로 흐른 값만 받아봄

 

 

import Foundation
import RxSwift

struct MyError: Error {}

let disposeBag = DisposeBag()

let subject = PublishSubject<Int>()

subject.onNext(10)

subject.subscribe(onNext: { data in
    print("onNext: \(data)")
}).disposed(by: disposeBag)

subject.onNext(20)
subject.onNext(30)

/* 결과
구독 시점 이후의 값만 찍힘
onNext: 20
onNext: 30
*/


9.  Relay

     Relay도 Subject와 비슷하게 Observable과 Observer의 역할을 겸함

    그러나, 에러나 완료 이벤트를 방출하지 않도록 설계된 RxCocoa의 객체.

    에러나 완료가 되지 않기 때문에, 주로 UI 이벤트에서 처리하며(UI를 그리는 작업이 멈추면 안되니까), 애플리케이션 상태 관리에 유용함

 

    1. BehaviorRelay : 초기 값을 가지며, 구독을 시작하면 가장 최근에 방출되었던 값을 받으며 구독 시작

import Foundation
import RxSwift
import RxCocoa

struct MyError: Error {}

let disposeBag = DisposeBag()

let relay = BehaviorRelay(value: 0)

relay.accept(10)

relay.subscribe(onNext: { data in
    print("onNext: \(data)")
}).disposed(by: disposeBag)

relay.accept(20)
relay.accept(30)

/* 결과
구독 시점에서 가장 최근에 방출된 값인 10부터 출력됨
onNext: 10
onNext: 20
onNext: 30
*/

 

    2. PublishRelay : 초기값을 가지지 않으며, 구독 이후로 흐른 값만 받아옴

import Foundation
import RxSwift
import RxCocoa

struct MyError: Error {}

let disposeBag = DisposeBag()

let relay = PublishRelay<Int>()
relay.accept(10)

relay.subscribe(onNext: { data in
    print("onNext: \(data)")
}).disposed(by: disposeBag)

relay.accept(20)
relay.accept(30)
/* 결과
onNext: 20
onNext: 30
*/

10.  RxCocoa

     RxSwift : Swift에 대한 Rx 프로그래밍을 지원 (얘로는 서버개발을 할 수 있음 - 물론 하지는 않음)

     RxCocoa : iOS, macOS에 대해서 Rx 프로그래밍을 지원  (얘로는 아님. UI 바인딩에 특화된 도구)

     RxCocoa는 UIButton Tap에 대한 rx 인터페이스, UITextView의 text 감지에 대한 rx 인터페이스 등 UI 관련 작업에 특화된 도구

 

버튼을 누르면 "버튼이 클릭되었습니다"라고 출력되는 프로그램 만들기

 

      1. button.rx.tap()

import UIKit
import SnapKit
import RxSwift
import RxCocoa

class ViewController: UIViewController {

    private let disposeBag = DisposeBag()

    let button: UIButton = {
        let button = UIButton()
        button.backgroundColor = .blue
        button.setTitle("버튼", for: .normal)
        return button
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        initUI()
        bind()
    }

    private func initUI() {
        view.backgroundColor = .white
        [button].forEach { view.addSubview($0) }

        button.snp.makeConstraints {
            $0.width.equalTo(120)
            $0.height.equalTo(80)
            $0.center.equalToSuperview()
        }
    }

    private func bind() {
        button.rx.tap
            .subscribe(onNext: { data in
                print("버튼이 클릭되었음")
            }).disposed(by: disposeBag)
    }
}

 

       만약 UIKit으로 이 작업을 한다면, 아래의 형태였을 것이다.

import UIKit

class UIKitViewController: UIViewController {

    let button = UIButton()

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

    private func configureUI() {
        { ... } // 버튼 설정들
        button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
    }

    @objc func buttonTapped() {
        print("버튼이 클릭되었습니다")
    }
}

    

       RxCocoa를 사용하면, 버튼을 구독하게 하고 버튼의 상태가 변할 때마다 인지하게 할 수 있다

11.  RxCocoa 활용 예제

- Rx를 활용해서 버튼을 클릭했을 때 서버에게 데이터를 요청
- 성공적으로 데이터 응답을 받으면, 버튼 색상을 초록색으로 변경
- 실패했다면 버튼의 색상을 빨간색으로 변경
- 이떄 버튼의 중복클릭을 방지하기 위해 throttle도 적용하기
   (혹자가 1초에 버튼을 100번 누르면 서버에 요청을 100번 하게 됨. 따라서 중복 클릭은 방지해야 함)
import UIKit
import SnapKit
import RxSwift
import RxCocoa

struct MyError: Error {}

final class ViewController: UIViewController {

    private let disposeBag = DisposeBag()

    private let ioScheduler = SerialDispatchQueueScheduler(qos: .default)

    let button: UIButton = {
        let button = UIButton()
        button.backgroundColor = .blue
        button.setTitle("버튼", for: .normal)
        return button
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        initUI()
        bind()
    }

    private func initUI() {
        view.backgroundColor = .white
        [button].forEach { view.addSubview($0) }

        button.snp.makeConstraints {
            $0.width.equalTo(120)
            $0.height.equalTo(80)
            $0.center.equalToSuperview()
        }
    }

    private func bind() {
        button.rx.tap
            .throttle(.seconds(2), scheduler: MainScheduler.instance)
            .map { [weak self] in
                guard let self else { throw MyError() }
                return getRandomInt().isEvenNumber()
            }
            .subscribe(on: ioScheduler)
            .observe(on: MainScheduler.instance)
            .subscribe(onNext: { [weak self] data in
                guard let self else { return }
                print("버튼이 클릭되었음. \(data)")

                if data {
                    button.backgroundColor = .green
                } else {
                    button.backgroundColor = .red
                }
            }).disposed(by: disposeBag)
    }

    /// 1부터 10 중에 랜덤한 정수를 얻는 함수 (서버랑 소통하는 상황 가정)
    func getRandomInt() -> Int {
        let randomNumber = Int.random(in: 1...10)
        print("randomNumber: \(randomNumber)")
        return randomNumber
    }
}

extension Int {
    func isEvenNumber() -> Bool {
        self % 2 == 0
    }
}