RxSwift 대망의 마지막 편..1

2020. 8. 14. 03:30iOS/RxSwift

안녕하세요 !!! 

오늘은 RxSwift를 마지막으로 다루어 보겠습니다. 이전 동안 유튜브 '곰튀김'님의 강의영상을 보고 쭉 진행해 왔었는데요 ! 오늘 무려 두시간 동안 강의를 쭉 보면서 실습을 해보았습니다 !! 본격적인 실습에 나서니까 굉장히 흥미로운 내용들이 많더라구요 !!

먼.저 ! 이전에 했던 내용들을 살짝 복습해 보겠습니다. (주섬주섬..)

hereismyblog.tistory.com/10

 

RxSwift, 이제 드디어 뭐 좀 만들어 보자 !!!!

오늘도 RxSwift 입니다. 이전까지 예제 프로젝트에서 Rx에 대해 기본 개념들을 알아보는 시간들을 가져봤습니다. 그러나 이러한 개념을 잡는 것과 실제 프로젝트에 적용시켜 보는 것은 다른 문제��

hereismyblog.tistory.com

이제 드디어 ! 뭐좀 만들어 보기 시작했군요. 실제 프로젝트에서 ViewModel 클래스도 생성해서 정의해보고 스트림을 생성해 직접 subscribe 동작까지 달아 보았습니다. 또한 기존의 Observable이 외부에서 데이터 처리가 불가능하다는 점을 알았고 이를 해결하기 위해 Subject 라는 놈을 써보면서 Subject의 종류중 하나인 PublishSubject 라는 스트림을 사용해 보았습니다.

이 놈은 외부에서 데이터 처리가 가능하기 때문에 버튼을 누를 때마다 뷰모델에 접근해서 스트림에 onNext로 데이터를 전달해 주었습니다. 이에 따라 여러 Operator를 통해 데이터를 가공하여 화면에 뿌려주는 것 까지 진행해 보았는데요 ! 

이제 이 Subject를 활용해서 여러가지 스트림을 생성시켜 데이터를 화면애 뿌려주도록 해보겠습니다.

우리의 최종적인 목표는 각 메뉴에 따라 가격이 있고 각 메뉴를 고르면 총 가격이 늘어나도록 해주는 것이 목표입니다. 따라서 초기에 메뉴에 대한 데이터들이 존재해야 합니다. 따라서 우리는 MVVM을 적용시키는 것이 목표기 때문에 뷰모델에서 이러한 메뉴 데이터를 생성시켜 Stream을 통해 전달해 주어야 겠죠 ?? 뷰컨트롤러는 정말 화면을 구성하는 것만을 담당해야만 합니다. 

class MenuListViewModel {
    var menus: [Menu]
    var menuObservable = PublishSubject<[Menu]>()
    ...    
    
    init() {
       
        menus = [
            Menu(name: "튀김1", price: 4000, count: 0),
            Menu(name: "튀김2", price: 5000, count: 0),
            Menu(name: "튀김3", price: 3000, count: 0),
            Menu(name: "튀김4", price: 1000, count: 0),
            Menu(name: "튀김5", price: 2000, count: 0)
        ]
        
        menuObservable.onNext(menus)
    }
}

따라서 뷰모델에서는 메뉴데이터를 저장할 배열과 PublishSubject라는 스트림을 이용하여 메뉴 데이터를 전달해 주었습니다.

또한 여기서 새로운 것을 배웁니다. 이 프로젝트에서는 메뉴들이 테이블뷰를 통해 보여지게 됩니다.

테이블 뷰는 보통 테이블뷰 데이터 소스라는 프로토콜을 채택하여 구현하는데요 ! 이 Rx에서는 operator를 통해 간단히 데이터를 뿌려줄 수가 있습니다.

class MenuViewController: UIViewController {
    
    var viewModel: MenuListViewModel?
    let disposeBag = DisposeBag()
    let cellID = "MenuItemTableViewCell"
    
    // MARK: - Life Cycle
    override func viewDidLoad() {
        super.viewDidLoad()
        viewModel = MenuListViewModel()
        
        viewModel?.itemCount
            .map { "\($0)"}
            .observeOn(MainScheduler.instance)
            .subscribe(onNext: { [weak self] in
                self?.itemCountLabel.text = $0
            })
            .disposed(by: self.disposeBag)
    }
    ...
}

 

기존에 stream에서는 데이터를 화면에 뿌려주기위 해 .observeOn이라는 함수를 통해 메인 스레드를 호출시켜 UI를 변경하였습니다. (UI 변경은 메인스레드에서만 가능하다.. 메모..또 메모 ..)

