๋ง›๋ณด๊ธฐ์˜ ๋งˆ์ง€๋ง‰์œผ๋กœ, ๊ฐ„๋‹จํ•˜๊ฒŒ ํ”„๋กœ์ ํŠธ์— ์ ์šฉํ•ด๋ณด์ž. MVVM๊ณผ ์ฐฐ๋–ก์ธ RxSwift๋ฅผ ์ ์šฉํ•ด๋ณด๋ฉด์„œ ์•„ํ‚คํ…์ณ์™€ ์‚ฌ์šฉ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ์ต์ˆ™ํ•ด์ ธ๋ณด์ž.

ํ”„๋กœ์ ํŠธ ๊ฐœ์š”

  • ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ๊ฐ’์ด ๋ณ€๊ฒฝ๋˜๊ณ  ์•„๋ž˜์— ์ด ๊ธˆ์•ก์ด ๋‚˜ํƒ€๋‚œ๋‹ค.
  • ํ•ด๋‹น ํ”„๋กœ์ ํŠธ์˜ UI๋งŒ ๋Œ€์ถฉ ์™„์„ฑ๋œ ์ƒํƒœ์—์„œ ๋กœ์ง์„ MVVM์œผ๋กœ ๋งŒ๋“œ๋Š” ๊ฒƒ์ด ๋ชฉ์ .

๋ฌธ์ œ์ 

  • MVVM์˜ ํ•ต์‹ฌ์€, View์— ๊ด€๋ จ๋œ ๊ฐ’๋“ค์„ ๋ชจ์•„๋†“๋Š” ๊ณต๊ฐ„์œผ๋กœ์„œ, ๊ฐ’์ด ๋ณ€๊ฒฝ๋˜์—ˆ์„ ๋•Œ View์—์„œ ์ด๋ฅผ ๊ฐ€์ ธ๊ฐ€๋„๋ก ํ•˜๋Š” ๊ตฌ์กฐ๋ฅผ ๋งŒ๋“œ๋Š” ๊ฒƒ์ด๋‹ค.
  • ๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์— ์•ž์—์„œ ๋ฐฐ์šด Rx์˜ ๊ฐœ๋…์„ ์‚ฌ์šฉํ•˜๊ธฐ๊ฐ€ ์šฉ์ดํ•œ ๊ฒƒ
    • Observable๊ณผ ๊ฐ™์€ ๊ฐœ๋…์„ ์‚ฌ์šฉํ•˜๋ฉด, View ์—์„œ View Model์˜ ๊ฐ’์„ subscribeํ•˜๋ฉด ํ•ด๊ฒฐ๋˜๊ธฐ ๋•Œ๋ฌธ.
  • ํ•˜์ง€๋งŒ ๋ฌธ์ œ๊ฐ€ ์žˆ๋Š”๋ฐ, View์—์„œ ๋ฐœ์ƒํ•˜๋Š” action์„ ๊ธฐ๋ฐ˜์œผ๋กœ ViewModel์˜ ๊ฐ’์„ ๋ณ€๊ฒฝํ•ด์•ผ ํ•˜๋Š” ํ•„์š”์„ฑ์ด ์ƒ๊ธด๋‹ค.
  • Observable์€ ๋‹จ์ˆœํžˆ ๊ฐ’์„ ๋ฐ›์•„๋จน๋Š” ๋…€์„์ด๊ธฐ ๋•Œ๋ฌธ์— ์ด๊ฒƒ์ด ๋ถˆ๊ฐ€๋Šฅํ•˜๋‹ค.
  • ์ด๋Ÿฐ ํ•„์š”์„ฑ์— ์˜ํ•ด ๋‚˜์˜จ ๊ฒƒ์ด PublishSubject ๋ผ๋Š” ๋…€์„์ด๋‹ค.

๊ฐœ์š”

 
import Foundation
import RxSwift
 
class MenuListViewModel {
    
    var menuObservable = PublishSubject<[Menu]>()
    
    lazy var itemsCount = self.menuObservable.map {
        $0.map { $0.count }.reduce(0, +)
    }
    lazy var totalPrice = self.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),
            Menu(name: "ํŠ€๊น€1", price: 100, count: 0),
        ]
        
        self.menuObservable.onNext(menus)
    }
    
}
  • ์ด๋Ÿฐ ๋ทฐ๋ชจ๋ธ์„ ๋งŒ๋“ค์–ด๋†“๊ณ , view์˜ ๊ด€๋ จ๋œ ๊ฐ’์„ ์—ฌ๊ธฐ์— ์—…๋ฐ์ดํŠธ๋ฅผ ํ•˜๊ณ , subscrie๋ฅผ ํ†ตํ•ด์„œ view๋ฅผ ์ž๋™ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๊ฒƒ์ด ๋ชฉ์ ์ด๋‹ค.
  • ์ด๋ ‡๊ฒŒ ๋ฌถ์–ด ๋†“๋Š” ์ž‘์—…์€ viewDidLoad์—์„œ ์ˆ˜ํ–‰ํ•œ๋‹ค.
