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

2020. 6. 27. 09:19iOS/RxSwift

오늘도 RxSwift 입니다. 이전까지 예제 프로젝트에서 Rx에 대해 기본 개념들을 알아보는 시간들을 가져봤습니다.

그러나 이러한 개념을 잡는 것과 실제 프로젝트에 적용시켜 보는 것은 다른 문제일 수도 있죠 ..!

따라서 이번 시간에는 간단한 장바구니 프로젝트를 통해 어떤식으로 RxSwift를 프로젝트에 적용시켜야 하는지 알아보는 시간을 가져보겠습니다.

저희는 다음의 두 가지 화면에 적용시 볼 예정입니다.

각 메뉴들이 있고 매뉴의 개수를 늘이고 줄일 수 있으며 이러한 결과에 따라 가격과 최종 주문 화면이 바뀌는 방식입니다. 하지만 지금은 이러한 방식으로 돌아간다는 것만 알 뿐 실제로 아무런 동작도 하지 않는 화면입니다. 따라서 이 화면에 RxSwift를 적용시켜 보겠습니다.

먼저 주문화면에서는 메뉴에 대한 정보가 필요합니다. 메뉴에는 이름이 필요할 것이고 가격, 그리고 선택된 갯수가 필요합니다. 따라서 이러한 '뷰'를 표현해 주기 위한 '모델'을 만들어 보겠습니다.

import Foundation

// View를 위한 Model
// 'ViewModel'
struct Menu {
    var name: String
    var price: Int
    var count: Int
}

실제 데이터가 어떤 것인지는 모르지만, 위의 장바구니 뷰를 만들기 위해 만든 모델이고 이를 ViewModel이라고 부르도록 하겠습니다.

이제 이 뷰모델을 통해 메뉴 리스트를 보여줄 화면에 데이터를 뿌려주도록 해보겠습니다.

이 메뉴리스트는 테이블 뷰 형태로 제작이 되었기 때문에 더미데이터를 생성 시키고 tableView의 데이터소스에 적용시키는 기본적인 방법으로 매뉴리스트를 불러올 수 있습니다.

그러나 이런 메뉴리스트 데이터들을 뷰컨트롤러에서 만들지 말고 따로 빼주면 어떨까? 라고 생각을 하게 됩니다. 

class MenuListViewModel {
    var 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)
    ]
}

그리고 이 데이터들을 사용할 곳에 인스턴스를 생성하여 사용하도록 해줍니다. 테이블뷰에서는 이 뷰모델 인스턴스에 접근해서 데이터들을 받아오도록 처리해 줍니다.

class MenuViewController: UIViewController {
    
    let viewModel = MenuListViewModel()
    ...
}

또 메뉴 리스트 뿐만 아니라 전체 가격이라던가 선택한 메뉴의 개수 등도 표현해 주어야 하기 때문에 이런 정보들도 MenuListViewModel에 저장해 줍니다.

class MenuListViewModel {
    var 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)
    ]
    
    var itemsCount: Int = 5
    var totalPrice: Int = 10000
}

이제 이런 가격이나 개수들도 화면에서 뷰모델 인스턴스에 접근해서 각 값들을 할당해주도록 합니다.

따라서 어떤 버튼을 눌렀을 때 뷰 모델 안에 있는 정보들을 바꿔주고 UI를 업데이트 해주는 함수를 통해 뷰모델에 있는 정보를 받아와서 보여주게 됩니다. 그러나 이러한 방법은 매번 데이터들이 바뀔 때마다 UI를 업데이트 하는 함수를 호출해주어야 하는 귀찮음이 생깁니다.

이러한 귀찮음을 해결해 주기위해서 뷰모델에 있는 정보가 바뀌면 자동적으로 화면이 업데이트해주는 로직을 만들어 주겠습니다. 물론 RxSwift를 활용해서 말이죠 !

우선 변경해주어야 할 것은 받아올 정보들을 앞서 설명드렸던 Observable로 감싸주는 작업이 필요합니다. 따라서 버튼이 눌릴때마다 가격정보를 변경하도록 해주기 위해 totalPrice를 Observable<Int> 형으로 변경해 줍니다.