또한 subscribe를 통해 onNext를 통해 들어온 데이터들을 사용하였습니다. 그러나 이러한 작업을 한번에 해결해주는 bind라는 operator가 있습니다. 이는 RxCocoa 라는 pod의 import를 통해 가능해 집니다. 간단하게(조금더 공부해 보겠습니다.) 설명하자면 스트림을 통해 전달된 데이터를 바로바로 뿌려줄 수 있도록 UIKit과 결합하여 사용할 수 있게 됩니다. 

        viewModel?.totalPrice
            .map { $0.currencyKR() }
            .bind(to: self.totalPrice.rx.text) // 순환참조 문제가 사라진다.
            .disposed(by: self.disposeBag)
            
            
 // var rx: Reactive<UILabel> { get set }

이는 UI 컴포넌트에서 rx라는 프로퍼티를 이용하여 사용할 수 있게 됩니다. 이는 메인스레드를 매번 호출하는 수고를 덜어주고 자동으로 데이터를 UI컴포넌트에 뿌려주며 계속해서 작성해 주었던 순환참조 문제를 해결하기위한 코드가 필요 없어 지게 됩니다 .. ! 신.세.계..

이는 또한 테이블뷰에도 마찬가지 입니다. 이 bind라는 함수를 통해 데이터소스를 구현해 주지 않고도 자연스레 테이블뷰를 구현할 수 있습니다.

        viewModel?.menuObservable
            .bind(to: tableView
                .rx
                .items(cellIdentifier: cellID,
                       cellType: MenuItemTableViewCell.self)) { index, item, cell in
                        // index : index
                        // item : menu 배열중 하나가 들어온다.
                        // cell : 지정해준 Cell의 인스턴스
                        cell.title.text = item.name
                        cell.price.text = "\(item.price.currencyKR())"
                        cell.count.text = "\(item.count)"
                        
                        cell.onChaged = { [weak self] increase in
                            
                            self?.viewModel?.changeCount(item, increase)
                        }
        }.disposed(by: self.disposeBag)

당연스럽게도 rx를 통해 테이블뷰를 구성하기 위해선 몇가지 필요한 작업들이 있습니다. 기존에 셀을 만들었던 것과 유사합니다. tableView.rx.items()함수를 통해 각 셀을 구성하는데요 ! 기존에도 필요했던 셀의 id나 셀을 위해 생성해둔 셀 클래스들을 지정해 주어야 합니다. 

그리고 closure를 통해 각 셀에 데이터를 뿌려주게 됩니다.

그.러.나 !!! 실행을 시켜보면 ..

띠용..! 아무것도 안나옵니다.

분명히 뷰모델에서 데이터를 생성시켜 전달해 주었는데요 !! 

왜이럴까..? 를 생각해 보면 이 PublishSubject라는 놈의 특징을 알아야 합니다. 

이 PublishSubject라는 아이는 스트림이 생성되고  subscribe를 하기 전에 이전에 스트림을 통해 들어온 데이터는 받아오지 않고 새로 들오올 이벤트만을 기다리게 됩니다. 따라서 위에서 생성시켜줄 때도 따로 설정해 준 것이 없었죠. 

그런데 이번에 구현하는 프로젝트 에서는 뷰모델의 생성자를 통해 데이터를 생성하고 전달해 주었습니다. 그리고 이 스트림에 subscribe하는 타이밍은 viewDidLoad의 시점입니다. 따라서 뷰모델인스턴스를 생성하고 난뒤에는 아무리 날고 기어도 이전에 전달해 주었던 데이터들은 싹 무시하게 됩니다.

이러한 문제를 해결하는 방법은 여러가지가 있겠지만 이번에는 BehaviorSubject를 통해 문제를 해결해 보겠습니다.

이 BehaviorSubject의 특징은 바로

구독 시점에 나타납니다. 기존의 PublishSubject에서는 구독한 시점이후로 부터 이벤트를 기다리게 되는데요 !

이 BehaviorSubject는 구독한 시점으로 부터 바로 이전에 전달된 데이터를 받아와 실행하게 됩니다. 따라서 스트림을 생성할 때 생성자에서 무조건 초기 데이터를 전달하게 끔 되어 있습니다. 만약 구독 시점에서 이전에 전달된 데이터가 없다면..? 뭔가 이상하겠죠 ? 따라서 시작부터 무슨 데이터를 넣을껀지 결정하고 스트림을 생성하게끔 하였습니다. 변경할껀 딱 한줄이면 됩니다.

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