override func viewDidLoad() {
    super.viewDidLoad()
        
    self.viewModel.itemsCount
        .map { "\($0)" }
        .subscribe(onNext: { [weak self] in
            self.totalPrice.text = $0
        })
        .disposed(by: self.disposeBag)
    
    self.viewModel.totalPrice
        .map { $0.currencyKR() }
        .subscribe(onNext: {
            self.totalPrice.text = $0
        })
        .disposed(by: self.disposeBag)
}
    
  • ์˜ˆ๋ฅผ ๋“ค๋ฉด ์ด๋Ÿฐ์‹์œผ๋กœ ์ฒ˜๋ฆฌํ•œ๋‹ค.
  • ๊ทธ๋Ÿฐ๋ฐ, ์ด๋Ÿฌํ•œ ๋ฐฉ๋ฒ•์—์„œ RxCocoa ๋ผ๋Š” ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์‚ฌ์šฉํ•˜๊ฒŒ ๋˜๋ฉด, ๋ณด๋‹ค ์‰ฝ๊ฒŒ ์ฒ˜๋ฆฌ๊ฐ€ ๊ฐ€๋Šฅํ•˜๋‹ค.
    • UIKit์—์„œ Rx๋ฅผ ํŽธํ•˜๊ฒŒ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด ์ œ๊ณตํ•˜๋Š” ํ”„๋ ˆ์ž„์›Œํฌ์ด๋‹ค.
  • ์œ„์˜ viewModel์„ ๋ณด๋ฉด, menu๊ฐ€ ๋ณ€๊ฒฝ๋จ์— ๋”ฐ๋ผ ํŒŒ์ƒ๋˜๋Š” ๋ณ€์ˆ˜์˜ ๊ฐ’์„ ๋งŒ๋“ค์ˆ˜ ์žˆ๊ฒŒ ๋œ๋‹ค.
  • ์ด๋ ‡๊ฒŒ ์—ฐ๊ฒฐ๋œ ๊ด€๊ณ„ ์ž์ฒด๋ฅผ stream์ด๋ผ ํ•œ๋‹ค.
self.viewModel.itemsCount
    .map { "\($0)" }
    .bind(to: itemCountLabel.rx.text)
    .disposed(by: self.disposeBag)
  • ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์ข‹์€ ์ ์ด ์žˆ๋Š”๋ฐ, ์ผ๋‹จ subscribe๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด [weak self] ๋ฅผ ์‚ฌ์šฉํ•ด์ฃผ์–ด์•ผ ํ•œ๋‹ค.
  • ํ•˜์ง€๋งŒ binding์„ ์‚ฌ์šฉํ•˜๋ฉด ์ˆœํ™˜์ฐธ์กฐ ์—†์ด ๊ฐ€๋Šฅํ•˜๋‹ค. ์•„๋งˆ ๋‚ด๋ถ€๊ตฌํ˜„์œผ๋กœ ์ˆจ๊ฒจ์ ธ ์žˆ์„ ๋“ฏ
  • ๊ทธ๋ฆฌ๊ณ  ์ฝ”๋“œ ๊ธธ์ด๋„ ์ค„์–ด๋“ ๋‹ค.

subject

  • ์œ„์˜ ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ•˜๋ฉด PublishSubject์˜ ํŠน์„ฑ์ƒ ์—ฐ๊ฒฐ์ด ๋œ ์ดํ›„์— ๋ฐ์ดํ„ฐ์— ๋Œ€ํ•ด์„œ๋งŒ ์•Œ๋ฆผ์„ ์ฃผ๊ธฐ ๋•Œ๋ฌธ์— ์ดˆ๊ธฐ ์„ค์ •๊ฐ’์— ๋Œ€ํ•ด์„œ ์—…๋ฐ์ดํŠธ๋ฅผ ์ˆ˜ํ–‰ํ•˜์ง€ ๋ชปํ•œ๋‹ค.
  • ์ด ๊ฒฝ์šฐ์—๋Š” behavierSubject๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค.
  • ๊ฒฐ๊ตญ ์ƒํ™ฉ์— ๋”ฐ๋ผ ๋ฌธ์„œ๋ฅผ ์ž˜๋ด์•ผ ํ•œ๋‹ค.
  • ๋˜ ์ค‘์š”ํ•œ ๊ฒƒ์ด tableview์˜ datasource๋ฅผ ํ•ด์ œํ•œ ์ƒํƒœ๋กœ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค.
