Programming/iOS

[RxSwift] 4시간 안에 빠르게 익혀 실무에 사용하기 시즌 2- 내용정리!

devssun 2020. 4. 24. 21:51
728x90
반응형

RxSwift

강의 자료
iamchiwon/RxSwift_In_4_Hours

[1교시] 개념잡기 - RxSwift를 사용한 비동기 프로그래밍

  • onLoad() - LOAD 버튼을 누르면 상단 시간이 멈추고, 버튼도 눌린 상태에서 멈췄다가 json을 모두 다운로드하면 시간이 다시 흘러간다.

    → 👋왜 멈출까? 동기로 동작하기 때문, 비동기로 바꿔줘야 한다.

    → 해당 함수에서 서버 호출 부분을 DispatchQueue.global().async{} 로 감싸 준다.

    → 👋오류가 발생한다. iOS에서 UI 변경은 main Thread에서 해야 한다. 코드 중 UI를 변경하는 부분을 main Thread에서 하도록 변경한다.

    → 시간도 멈추지 않고, indicator도 떴다가 사라진다.

비동기 프로그래밍

  • 다른 쓰레드를 사용해서 현재 하고 있는 작업은 작업대로 하고 다른 스레드에서 내가 원하는 작업을 동시에 수행하는 것

  • iOS에서는 DispatchQueue, OperationQueue 가 비동기 프로그래밍을 하기 위한 대표적인 메소드이다.

  • DispatchQueue 부분을 함수로 분리하기

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

    → 👋함수로 분리해서 사용하는 부분은 간결해지긴 했지만, completion 구문 등등.. 또 비동기 호출이 많아지면?? 코드가 점점 복잡해진다. 그냥 String을 리턴할 수 없을까?

    → 아래와 같이 수정할 수 있다.

    나중에생기는데이터<T> 와 같은 기능을 갖는 유틸리티 - PromiseKit, Bolt, RxSwift 등

      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)
          }
      }
    
      func downloadJson(_ 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)
                      }
                  }
              }
          }
    • RxSwift로 바꿔 보기

      • 나중에생기는데이터<T>Observable<T>
      • 나중에오면subscribe
      _ = downloadJson(MEMBER_LIST_URL)           
                            .subscribe { event in // 클로저 범위는 여기부터         
                                switch event {
                     case .next(let json):
                         self.editView.text = json
                         self.setVisibleWithAnimation(self.activityIndicator, false)
                     case .completed:
                         break
                     case .error:
                         break
                     }
             } // 여기까지
      }
      RxSwift는 비동기적으로 발생한 데이터를 completion 같은 클로저로 전달하는 것이 아니라 리턴 값으로 전달하기 위해 만들어진 유틸리티이다.
    • 👋순환 참조

      • 위 구문에서 순환 참조가 발생함

        → 클로저가 subscribe { } 부분인데, 클로저가 self를 캡쳐하면서 RC가 증가하기 때문에 순환 참조가 발생한다. RC가 감소될 때는 클로저가 없어질 때이다.

        → 그럼 클로저는 언제 없어지나? complete, error 일 때

      • 순환 참조 해결 방법 1 - [weak self] 추가

      • 순환 참조 해결 방법 2 - onCompleted() 추가

        onNext() 이후 onCompleted() 추가 (처리가 끝남을 알림), 그러면 onLoad() 함수의 switch 문에서 next를 실행하고 complete 가 실행하고 클로저가 사라져 클로저가 가지고 있는 self에 대한 RC도 사라진다.

    • Observable의 생명주기

      1. Create (만들었다고 실행되는 건 아님)
      2. Subscribe (Observable은 subscribe 됐을 때 동작한다)
      3. onNext
      4. —— 끝 —— →동작이 끝난 observable은 재사용 불가, 새로운 subscribe이 있어야 동작함
      5. onCompleted / onError
      6. Disposed
    • But, 지금까지 만들어진 코드도 길다. 추가적인 귀찮음을 없애보자

      Sugar API (→ Operator)
    • Observable.create 를 이용하면 Hello World 한 문장을 출력하는 데도 4줄의 코드가 필요하다.

    • just , from 사용하기

      // Hello World 한 문장을 출력하는 데도 4줄의 코드가 필요하다. 
      return Observable.create { emitter in
        emitter.onNext("Hello World")
        emitter.onCompleted()
        return Disposables.create()
      }
      
      // 1줄로 바꾸기
      return Observable.just("Hello World")
      // Optional("Hello World")
      
      // 2개 이상 값 보내기
      return Observable.just(["Hello"], ["World"])
      // [Optional("Hello") Optional("World")]
      
      return Observable.from(["Hello"], ["World"])
      // Optional("Hello")
      // Optional("World")
    • subscribe

      _ = downloadJson(MEMBER_LIST_URL)
                .subscribe(onNext: { print($0) },
                           onError: { err in print(err) },
                           onCompleted: { print("Com") })
    • observeOn 을 사용해서 DispatchQueue 구문을 줄일 수 있다.

      • observeOn 은 RxSwift가 OperationQueue 메소드를 래핑한 함수다.
      Operator
    • 데이터를 전달되는 중간에 가로채서 바꿔치기 하는 역할을 가짐, 종류가 매우 많다. (마블 그림에 함수의 설명이 나오니 그림을 이해할 줄 알면 동작을 이해할 수 있다.)

    • http://reactivex.io/documentation/operators.html

      _ = downloadJson(MEMBER_LIST_URL)
                .observeOn(MainScheduler.instance) // operator
                .subscribe(onNext: { json in
                    self.editView.text = json
                    self.setVisibleWithAnimation(self.activityIndicator, false)
                })

    [2교시] RxSwift 활용하기 - 쓰레드의 활용과 메모리 관리
    • share

    • combine, merge - 여러 데이터 합치기, zip - 하나씩 쌍으로 만들어서 전달, 쌍이 없으면 전달 못함

    • **observeOn vs subscribeOn 차이점**

      • observeOn - 바로 밑의 쓰레드에 영향을 줌, downstream에 영향
      • subscribeOn - 위치에 상관없이 맨 처음 쓰레드에 영향을 줌, upstream에 영향

    [3교시] RxSwift 활용범위 넓히기 - UI 컴포넌트와의 연동
    • Subject는 observable 밖에서 데이터를 컨트롤해서 새로운 값을 집어넣을 수 있다.

    • 양방향성을 가진다.

    • Observable.create 했을 때 받았던 emitter와 같은 역할을 하는데 emitter와 달리 observable 밖에서 사용 가능

      Subject의 종류

      참고 - https://github.com/fimuxd/RxSwift/blob/master/Lectures/03_Subjects/Ch3. Subjects.md

    • (AsyncSubject, BehaviorSubject, PublishSubject, ReplaySubject)
    1. AsyncSubject

      • 여러 개가 구독해도 데이터를 내려보내지 않고 complete됐을 때 데이터를 내려 보낸다.

    2. BehaviorSubject

      • 기본 값을 하나 가지고 시작하고 subscribe가 되자마자 앞에서 설정한 기본 값을 내려 준다. 구독 이후에는 데이터가 생기면 내려 보낸다.

      • 추가로 subscribe이 되면 최근에 만들어진 값을 내려보내고 이후 값이 생성되면 이어서 값을 내려 보낸다.

    3. PublishSubject

      • Subject에 누군가 subscribe할 수 있다. 내부에서 데이터가 생성이 되면 데이터를 그대로 내려 보낸다.

    4. ReplaySubject

      • 처음 동작은 PublishSubject와 동일하고 다음 구독이 생성되면 이전에 발생한 데이터를 한꺼번에 내려 보낸다. (replay)

  • Subject (http://reactivex.io/documentation/subject.html)
  • Stream의 분리 및 병합

예제를 그림으로 이해해 보자

/// [Menu]를 받는 observable
var menuObservable = BehaviorSubject<[Menu]>(value: [])

/// menuObservable의 값이 바뀌면 아이템 갯수가 다시 계산된다
lazy var itemsCount = menuObservable.map {
    $0.map { $0.count }.reduce(0, +)
}

/// 메뉴의 count가 바뀌면, 즉 menuObservable의 값이 바뀌면 총합을 다시 계산한다.
lazy var totalPrice = menuObservable.map {
    $0.map { $0.price * $0.count }.reduce(0, +)
}

init() {
    let menus: [Menu] = [
        Menu(name: "튀김1", price: 100, count: 0),
        Menu(name: "튀김1", price: 100, count: 0),
        Menu(name: "튀김1", price: 100, count: 0),
        Menu(name: "튀김1", price: 100, count: 0)
    ]

    menuObservable.onNext(menus)
}

RxCocoa

  • UI 처리 (UIKit.rx)
  • UI 작업의 특징
    항상 UI Thread 에서만 돌아가야 함, 그래서 observeOn 을 항상 쓴다.
    → UI는 작업을 처리하다 오류가 나면 작업이 끝나고 이 스트림은 다시 사용, 처리할 수 없다. 그래서 에러가 나도 스트림이 끊어지지 않도록 처리해야 한다. (ex: catchErrorJustReturn(""), asDriver(), drive())
  • Observable / Driver (UI 용)
    → driver는 항상 메인 쓰레드에서 동작한다. observeOn이 필요없다.
  • Subject / Relay (UI 용)
    → Subject와 같지만 에러가 나도 끊어지지 않는 다는 점이 다르다.
    → 무조건 끝나기 때문에 onNext() 가 아닌 accept() 를 사용한다.

[4교시] RxSwift 를 활용한 아키텍쳐 - 프로젝트에 MVVM 적용하기

MVC

Input을 Controller가 받는다. 입력에 대한 출력도 Controller가 한다. (View, Model Update)

View와 Controller는 UIKit에 종속되어 테스트가 불가능하지만 Model은 종속되지 않으므로 테스트가 가능하다.

MVP

Controller의 역할을 제한하자. ViewController의 역할을 View에 넘기고, VC에서 했던 로직만 따로 빼어 Presenter로 한다. 모든 판단과 로직은 Presenter가 하게 만든다.
View ↔ Presenter는 1:1 관계를 갖는다.

MVVM

ViewModel이 화면에 어떤 걸 그리라고 지시하지 않는다. View가 ViewModel을 바라보면서 값이 바뀌면 스스로 갱신한다.
MVP나 MVVM은 아이디어는 비슷하고 처리하는 방식이 조금 다르다.

반응형