본문 바로가기
iOS/RxSwift

RxSwift를 사용한 비동기 프로그래밍 #4 [마지막]

by HaningYa 2020. 6. 3.
728x90


 

https://github.com/iamchiwon

 

iamchiwon - Overview

https://iamchiwon.github.io http://www.makecube.in - iamchiwon

github.com


자 서버가 개발되고 JSON 이 온다!

이제 서버에 맞게 바꿔야한다.

 

서버에서 주는 Json 예시

서버의 데이터를 ViewModel 에 맞게 컨버팅 해주는 작업이 필요하다.

 

fetching 하는 레거시 코드를 가지고 있다.

class APIService {
    static func fetchAllMenus(onComplete: @escaping (Result<Data, Error>) -> Void) {
        URLSession.shared.dataTask(with: URL(string: MenuUrl)!) { data, res, err in
            if let err = err {
                onComplete(.failure(err))
                return
            }
            guard let data = data else {
                let httpResponse = res as! HTTPURLResponse
                onComplete(.failure(NSError(domain: "no data",
                                            code: httpResponse.statusCode,
                                            userInfo: nil)))
                return
            }
            onComplete(.success(data))
        }.resume()
    }
}

이 기존코드를 rx로 고치지 말고 rx 코드로 레거시 코드를 감쌀 수 있다.

 

   static func fetchAllMenuesRx() -> Observable<Data> {
        return Observable.create() {emitter in
            fetchAllMenus{ result in
                switch result{
                case .success(let data):
                    emitter.onNext(data)
                    emitter.onCompleted()
                    break
                case .failure(let err):
                    emitter.onError(err)
                    break
                }
            }
            return Disposables.create()
        }
    }

 

이 코드를 viewModel의 init 부분에서 더미 데이터를 삭제하고 집어넣는다. 

 

그리고 JSON 데이터가 오는 형식인 MenuItem을 viewModel 에 필요한 Menu 라는 모델로 컨버팅 하는 코드를 Menu 에 extension으로 작성한다.

(서버에서 주는 데이터는 name 과 price 이고 viewModel 에는 id, count 도 필요해서 이 작업이 필요하다)

 

extension Menu{
    static func fromMenuItems(id: Int, item : MenuItem) -> Menu {
        return Menu(id: 0, name: item.name, price: item.price, count: 0)
    }
}

 

완성된 viewModel 의 init() 부분

 init() {
        _ = APIService.fetchAllMenuesRx()
            .map{ data -> [MenuItem] in
                struct Response : Decodable {
                    let menus : [MenuItem]
                }
                let response = try! JSONDecoder().decode(Response.self, from: data)
                
                return  response.menus
        }
        .map { menuItems -> [Menu] in
            var menus:[Menu] = []
            menuItems.enumerated().forEach{ (index, item) in
                let menu = Menu.fromMenuItems(id: index, item: item)
                menus.append(menu)
            }
            return menus
        }
        .take(1)
        .bind(to: menuObservable)
    }

 


MVVM

디자인, 서버, 클라이언트 동시에 진행 할 수 있다. 

View에 필요한 Model 은 따로 만든 뒤 나중에

실제로 서버에서 데이터가 나오면(Domain Model) 컨버팅해서 view에 필요한 viewModel 을 만든다.


MVC

https://github.com/iamchiwon/RxSwift_In_4_Hours/blob/master/docs/mvc.jpeg

  • Controller 는 UIViewController 가 역할을 하며 사용자 Input action 을 Controller 가 받는다
  • 거기에 대한 결과물 출력, 업데이트도 controller 가 시킨다.
  • 거기에 대한 데이터는 Model (pure model) 특정 플랫폼(UIKit) 에 종속되지 않아서 Testable 하다
  • UIKit에 종속되는 순간 Test하기가 힘들다.
  • 컨트롤러의 역할을 좀 제한하자.

MVP

https://github.com/iamchiwon/RxSwift_In_4_Hours/blob/master/docs/mvp.jpeg

  • controller 의 역할중 사용자 입력 받는 부분을 view 쪽으로 넘겨버리자. (viewcontroller)
  • controller 의 로직 부분만 presenter에 구현하자.
  • view와 presenter 는 1대1 관계이다. (view 가 물어보면 presenter 가 대답한다.)
  • view는 멍청하게 테스트가 필요없게 만들어 버린다.(Uikit 있는애들)
  • 테스트하기 좋은 애들을 다 presenter 로 넘긴다.
  • 그러다 보니 view 와 presenter 가 1대1 관계이다 보니 귀찮다.
  • 비슷한 데이터를 처리하는 형태가 여러개가 있더라도 Presenter 를 계속 만들어야한다.
  • 그냥 Presenter 를 재사용하면 안되는감?

MVVM