import Foundation
import RxSwift
 
class MenuListViewModel {
    
    var menuObservable = BehaviorSubject<[Menu]>(value: [])
    
    lazy var itemsCount = self.menuObservable.map {
        $0.map { $0.count }.reduce(0, +)
    }
    lazy var totalPrice = self.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),
            Menu(name: "ํŠ€๊น€1", price: 100, count: 0),
        ]
        
        self.menuObservable.onNext(menus)
    }
    
    func clearAllItemSelections() {
        self.menuObservable
            .map { menus in
                return menus.map {
                    Menu(name: $0.name, price: $0.price, count: 0)
                }
            }
            .take(1) // ๋งŒ์•ฝ์— ์ด๊ฒŒ ์—†์œผ๋ฉด ์—ฐ๊ด€๊ด€๊ณ„๊ฐ€ ๋งŒ๋“ค์–ด์ ธ์„œ, ๊ฐ’์ด ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค ํ˜ธ์ถœ๋˜์„œ count๊ฐ€ 0์œผ๋กœ ๊ณ ์ •๋˜์–ด ์žˆ์„ ๊ฒƒ์ž„
            .subscribe(onNext: {
                self.menuObservable.onNext($0)
            })
    }
    
}
 
 
import UIKit
import RxCocoa
import RxSwift
 
class MenuViewController: UIViewController {
    // MARK: - Life Cycle
    
    let viewModel = MenuListViewModel()
    var disposeBag = DisposeBag()
    
    let cellID = "MenuItemTableViewCell"
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.viewModel.menuObservable
            .bind(to: tableView.rx.items(cellIdentifier: self.cellID, cellType: MenuItemTableViewCell.self)) { index, item, cell in
                
                cell.title.text = item.name
                cell.price.text = "\(item.price)"
                cell.count.text = "\(item.count)"
            }
            .disposed(by: self.disposeBag)
        
        self.viewModel.itemsCount
            .map { "\($0)" }
            .bind(to: self.itemCountLabel.rx.text)
            .disposed(by: self.disposeBag)
        
        self.viewModel.totalPrice
            .map { $0.currencyKR() }
            .bind(to: self.totalPrice.rx.text)
            .disposed(by: self.disposeBag)
    }
    
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        let identifier = segue.identifier ?? ""
        if identifier == "OrderViewController",
           let orderVC = segue.destination as? OrderViewController {
            // TODO: pass selected menus
        }
    }
    
    func showAlert(_ title: String, _ message: String) {
        let alertVC = UIAlertController(title: title, message: message, preferredStyle: .alert)
        alertVC.addAction(UIAlertAction(title: "OK", style: .default))
        present(alertVC, animated: true, completion: nil)
    }
    
    // MARK: - InterfaceBuilder Links
    
    @IBOutlet var activityIndicator: UIActivityIndicatorView!
    @IBOutlet var tableView: UITableView!
    @IBOutlet var itemCountLabel: UILabel!
    @IBOutlet var totalPrice: UILabel!
    
    @IBAction func onClear() {
        self.viewModel.clearAllItemSelections()
    }
    
    @IBAction func onOrder(_ sender: UIButton) {
        // TODO: no selection
        // showAlert("Order Fail", "No Orders")
//        performSegue(withIdentifier: "OrderViewController", sender: nil)
        
        self.viewModel.menuObservable.onNext([
            Menu(name: "changed", price: Int.random(in: 100...1000), count: Int.random(in: 0...10)),
            Menu(name: "changed", price: Int.random(in: 100...1000), count: Int.random(in: 0...10)),
            Menu(name: "changed", price: Int.random(in: 100...1000), count: Int.random(in: 0...10)),
        ])
    }
}
 
  • ์ฝ”๋“œ๋ฅผ ๋ˆˆ์œผ๋กœ ์ญ‰ ํ›‘์œผ๋ฉด์„œ ๊ฐ€๋ณด๋ฉด ์ƒ๊ฐ๋ณด๋‹ค ๋ณ„๊ฒŒ ์—†๋‹ค๋Š” ๊ฒƒ์„ ์•Œ๊ฒŒ๋œ๋‹ค.
  • ์•„๋‹ˆ๋‹ค. ์•Œ๋ฉด์•Œ์ˆ˜๋ก ์–ด๋ ค์› ๋‹ค. ์ทจ์†Œ