이렇게 스트림의 종류만 바꿔주면 나머지는 변경할 필요가 없이 동작이 됩니다. 이는 모든 것이 Observable이라는 거대한 구조 아래에 있기 때문인데요 ..! 필요로한 기능에 따라 여러가지 다양한 스트림을 통해 구현도록하고, 이를 구독하는 동작에서는 데이터를 보여주는 동작만 하게 합니다. 따라서 스트림에 따라 다양한 방식으로 구현이 되고 데이터의 조작 또한 이 스트림 안에서 진행되기 때문에 결국 화면을 보여주는 코드는 수정할 필요 없이 진행된다는 아주 스윗한 구조로 되어있습니다.

타다 ~!

그리고 뷰모델에서는 메뉴의 정보뿐만 아니라 메뉴가 선택된 개수도 데이터로 저장되어 있습니다. 이러한 정보는 총 가격이나 선택된 아이탬의 개수를 보여주는데 필요한 데이터가 되겠습니다. 따라서 이러한 메뉴데이터를 감시하는 스트림을 통해 들어오는 데이터들을 가공하여 총 가격을 전달하는 스트림과 선택된 아이탬의 개수를 전달하는 스트림을 생성시켜 줄 수 있게 됩니다.

    @IBAction func onOrder(_ sender: UIButton) {

        viewModel?.menuObservable.onNext([
            Menu(name: "커피", price: 2000, count: Int.random(in: 0...3)),
            Menu(name: "라면", price: 5000, count: Int.random(in: 0...3)),
            Menu(name: "떡볶이", price: 4000, count: Int.random(in: 0...3)),
            Menu(name: "김밥", price: 2000, count: Int.random(in: 0...3)),
            Menu(name: "사이다", price: 500, count: Int.random(in: 0...3)),
        ])

    }

 

간단하게 order버튼을 통해 선택된 개수를 랜덤으로 하는 배열을 menuObservable에게 전달해주었습니다.

따라서 이런 menuObservable에서 파생되어 총 선택된 메뉴의 개수와 총 가격을 전달해 주도록 하는 스트림 또한 생성해 줄 것 입니다.

    var menuObservable = BehaviorSubject<[Menu]>(value: [])
    
    lazy var itemCount = menuObservable.map {
        $0.map {
            $0.count
        }.reduce(0, +)
    }
    
    lazy var totalPrice = menuObservable.map {
        $0.map {
            $0.price * $0.count
        }.reduce(0, +)
    }
    

앞서 뷰모델이라는 클래스 안에서 itemCount와 totalPrice라는 변수를 생성해 주었습니다. 이는 

lazy var itemCount: Observable<Int> { get set }

보시는 바와 같이 Observable 객체이며 menuObservable의 스트림에서 파생되어 전달되는 메뉴데이터를 map operator를 통해 가공하여 전달하게 됩니다. 이러한 스트림으로부터 전달받은 데이터를 subscribe, bind를 통해 UI를 업데이트 하는 동작을 수행하게 됩니다.

        viewModel?.totalPrice
            .map { $0.currencyKR() }
            .bind(to: self.totalPrice.rx.text) // 순환참조 문제가 사라진다.
            .disposed(by: self.disposeBag)

        viewModel?.itemCount
            .map { "\($0)"}
            .observeOn(MainScheduler.instance)
            .subscribe(onNext: { [weak self] in
                self?.itemCountLabel.text = $0
            })
            .disposed(by: self.disposeBag)

앞서 보았던 코드와 같이 각각 뷰모델에 존재하는 스트림을 구독하여 화면을 조작하게끔 하였습니다.

이런식으로 Order버튼을 눌렀을 때 랜덤으로 저장된 각 메뉴의 개수에 따라 총 가격과 총 아이템의 개수가 달라지게 됩니다.

후.. 원래는 이번 편에 끝내려고 했지만 ,, 이야기할 거리가 너무 많아서 한번정도 더 해보도록 하겠습니다.! 

다음편에는 Clear버튼을 통해 데이터를 초기화 하는 방법과 테이블뷰에 있는 버튼을 통해 뷰모델을 조작하는 방법에 대해 알아보겠습니다. 그리고 다시한번 MVVM 을 되짚고 다른 여러가지 Sugar API에 대해서도 알아보고 최종적으로 마치도록 하겠습니다.

읽어주셔서 감사합니다 !!