ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [MVVM 연습 프로젝트][Lotto](1) - MVVM으로 리펙토링
    iOS 2021. 12. 30. 01:39

    이번에는 MVC로 만든 Lotto프로젝트를 MVVM으로 바꿔보겠습니다.

    MVVM flow의 핵심이 되는 Observable 클래스를 만들고, 그 클래스를 이용해서 MVVM으로 쉽게 전환할 예정입니다.

    코드는 아래 명령을 통해 다운받으실 수 있습니다.

    git clone -b lotto_1 https://github.com/sseungmn/MVVM_Practice.git

    Observable

    말 그대로, 관찰이 가능하다는 의미입니다.

    Observable.swift파일을 하나 만들어서 아래 코드를 작성합니다.

    import Foundation
    // 모든 타입에 대해서 Observable객체를 만들 수 있게 하기 위해서
    // Generic을 사용합니다. T는 Int, String, [Int], Model 등등 모든것이 될 수 있습니다.
    class Observable<T> {
      // listener에는 value의 값이 변경되었을 때 실행될 클로저를 저장합니다.
      typealias Listener = ( (T) -> Void )
      private var listener: Listener?
    
      var value: T {
        // value에는 해당 객체가 갖는 값을 저장합니다.
        didSet {
          // value의 값이 변경되면 listener을 통해서 View에 데이터가 변경되었음을 알립니다.
          // 이를 reactive 패러다임에서는 publish라고 지칭합니다.
          listener?(value)
        }
      }
    
      // 항상 객체가 생성될 때에는 값을 갖고있어야 합니다.
      init(_ value: T) {
        self.value = value
      }
    
      // value의 값이 변경되었을 때 실행될 클로저를 받아옵니다.
      // 즉 값이 변경되는것에 대한 반응을 정의합니다.
      // reactive 패러다임에서는 일반적으로 subscribe라고 하는데
      // 특히 View의 객체와 관련된 클로저에 대해서는 bind라고 하는 것 같습니다.
      func bind(_ completion: @escaping Listener) {
        // completion(value)
        // -> bind를 하자 마자 이미 있던 값에 대해서 클로저를 실행해줍니다.
        // -> reactive 패러다임에서는 해당 코드의 유무에 따라서 Observable의 종류를 나누기도 합니다.
    
        // 
        self.listener = completion
      }
    }

    ViewModel

    ViewModel은 Model을 Model답게, View를 View답게 유지할 수 있게 그 사이의 일들을 처리해주는것 이라고 생각합니다.

    LottoViewModel

    import Foundation
    
    class LottoViewModel {
      // Model에서 받아올 data를 Observable로 선언합니다.
      var num1 = Observable<Int>(0)
      var num2 = Observable<Int>(0)
      var num3 = Observable<Int>(0)
      var num4 = Observable<Int>(0)
      var num5 = Observable<Int>(0)
      var num6 = Observable<Int>(0)
      var num7 = Observable<Int>(0)
      var date = Observable<String>("")
      var money = Observable<String>("")
    
      // ViewController에서 해당 코드를 가져왔습니다.
      // 해당 method는 View를 구성하는데 관련이 없기 때문에 ViewController에 있으면 안되기 때문입니다.
      // API request의 결과로 바뀐 Model의 값들을 View에게 알려주기 위해서
      // 각각의 Observable객체의 값에 넣어줍니다. 
      // -> Observable의 값이 변경됨에 따라서 View와 bind된 클로저가 실행됩니다.
      func fetchLottoInfo(_ round: Int) {
        APIService.requestLotto(round) { [weak self] lotto, error in
          guard error == nil else { return }
          guard let lotto = lotto else { return }
    
          // Observable에 값을 대입하면, bind된 클로저가 실행됩니다.
          DispatchQueue.main.async {
            self?.num1.value = lotto.drwtNo1
            self?.num2.value = lotto.drwtNo2
            self?.num3.value = lotto.drwtNo3
            self?.num4.value = lotto.drwtNo4
            self?.num5.value = lotto.drwtNo5
            self?.num6.value = lotto.drwtNo6
            self?.num7.value = lotto.bnusNo
            self?.money.value = "\(lotto.firstWinamnt)원"
            self?.date.value = lotto.drwNoDate
          }
        }
      }
    
    }

    굳이?

    위 코드에서 DispatchQueue.main.async {}를 굳이 사용한 이유는 무엇일까요?

    생각해보면, Observable에 bind된 클로저는 View 즉, UI를 바꿔주는 명령에 해당합니다.

    UI에 대한 코드는 항상 main thread에서 실행되어야 한다는 것을 여러번 강조했었습니다.

    main thread에서 실행되게 하는 방법은 여러가지가 있을 텐데,

    아마 Observable class에서 listener가 실행되는 부분에서 thread를 바꿔줄 수도 있을 것 입니다.

    import Foundation
    
    class Observable<T> {
      typealias Listener = ( (T) -> Void )
      private var listener: Listener?
    
      // 이곳에 추가
      var value: T {
        didSet {
          DispatchQueue.main.aysnc {
            listener?(value)
          }
        }
      }
      // 혹은 이곳에 추가
      func bind(_ completion: @escaping Listener) {
        DispatchQueue.main.async {
          self.listener = completion
        }
      }
    }

    하지만 이렇게 생각해봅시다.

    우리가 굳이굳이 Observable<T>라고 선언하며 General을 사용한 이유는 범용성 때문입니다.

    어떤 Observable의 Listener에서는 UI를 변경할 수도 있고, 다른 Listener에서는 어떤 계산을 실행 수도 있습니다.

    즉 모든 Listener가 UI를 변경하는것이 아니기 때문에, 위와 같이 Observable 클래스를 변경하는 것은 취지에 어긋난다고 생각합니다.

    View

    LottoViewController

    import UIKit
    
    class LottoViewController: UIViewController {
    
      let mainView = LottoView()
      // viewModel에 대한 의존성을 추가합니다.
      // 이렇게 해서 ViewModel은 View를 모르지만, View는 ViewModel을 알고있습니다
      let viewModel = LottoViewModel()
    
      override func loadView() {
        super.loadView()
        view = mainView
      }
      override func viewDidLoad() {
        super.viewDidLoad()
        setConfiguration()
      }
      
      func setConfiguration() {
        bindView()
    
        let round = 100
        viewModel.fetchLottoInfo(round)
      }
    
      // 각각의 Observable의 값이 변경되었을 때,
      // 해당 값을 어디에 대입할지 말해줍니다.
      // Observable을 subscribe한다고도 말합니다.
      func bindView() {
        viewModel.num1.bind { num in
          self.mainView.num1Label.text = "\(num)"
        }
        viewModel.num2.bind { num in
          self.mainView.num2Label.text = "\(num)"
        }
        viewModel.num3.bind { num in
          self.mainView.num3Label.text = "\(num)"
        }
        viewModel.num4.bind { num in
          self.mainView.num4Label.text = "\(num)"
        }
        viewModel.num5.bind { num in
          self.mainView.num5Label.text = "\(num)"
        }
        viewModel.num6.bind { num in
          self.mainView.num6Label.text = "\(num)"
        }
        viewModel.num7.bind { num in
          self.mainView.num7Label.text = "\(num)"
        }
        viewModel.date.bind { date in
          self.mainView.dateLabel.text = date
        }
        viewModel.money.bind { money in
          self.mainView.moneyLabel.text = money
        }
      }
    }

    결과를 확인하면, viewController에서 임시로 대입한 100회차에 대한 정보가 잘 나오는것을 볼 수 있습니다.

    100회차 로또정보

    댓글

Designed by Tistory.