MVC, MVP, MVVM

  • ์ˆœ์ฐจ์ ์œผ๋กœ ๋”ฐ๋ผ๊ฐ€๋ฉด์„œ ์žฅ๋‹จ์ ์„ ์•Œ์•„๋ณด์ž.

MVC

![imAssets/134312855-4b0658ff-90d7-4d21-bfd6-e68e099b18ff.png){: .center-small}MVC

  • Model์—์„œ View๋ฅผ ๋ฐ”๋กœ ์—…๋ฐ์ดํŠธ
  • ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง, action ์ฒ˜๋ฆฌ๋“ฑ์„ ๋ชจ๋‘ VC๊ฐ€ ๋‹ด๋‹นํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๊ต‰์žฅํžˆ ์ปค์ง„๋‹ค.
  • ๊ทธ๋ ‡๊ฒŒ ๋˜๊ธฐ ๋•Œ๋ฌธ์— VC๊ฐ€ UIKit์„ ์ƒ์†๋ฐ›๊ฒŒ ๋˜์–ด ํ…Œ์ŠคํŠธํ•˜๊ธฐ๊ฐ€ ์–ด๋ ต๋‹ค.
  • ๊ทธ๋Ÿฌ๋ฉด ์ปจํŠธ๋กค๋Ÿฌ์˜ ์—ญํ• ์„ ์ข€ ์ œํ•œํ•ด๋ณด์ž.

MVP

![imAssets/134313110-06cabc28-4efa-4315-a01a-c7c4d5844a2c.png){: .center-small}MVP

  • ์—ฌ๊ธฐ์„œ ๋ณด๋ฉด VC๊ฐ€ View๋ฅผ ๋ชจ๋‘ ๋‹ด๋‹นํ•˜๋Š” ๊ทธ๋ฆผ์ด๋‹ค.
  • ๊ทธ๋ฆฌ๊ณ  ์ปจํŠธ๋กค๋Ÿฌ์˜ ๋กœ์ง ๋ถ€๋ถ„์„ ๋‹ด๋‹นํ•˜๋Š” ๋‹ค๋ฅธ ๊ฐ์ฒด๋ฅผ ํ•˜๋‚˜ ๋งŒ๋“ค์ž.
  • View๋ฅผ ๋ฉ์ฒญํ•˜๊ฒŒ ๋งŒ๋“ค๊ณ , ๋กœ์ง ๋‹ด๋‹นํ•˜๋Š” ๋ถ€๋ถ„์„ ๋”ฐ๋กœ ๋นผ์ž.
  • ์ผ๋‹จ ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด, Presenter๋Š” model์˜ ๊ฐ’์„ ๊ฐ€๊ณตํ•˜๊ณ , ๊ทธ๋ ค์•ผ ํ•˜๋Š” ๊ฐ’์ž์ฒด๋งŒ ๋„˜๊ฒจ๋ฒ„๋ฆฐ๋‹ค.
  • ์ด๋Ÿฌ๋ฉด View๋Š” ๊ทธ๋ฆฌ๊ธฐ๋งŒ ํ•˜๋Š” ์š”์†Œ๊ฐ€ ๋˜์–ด๋ฒ„๋ฆฌ๊ณ , Presenter์˜ ๋กœ์ง์„ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋œ๋‹ค.
  • ๊ทธ๋Ÿฐ๋ฐ ์ด๋ ‡๊ฒŒ ๋˜๋ฉด, Viewํ•˜๊ณ  (ํŠน์ • VC) Presenter๊ฐ€ 1:1์ด ๋˜์–ด์•ผ ํ•œ๋‹ค.
  • ๊ทธ๋ฆฌ๊ณ  ๊ฒฐ๊ตญ View์˜ ์•ก์…˜์ด ๋ฐœ์ƒํ•  ๊ฒฝ์šฐ์— Presenter๊ฐ€ ๋ฌด์—‡์„ ๊ทธ๋ ค์•ผ ํ•˜๋Š”์ง€ ๋‹ค ๊ณ„์‚ฐํ•ด์„œ ์ผ์ผํžˆ ์ง€์‹œ๋ฅผ ๋‚ด๋ ค์•ผ ํ•œ๋‹ค.
    • ์ง€์‹œ๋ฅผ ๋‚ด๋ฆฐ๋‹ค๋Š” ๊ฒƒ์€ ํŠน์ • ๋ทฐ๋ฅผ ์ฐพ์•„์„œ ๊ทธ ์•ˆ์˜ ํ”„๋กœํผํ‹ฐ๋ฅผ ์ฐพ์•„์„œ ๊ฐ’์„ ์—…๋ฐ์ดํŠธ ํ•˜๋Š” ๊ฒƒ์„ ๋งํ•จ
  • ๊ทธ๋ฆฌ๊ณ  ๋งŒ์•ฝ์— ๋˜‘๊ฐ™์€ ๊ฐ’์„ ๋ฐ˜์˜ํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ์—๋„ View์™€ Presenter๊ฐ€ 1:1์ด์–ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ค‘๋ณต์ฝ”๋“œ๊ฐ€ ๋ฐœ์ƒํ•จ

