옵저버 패턴

  • 1:N 관계로 이루어진, 관찰 패턴
  • 객체를 구독하면 옵저버가 상태를 알려줌
    Observer Pattern

함수형 프로그래밍

  • 1급 객체
    • 변수나 상수에 저장을 할 수 있어야 함
    • 함수에서 return 할 수 있어야 함
    • 파라미터로 전달할 수 있어야 함
  • 순수 함수
/* 환율 계산해주는 프로그램 이 코드는 순수 함수라고 할 수 없다. → 전역 상태에 의존 = 참조 투명성이 없다 */ 
var rate = 1120 
func krw(usd: Int) -> Int { 
    return usd * rate 
} 

krw(usd: 2) // 2240 
krw(usd: 3) // 3360 
rate = 1130 
krw(usd: 2) // 2260 
krw(usd: 3) // 3390

ReactiveX

  • 관찰 가능한 스트림을 이용한 비동기 프로그래밍 API
  • Reactive eXtension

RxSwift

  • Observable
    • Rx의 기본이 되는 스트림을 가지고 있는 기본 시퀀스
    • 하나 또는 연속된 항목을 배출
    • 구독 후 next, error, completed로 처리
      • next 스트림
        • 연속된 값들을 배출하고 옵저버는 next 스트림을 관찰 및 구독해서 원하는 행동을 함
      • error 스트림
        • 값을 배출하다가 에러가 생기면 error를 배출한 다음 해당 Observable은 스트림을 멈춤
      • complete
        • Observable이 모든 값을 다 배출하면 이 상태(?)가 됨
        • error가 발생하면 complete은 발생하지 않음!
  • Operator
    • Observable 안에서 데이터 처리를 위한 모든 연산들
    • map, filter, merge 등 수십가지의 연산이 있음
      • 메소드 체이닝으로 많이 진행함
  • Single
    • Observable 같이 N번이 아닌, 단번의 데이터를 처리하기 위한 시퀀스
    • 구독 후 success, error 두 가지로 처리
    • 한번만 오고, 한번에 끝남
  • Subject
    • 여러개의 Observer가 데이터를 관찰해야 할 때 사용하는 객체
    • Observer 와 Observable 사이의 브릿지 역할
    • 여러 개의 Observer가 데이터를 관찰해야하는 경우 자주 쓰임

굳이?

  • 왜 비용을 들여가면서?
  • 왜 잘 돌아가는 코드를 바꿔가면서?
  • 왜 꼭 더 난이도 있는 RxSwift를?

이유

  • 간결해지는 코드
    • 조금 더 길어지더라도, 간단하면서도 깔끔해지는 코드
  • 간편한 비동기 관리

RxSwift 알아보기 1 ~ 3 기록

정수형 배열의 엘리먼트를 체크하는 함수 (RxSwift)

// items 라는 정수 타입의 배열을 파라미터로 받음 -> 정수 제네릭 타입의 Observable 객체를 리턴
func checkArrayObservable(items: [Int]) -> Observable<Int> {
    return Observable<Int>.create { observer -> Disposable in

        for item in items {
            if item == 0 {
                observer.onError(NSError(domain: "Error: value is zero.", code: 0, userInfo: nil)) // 0이면 에러를 흘려준다
                break
            }

            observer.onNext(item) // 0이 아니면 각 엘리먼트를 next로 흘려준다

            sleep(1)
        }

        observer.onCompleted() // 모든 순회가 끝나면 completed 되었다는 걸 알림

        return Disposables.create()
    }
}

Subscribe, Dispose

Subscribe

Observable의 stream을 관찰하고 구독해서 받는 역할
Disposable 이라는 객체를 반환한다!

예제 코드

// interval: n초마다 정수 타입의 스트림이 방출됨
Observable<Int>.interval(RxTimeInterval.seconds(1), scheduler: MainScheduler.instance)
    .take(10) // parameter 만큼의 스트림 허용
    .subscribe(onNext: { value in
        print(value)
    }, onError: { error in
        print(error)
    }, onCompleted: {
        print("onCompleted")
    }, onDisposed: {
        print("onDisposed")
    })

// 예외 상황 발생
// 카운팅이 끝나기 전에 뷰 컨트롤러를 해제해 버린다면?
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
    UIApplication.shared.keyWindow?.rootViewController = nil
} // 앱이 죽었음에도 불구하고 끝까지 카운트가 진행됨

이것을 해결해보자.

Disposable

disposable: 처분할 수 있는, 사용 후 버릴 수 있는

public protocol Disposable {
    /// Dispose resource.
    func dispose()
}


// interval: n초마다 정수 타입의 스트림이 방출됨
let disposable = Observable<Int>.interval(RxTimeInterval.seconds(1), scheduler: MainScheduler.instance)
    .take(10) // parameter 만큼의 스트림 허용
    .subscribe(onNext: { value in
        print(value)
    }, onError: { error in
        print(error)
    }, onCompleted: {
        print("onCompleted")
    }, onDisposed: {
        print("onDisposed")
    })

// 예외 상황 발생
// 카운팅이 끝나기 전에 뷰 컨트롤러를 해제해 버린다면?
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
    disposable.dispose()
} // 반환된 disposable 객체를 가지고 있다가 뷰 컨트롤러가 deinit 될 때 dispose 실행

만약 구독받는 Observable이 여러개라면?

→ DisposeBag을 사용하자

extension Disposable {
    /// Adds `self` to `bag`
    ///
    /// - parameter bag: `DisposeBag` to add `self` to.
    public func disposed(by bag: DisposeBag) {
        bag.insert(self)
    }
}

파라미터로 들어오는 DisposeBag 객체에 자신을 insert 한다.