https://github.com/iamchiwon/RxSwift_In_4_Hours/blob/master/docs/mvvm.jpeg

  • presenter가 viewModel 로 바뀐것
  • viewModel 이 화면에 뭘 그리라고 지시하지 않는다.(화살표)
  • view 는 그려야될 요소가 바뀌면 스스로 갱신한다. (데이터 바인딩을 기반으로 view 가 viewModel 을 바라보고 있다)
  • 비슷한 형태의 View들이 있고 기반 데이터가 같다면 하나의 ViewModel 로 처리할 수 있다.
  • 어떤 뷰는 list, 어떤 뷰는 썸네일일 수 있는데 같은 데이터 기반이면 ViewModel 하나로 처리한다.


Subject

  • observable은 데이터가 미리 정해져 있는 형태의 stream 이다.
  • create 할 때 부터 정해져 있다.
  • 추가적으로 runtime에 외부 control 에 의해서 데이터가 동적으로 생성되는 애가 필요하다.
  • 데이터를 넣어줄 수 있고, subsctibe 할 수 있는 양방향성이 필요했다.
  • 그놈이 Subject 이다.

RxCocoa

  • UIKit의 rx 처리이다.
  • UIkit.rx 해서 나오는 코드들이다.
  • UI의 특징은 항상 UI 쓰레드에서 돌아야 한다.
  • 그래서 observeOn(MainScheduler.instance)는 항상 들어가야한다.
  • Outlet 으로 밖에다 Instance 를 만들어 처리하다 보니 Self 를 써서 순환참조가 일어나는 경우가 있다.
  • UI는 데이터를 처리하다 에러가 나면 Stream 이 끊어진다. (재활용이 안된다.)
  • 화면을 그리다가 에러가 나면 stream 이 끊어져서 더이상 동작하지 않는다.(그렇게 되면 안된다.)
    .catchErrorJustReturn("") 으로 에러가 났을때 빈문자열을 그냥 리턴하는 걸로
viewModel.itemsCount
            .map{"\($0)"}
            .catchErrorJustReturn("")
            .observeOn(MainScheduler.instance)
            .bind(to: itemCountLabel.rx.text)
            .disposed(by: disposeBag)

그런데 항상 observeOn과 catchErrorJustReturn 이 필요하다. 그래서 UI처리 용도로 Driver 를 쓴다.

 

viewModel.itemsCount
            .map{"\($0)"}
            .asDriver(onErrorJustReturn: "")
            .drive(itemCountLabel.rx.text)
            .disposed(by: disposeBag)

 

driver 로 바뀌면 error 의 경우 asDriver로 바꿔서 처리하고 driver는 항상 메인 쓰레드에서 알아서 동작한다.

에러가 나도 끊어지지 않는 Subject가 필요해서 Driver를 쓰는 것이다.

 

에러가 나도 stream 이 끊어지지 않는 Subject

 

    lazy var menuObservable = BehaviorSubject<[Menu]>(value: [])
    //이 subject는 UI 에 영향을 끼친다.

 

RxRelay : Subject랑 똑같은데 에러가 나도 끊어지지 않는다.

    lazy var menuObservable = BehaviorRelay<[Menu]>(value: [])

 

에러가 나도 오로지 받아드리기만 해서 onNext 나 onComplete 을 쓰지 않는다.

accept 를 쓴다.

 

[기존코드]

func clearAllItemSelections(){
        menuObservable
            .map{menus in
                menus.map { m in
                    Menu(id:m.id, name: m.name, price: m.price, count: 0)
                }
        }
        .take(1)
        .subscribe(onNext: {
            self.menuObservable.onNext($0)
        })
    }

[RxRelay 바꾼코드]

func clearAllItemSelections(){
        menuObservable
            .map{menus in
                menus.map { m in
                    Menu(id:m.id, name: m.name, price: m.price, count: 0)
                }
        }
        .take(1)
        .subscribe(onNext: {
            self.menuObservable.accept($0)
        })
    }

 

정리하자면

UI는 메인쓰레드와 에러나도 Stream 끊어지면 안된다는 이유 때문에

Observable의 UI 용도는 Driver 이고 

Subject의 UI 용도는 Relay 이다.


 

Rx를 처음 배우고 써봤다.

이전에 POS 앱을 개발할 때 이 예시와 비슷하게 메뉴 리스트가 서버로 부터 오고 count, total price 이런 값을 UI 에 갱신해줘야 했다.

그땐 데이터에 따라 업데이트 되는 항목을을 updateUI 메소드에 묶어서 갱신 될 때 마다 호출하였는데 진작에 Rx를 알았더라면 좀더 깔끔하고 직관적으로 구현할 수 있었겠다는 생각을 했다.

또한 Rx는 비동기 통신에만 쓰는 줄로 알았는데 MVVM 의 데이터 바인딩에서 사용할 수 있다는 걸 알게 되었다. 이전에  MVVM 예제를 만들때 데이터 바인딩을 기본적으로 지원해 주지 않아 라이브러리를 써야하거나 completion handler 를 통해 조금 번거롭게 구현해야 했었는데 Rx 하나면 해결 되는 것 같다.

 

좋은 강의 만들어 주신 Song Chiwon 님께 구독과 좋아요 Follow Star을 남긴다!

[감사합니다!]

 

728x90

댓글