MVVM

![imAssets/134313189-54dcf074-154d-43ec-89a0-40a89781ac92.png){: .center-small}MVVM

  • ๊ทธ๋ž˜์„œ View์™€ Model์„ 1:๋‹ค ๊ด€๊ณ„๋กœ ๋งŒ๋“ค์–ด๋ฒ„๋ฆผ
  • ์—ฌ๊ธฐ์„œ ์กฐ์‹ฌํ•ด์•ผ ํ•˜๋Š” ๊ฒƒ!
    • json์œผ๋กœ ๋ฐ›์•„์˜ค๋Š” ๋ฐ์ดํ„ฐ์˜ ๋ชจ๋ธ (์ฆ‰, json์„ ํŒŒ์‹ฑํ•ด์„œ ๋งŒ๋“ค์–ด์ง€๋Š” ๋ชจ๋ธ)์„ Domain Model์ด๋ผ ๋ถ€๋ฅด๊ณ 
    • ์‹ค์ œ ํ™”๋ฉด์— ๋ณด์—ฌ์งˆ model (์œ„์˜ ๊ฒฝ์šฐ์—๋Š” count, id ์™€ ๊ฐ™์€ ๋‹ค๋ฅธ ๋ณ€์ˆ˜๊ฐ€ ํ•„์š”ํ–ˆ์Œ) ์—ญ์‹œ Viewmodel์ด๋ผ ๋ถ€๋ฆ„
    • ์ด๊ฑด backend์™€ ์†Œํ†ตํ•˜๋Š”๋ฐ ์žˆ์–ด ๋‚ด๋ ค์˜ค๋Š” ๋ฐ์ดํ„ฐ์™€ ์‹ค์ œ ๋งŒ๋“ค๋ฉด์„œ ๋ณด์—ฌ์งˆ ๋ชจ๋ธ์— ์ฐจ์ด๊ฐ€ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๋ฐœ์ƒํ•จ
    • ์—ฌ๊ธฐ์„œ๋Š” Architecture์ ์ธ ์ธก๋ฉด์—์„œ ViewModel์„ ๋งํ•จ
    • ์ด๋Š” ๋ณด์—ฌ์ง€๋Š” ํ™”๋ฉด๊ณผ Model ์‚ฌ์ด์—์„œ ๋ณด์—ฌ์งˆ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ง„ ์ƒํƒœ๋กœ ์กด์žฌํ•˜๋Š” ๊ฐ์ฒด๋ฅผ ๋งํ•จ
  • ํ•ต์‹ฌ์€ View๊ฐ€ ๊ด€์ฐฐ์„ ํ•˜๊ณ  ์žˆ๋‹ค๊ฐ€, ์ž๊ธฐ๊ฐ€ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ง€๊ณ  ๊ฐ€๋Š” ๊ฒƒ์ž„
  • ์ด๋ ‡๊ฒŒ ๋˜๋ฉด ๋‹ค๋ฅธ ๋ทฐ์—์„œ ๊ฐ™์€ ๋ทฐ๋ชจ๋ธ์„ ๋ฐ”๋ผ๋ณด๊ณ  ๋ณด์—ฌ์ง€๋Š” ๋ฐฉ๋ฒ•์„ ๋‹ฌ๋ฆฌํ•  ์ˆ˜ ์žˆ์Œ
    • ๊ฐ™์€ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ง€๊ณ  ํ•˜๋‚˜๋Š” ํ…Œ์ด๋ธ”๋ทฐ, ํ•˜๋‚˜๋Š” ์„ฌ๋„ค์ผ๋ทฐ๋กœ ๋งŒ๋“ค์ˆ˜ ์žˆ๋Š” ๊ฒƒ๊ณผ ์ผ๋งฅ ์ƒํ†ตํ•จ