모든 Disposable 객체에 disposed를 해주면 해당 파라미터인 disposeBag에 등록되고 disposeBag 객체가 해제되면서 등록된 모든 disposable이 다 같이 dispose 되어버린다.

/// var로 선언한 이유: disposeBag이 해제되면 모든 disposable이 dispose되는 원리를 개발도중 사용할 수 있음
/// subscribe 중이던 disposable을 초기화하고 싶으면 새로운 DisposeBag 객체를 넣어주면 끝!
var disposeBag = DisposeBag()

// interval: n초마다 정수 타입의 스트림이 방출됨
Observable<Int>.interval(RxTimeInterval.seconds(1), scheduler: MainScheduler.instance)
    .take(10) // parameter 만큼의 스트림 허용
    .subscribe(onNext: { value in
        print(value)
    }, onError: { error in
        print(error)
    }, onCompleted: {
        print("onCompleted")
    }, onDisposed: {
        print("onDisposed")
    })
    .disposed(by: disposeBag) // **← 여기 부분!**

RxSwift In 4Hours

동기/비동기 처리

@IBAction func onLoadAsync(_ sender: Any) {
    // TODO: async
    DispatchQueue.global().async { [weak self] in
        guard let self = self else { return }
        let image = self.loadImage(from: self.IMAGE_URL)
        DispatchQueue.main.async {
            self.imageView.image = image
        }
    }
}

private func loadImage(from imageUrl: String) -> UIImage? {
    guard let url = URL(string: imageUrl) else { return nil }
    guard let data = try? Data(contentsOf: url) else { return nil }

    let image = UIImage(data: data)
    return image
}

just

Observable.just("Hello World")
    .subscribe(onNext: { str in
        print(str)
    })
    .disposed(by: disposeBag)

just의 인자로 넣어준 "Hello World"str로 넘어간다.

  • Observable.create()를 대신 해주는 메소드
  • 배열을 넣으면 배열을 처리함(아무거나 넣어도 된단 소리)

from

Observable.from(["RxSwift", "In", "4", "Hours"])
    .subscribe(onNext: { str in
        print(str)
    })
    .disposed(by: disposeBag)

from에 배열이 들어가면 하나씩 실행한다.

map

Observable.just("Hello")
    .map { str in "\(str) RxSwift" }
    .subscribe(onNext: { str in
        print(str)
    })
    .disposed(by: disposeBag)

실행순서

  1. just(Hello)
  2. map { str in "\(str) RxSwift" }
  3. onNext: { str in print(str) }
  4. 결과: Hello RxSwift
Observable.just(["Hello", "World"])
    .map { str in "\(str) RxSwift" }
    .subscribe(onNext: { str in
        print(str)
    })
    .disposed(by: disposeBag)

실행순서

  1. just(["Hello", "World"])
  2. map { str in "\(str) RxSwift" }
  3. onNext: { str in print(str) }
  4. 결과: ["Hello", "World"] RxSwift
Observable.from(["Hello", "World"])
    .map { str in "\(str) RxSwift" }
    .subscribe(onNext: { str in
        print(str)
    })
    .disposed(by: disposeBag)

실행순서

  1. just(["Hello", "World"]) → 첫번째 원소
  2. map { str in "\(str) RxSwift" }
  3. onNext: { str in print(str) }
  4. 결과: Hello RxSwift
  5. just(["Hello", "World"]) → 두번째 원소
  6. map { str in "\(str) RxSwift" }
  7. onNext: { str in print(str) }
  8. 결과: World RxSwift

filter

Observable.from([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
    .filter { $0 % 2 == 0 }
    .subscribe(onNext: { n in
        print(n)
    })
    .disposed(by: disposeBag)

true일 때만 데이터가 밑으로 내려감

이미지를 다운받는 과정

위에서 배운 걸로 해석해보자.

Observable.just("800x600")
    .map { $0.replacingOccurrences(of: "x", with: "/") } // → "800/600"
    .map { "https://picsum.photos/\($0)/?random" } // → "https://picsum.photos/800/600/?random"
    .map { URL(string: $0) } // → URL 객체로 변환
    .filter { $0 != nil } // → URL 객체가 nil이면 내려가지 않음
    .map { $0! } // → 내려오면 강제 언랩핑 (위에서 체크했으니까 강제 언래핑해도 됨)
    .map { try Data(contentsOf: $0) } // → URL에 있는 데이터를 다운
    .map { UIImage(data: $0) } // → UIImage?
    .subscribe(onNext: { image in
        self.imageView.image = image
    })
    .disposed(by: disposeBag)

observeOn

Observable.just("800x600")
    .observeOn(ConcurrentDispatchQueueScheduler.init(qos: .default)) // 백그라운드에서 돌릴 때
    .map { $0.replacingOccurrences(of: "x", with: "/") } // → "800/600"
    .map { "https://picsum.photos/\($0)/?random" } // → "https://picsum.photos/800/600/?random"
    .map { URL(string: $0) } // → URL 객체로 변환
    .filter { $0 != nil } // → URL 객체가 nil이면 내려가지 않음
    .map { $0! } // → 내려오면 강제 언랩핑 (위에서 체크했으니까 강제 언래핑해도 됨)
    .map { try Data(contentsOf: $0) } // → URL에 있는 데이터를 다운
    .map { UIImage(data: $0) } // → UIImage?
    .observeOn(MainScheduler.instance) // 메인 스레드에서 돌리는거
    .subscribe(onNext: { image in
        self.imageView.image = image
    })
    .disposed(by: disposeBag)

observeOn을 해준 다음줄 부터 영향이 미친다.

subscribeOn

subscribe가 실행되는 시점부터 동작한다. 아무 위치에나 넣어도 상관없음!

side-effect

side-effect를 허용하는 건 두 개가 있다.

  • do
  • subscribe

참고 자료