class MenuListViewModel {
    var 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)
    ]
    
    var itemsCount: Int = 5
    var totalPrice: Observable<Int> = Observable.just(10000)
}

그리고 10000이라는 값을 subscribe를 통해 받을 수 있는 Observable을 생성하게 됩니다.

그리고 다시 뷰컨트롤러에서 데이터를 뿌려줄 때는 viewDidLoad 에서 뷰모델에 접근하여 데이터를 뿌려주게 됩니다.

    override func viewDidLoad() {
        super.viewDidLoad()
        
        viewModel.totalPrice
            .map { $0.currencyKR() }
            .subscribe(onNext: {
                self.totalPrice.text = $0
            })
            .disposed(by: self.disposeBag)
        updateUI()
    }

하나씩 천천히 들여다 보겠습니다.

우선 viewModel에서 totalPrice에 접근하게 됩니다. 이것은 앞서 Observable 데이터를 저장해둔 변수 입니다.

이 totalPrice값을 중간에 .map 을 통해 일반적인 한국 가격표 형식을 갖춘 문자열로 변경해 줍니다.

그리고 .subscribe를 통해 onNext로 전달된 데이터를 받아서 화면안의 label을 업데이트 해주고

.disposed 를 통해 미리 만들어 놓은 disposeBag안에 이런 하나의 Stream을 저장시킵니다.

여기까지 해 두면 처음 화면이 로드될 때 뷰모델 안에서 생성한 10000이라는 숫자를 화면에 보여줄 수 있게 됩니다.

그러나 앞서 구현했던 모습은 버튼을 누르면 뷰모델 안에 있는 데이터가 변경되고 이 변경된 값에 따라 화면을 업데이트 해주는 로직이였습니다. 근데 이제 이 데이터를 Observable로 감싸게 된다면, 외부에서는 건드릴 수 없는 데이터가 되기 때문에 버튼을 누를때마다 가격이 바뀌는 동작을 해줄 수가 없게 됩니다.

따라서 이제는 Observable이 아닌 Subject라는 놈을 사용해 보도록 하겠습니다. 이 Subject라는 놈은 Observable 처럼 subscribe로 데이터를 받아올 수 도 있지만, 외부에서 안에 있는 데이터를 처리해 줄 수 있는 놈입니다.

사용방법은 거의 비슷합니다. 데이터를 받아올 때의 로직은 그대로 사용할 수 있고, 뷰모델을 만들 때 Subject의 한 종류인 PublishSubject로 데이터를 감싸서 사용하게 됩니다. 또한 Just나 create로 옵져버블을 생성할 필요 없이 인스턴스 참조를 통해 Observable처럼 사용이 가능해 집니다.

class MenuListViewModel {
    var 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)
    ]
    
    var itemsCount: Int = 5
    var totalPrice: PublishSubject<Int> = PublishSubject()
}

따라서 데이터를 뿌려주는 로직은 그대로 가져 가도록 하고 버튼을 누를때마다 이 옵져버블 같은 객체에 onNext로 새로운 데이터를 담아 보낼 수  있게 됩니다.

    @IBAction func onOrder(_ sender: UIButton) {

        viewModel.totalPrice.onNext(100)
        viewModel.itemsCount += 1
        updateUI()
        
    }
    

그리고 새로운데이터가 생길때마다 기존 의 가격과 더해주어야 하기때문에 scan이라는 operator도 사용하여 점점 가격이 올라가는  상황을 볼 수가 있습니다.

다음은 .scan을 쓴 subscribe 호출 부분입니다.

    @IBAction func onOrder(_ sender: UIButton) {

        viewModel.totalPrice
            .scan(0, accumulator: +)
            .map { $0.currencyKR() }
            .subscribe(onNext: {
                self.totalPrice.text = $0
            })
            .disposed(by: self.disposeBag)
    }
    

 

자 오늘은 Observable을 프로젝트에 적용시켜 보는 시간을 가져보았고 외부에서 ㄷㅔ이터의 처리를 위해 일반적으로 많이쓰는 PersonalPublishSubject를 알아보았습니다. Subject에는 다양한 종류의 데이터타입이 있읍니다. 이는 공식문서를 보고 상황에 따라 쓰일 수 있습니다.