์ฃผ์˜์‚ฌํ•ญ ๋ฐ ํŒ

  • ์œ„์—์„œ ์ฝ”๋“œ๋ฅผ ์ญ‰ ๋ณด๋ฉด, Error๊ฐ€ ๋‚ฌ์„ ๋•Œ, observable์ด ๋Š์–ด์ง„๋‹ค๊ณ  ํ–ˆ๋‹ค.
  • ๊ทธ๋ ‡๋‹ค๋ฉด ๋งŒ์•ฝ UI์ชฝ์—์„œ ์—ฐ๊ฒฐ์„ ํ•ด๋‘์—ˆ๋Š”๋ฐ (bind) ์—๋Ÿฌ๊ฐ€ ๋‚˜๋ฒ„๋ฆฌ๋ฉด ์–ด๋–กํ• ๊นŒ?
  • stream ์ž์ฒด๊ฐ€ ๋Š์–ด์ ธ๋ฒ„๋ ค์„œ ๋‹ค์Œ ๋™์ž‘ (์˜ˆ๋ฅผ ๋“ค์–ด ๋ทฐ๋ฅผ ๋ฆฌ๋กœ๋“œํ•˜๋Š”)์„ ํ•˜๋”๋ผ๋„ ํ™”๋ฉด์ด ์—…๋ฐ์ดํŠธ ๋˜์ง€ ์•Š์„ ๊ฒƒ์ด๋‹ค.
  • ๊ทธ๋ž˜์„œ ํ•ต์‹ฌ์€, UI์ชฝ์—์„œ binding์„ ๊ฑธ ๋•Œ๋Š” ์—๋Ÿฌ๊ฐ€ ๋‚˜๋”๋ผ๋„ ํ•ด๋‹น ๋ฐ”์ธ๋”ฉ์ด ๋Š์–ด์ง€๊ฒŒ ํ•˜๋ฉด ์•ˆ๋œ๋‹ค.
  • ๊ทธ๋ž˜์„œ ์•„๋ž˜์™€ ๊ฐ™์ด ์‚ฌ์šฉํ•œ๋‹ค.
self.viewModel.itemsCount
    .map { "\($0)" }
    .catchErrorJustReturn("")
    .observeOn(MainScheduler.instance)
    .bind(to: self.itemCountLabel.rx.text)
    .disposed(by: self.disposeBag)
  • ์ด๋ ‡๊ฒŒ ์—๋Ÿฌ๋‚˜ ๊ฐ€๋ฉด ๊ทธ๋ƒฅ ๋นˆ ์ŠคํŠธ๋ง์„ ๋ฆฌํ„ดํ•ด๋ผ ์™€ ๊ฐ™์€ operator๊ฐ€ ์กด์žฌํ•œ๋‹ค.
  • ๊ทธ๋Ÿฐ๋ฐ ์ž˜ ์ƒ๊ฐํ•ด๋ณด๋ฉด, UI์—์„œ๋Š” ํ•ญ์ƒ ์ด ์„ธ๊ฐœ๋ฅผ ์„ธํŒ…์„ ํ•ด์ค˜์•ผ ํ•œ๋‹ค.
    • catchErrorJustReturn
    • observeOn
    • bind
  • ๊ทธ๋ž˜์„œ ๊ท€์ฐฎ์•„์„œ driver๋ผ๋Š” ๊ฒƒ์„ ๋งŒ๋“ค์—ˆ๋‹ค.
  • driver๋Š” ํ•ญ์ƒ main thread์—์„œ ๋™์ž‘ํ•œ๋‹ค.
self.viewModel.totalPrice
    .map { $0.currencyKR() }
    .asDriver(onErrorJustReturn: "")
    .drive(itemCountLabel.rx.text)
    .disposed(by: self.disposeBag)
  • ๋Š์–ด์ง€์ง€ ์•Š๋Š” bind๋ฅผ ๋งŒ๋“œ๋Š” ๋ฐฉ๋ฒ•์ด๋‹ค.
  • ์ž, ์—ฌ๊ธฐ๊นŒ์ง€ ๋ณด๋ฉด, ์‘? ๊ทธ๋Ÿผ ์• ์ดˆ์— ๋ฐœํ–‰ํ•˜๋Š” ์ชฝ์—์„œ๋„ Error๋‚˜ Complete ์ž์ฒด๊ฐ€ ํ•„์š”์—†๋Š” ๊ฑฐ์•„๋‹˜?
  • ์• ์ดˆ์— ๋ฐœํ–‰ํ•  ๋•Œ, onNext๋งŒ ์žˆ์œผ๋ฉด ์ €๋Ÿฐ ์ฒ˜๋ฆฌ ์ž์ฒด๋ฅผ ์•ˆํ•ด์ค˜๋„ ๋˜์ž–์•„?
  • ๊ทธ๋ž˜์„œ ๊ทธ๋Ÿฐ๊ฒŒ ์žˆ๋‹ค. ๋Š์–ด์ง€์ง€ ์•Š๋Š” Subject (Subject๋Š” ์™ธ๋ถ€์—์„œ Observable์˜ ๊ฐ’์„ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๋Š” ๊ฐ์ฒด๋ฅผ ๋งํ•จ)
  • RxRelay
import RxRelay
var menuObservable = BehaviorRelay<[Menu]>(value: [])
 
func clearAllItemSelections() {
    _ = self.menuObservable
        .map { menus in
            return menus.map { menu in
                Menu(id: menu.id, name: menu.name, price: menu.price, count: 0)
            }
        }
        .take(1) // ๋งŒ์•ฝ์— ์ด๊ฒŒ ์—†์œผ๋ฉด ์—ฐ๊ด€๊ด€๊ณ„๊ฐ€ ๋งŒ๋“ค์–ด์ ธ์„œ, ๊ฐ’์ด ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค ํ˜ธ์ถœ๋˜์„œ count๊ฐ€ 0์œผ๋กœ ๊ณ ์ •๋˜์–ด ์žˆ์„ ๊ฒƒ์ž„
        .subscribe(onNext: {
            self.menuObservable.accept($0)
        })
}
  • ์ด๋Ÿฐ ์‹์œผ๋กœ Reray๋กœ ์„ ์–ธ์„ ํ•ด์ฃผ๊ณ , onNext๊ฐ€ ์•„๋‹ˆ๊ณ  accept๋ผ๋Š” ๋ฉ”์„œ๋“œ๋กœ ๋ณ€๊ฒฝํ•ด์ฃผ๋ฉด ๋œ๋‹ค.
  • ๋‹ค๋ฅธ ๋™์ž‘์€ ๋ชจ๋‘ ๊ฐ™๋‹ค.
  • ์• ์ดˆ์— onNext๋ฐ–์— ์—†๋‹ค. ์˜ค๋กœ์ง€ ๊ฐ’์„ ๋ฐ›์•„๋“ค์ผ ์ˆ˜ ๋ฐ–์— ์—†๋‹ค.

์ •๋ฆฌ

  • MVVM ์€ ๋ทฐ์™€ ๊ด€๋ จ๋œ ๊ฐ’์„ ๋ชจ์•„๋†“์€ ๋ทฐ๋ชจ๋ธ์„ ๋งŒ๋“ค๊ณ , ์ด๋ฅผ View์—์„œ ๊ฐ์ง€๋œ ๋ณ€ํ™”๋ฅผ ์ ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์ด๋‹ค.
  • MVVM์„ ๊ตฌํ˜„ํ•˜๋Š”๋ฐ ์žˆ์–ด์„œ RxSwift๊ฐ€ ํ•„์ˆ˜์ ์ธ ๊ฒƒ์€ ์•„๋‹ˆ๋‹ค. ๋ทฐ์™€ ๊ด€๋ จ๋œ ๋ชจ๋ธ์„ ๋งŒ๋“ค๊ณ , ๋ณ€ํ™”๊ฐ€ ์ผ์–ด๋‚ฌ์„ ๋–„(didset) View๋ฅผ ์—…๋ฐ์ดํŠธ๋งŒ ์‹œ์ผœ์ค„ ์ˆ˜ ์žˆ๋‹ค๋ฉด MVVM์˜ ์ผ์ข…์ด๋ผ๊ณ  ํ•  ์ˆ˜ ์žˆ๋‹ค.
    • MVVM์€ ๋ทฐ์™€์˜ ์ข…์†์„ฑ์„ ์ตœ์†Œํ•œ์œผ๋กœ ๋‚ฎ์ถ”์–ด ํ…Œ์ŠคํŠธ๋ฅผ ํ•˜๊ธฐ ์šฉ์ดํ•œ ๊ตฌ์กฐ๋กœ ๋งŒ๋“ค๊ณ , ์ค‘๋ณต๋œ ์ฝ”๋“œ๋ฅผ ์ค„์ธ๋‹ค๋Š” ์ ์—์„œ ์ข‹์€ ๊ตฌ์กฐ์ด๋‹ค.
  • ํ•˜์ง€๋งŒ ์ด๋Ÿฌํ•œ ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ•˜๋Š”๋ฐ ์žˆ์–ด์„œ data Binding์„ ํ•œ๋‹ค๋ฉด ๋” ๊ตฌ์กฐ์ ์œผ๋กœ ๊น”๋”ํ•˜๊ฒŒ ์ฒ˜๋ฆฌ๊ฐ€ ๊ฐ€๋Šฅํ•˜๋‹ค.
    • data binding์ด๋ž€, Viewmodel์— ์žˆ๋Š” ๊ฐ’์ž์ฒด์™€ View์˜ ๋ณ€ํ™”๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ์š”์†Œ์™€ ์—ฐ๊ฒฐ์„ ์‹œํ‚ค๋Š” ๊ฒƒ์„ ๋งํ•œ๋‹ค.
  • ์ด ๊ณผ์ •์—์„œ KVO์™€ ๊ฐ™์€ ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ•  ์ˆ˜๋„ ์žˆ์„ ๊ฒƒ์ด๋‹ค.
    • KVO๋Š” NSObject๋ฅผ ์ƒ์†ํ•ด์„œ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋ฌด๊ฑฐ์šด ํŽธ
  • ์ด๋Ÿฐ ์ƒํ™ฉ์—์„œ RxSwift๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ฒ˜๋ฆฌํ•œ๋‹ค๋ฉด ์ข‹์€ ๋ฐฉํŽธ์ด ๋  ์ˆ˜ ์žˆ๋‹ค.
    • RxSwift๋Š” ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ์— ์žˆ์–ด ํƒ€์ž…์œผ๋กœ ๋ฆฌํ„ด๋ฐ›๊ธฐ ์œ„ํ•œ ์˜๋„๋กœ ๋งŒ๋“ค์–ด์กŒ์ง€๋งŒ, ๋ฐ์ดํ„ฐ ํ๋ฆ„์„ ์ฒ˜๋ฆฌํ•˜๋Š”๋ฐ ์šฉ์ดํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.
    • ์‹ค์ œ๋กœ ๋ฐ›์•„์„œ ์ฒ˜๋ฆฌํ•˜๋Š” ๋…€์„์ด ๊ตฌ๋…์„ ํ•˜๊ณ , ํ•ด๋‹น ๊ฐ’์ด ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐœํ–‰ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๊ฐ’์„ ๊ด€์ฐฐํ•˜๋Š” ๋กœ์ง์ž์ฒด๋Š” ์œ ์‚ฌํ•˜๋‹ค.
  • ์ด ๋•Œ, ์ด๋ฅผ UI์™€ ์—ฐ๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” RxCocoa ํ”„๋ ˆ์ž„ ์›Œํฌ์™€ RxRelay๋ฅผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋ฉด ๊ฐ„๊ฒฐํ•œ ์ฒ˜๋ฆฌ๊ฐ€ ๊ฐ€๋Šฅํ•˜๋‹ค.
  • Combine์€ apple์ด ๋งŒ๋“  ๋ฐ˜์‘ํ˜• ํ”„๋ ˆ์ž„์›Œํฌ๋ผ๊ณ  ์ƒ๊ฐํ•˜๋ฉด ์ข‹๋‹ค.
    • iOS 13์ดํ›„ ๋ถ€ํ„ฐ ์ ์šฉ์ด ๋˜๊ธฐ ๋•Œ๋ฌธ์—, ํ˜„์žฌ๋กœ์„œ๋Š” ์•„์ง ์ ์šฉํ•˜๊ธฐ๊ฐ€ ์กฐ๊ธˆ ์–ด๋ ค์šด ์ƒํƒœ์ด๋‹ค.
    • ํ•˜์ง€๋งŒ 1๋…„ ์ •๋„ ์‚ฌ์ด์˜ ์‹œ๊ฐ„๋‚ด์—์„œ ์‚ฌ์šฉํ•  ๊ฐ€๋Šฅ์„ฑ์ด ๋†’๊ธฐ ๋•Œ๋ฌธ์—, ํ˜„์žฌ๋Š” RxSwift๋กœ ๊ฐœ๋ฐœ์„ ์ง„ํ–‰ํ•ด๋ณด๊ณ , ๋‚˜์ค‘์— Combine์œผ๋กœ ๋ณ€๊ฒฝํ•˜๋Š” ์—ฐ์Šต์„ ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์•„๋ณด์ธ๋‹ค.
    • ๊ทธ๋ฆฌ๊ณ  Combine๊ณผ SwiftUI๊ฐ„์˜ ํ™œ์šฉ๋„๊ฐ€ ์ข‹๊ธฐ ๋•Œ๋ฌธ์—, ์ด๋ฅผ ๋‚˜์ค‘์— ์—ฐ์Šตํ•ด์•ผ ํ•œ๋‹ค.
    • Flutter๋Š” ์ถ”๊ฐ€๋กœ.. ํ•˜ ํ• ๊ฒŒ ๋งŽ๋„ค.

Reference