ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Model-View-ViewModel(MVVM)
    Swift 2022. 4. 8. 15:58

    안녕하세요.

     

    저번 시간에는 MVP 패턴을 통해서 Controller의 역할을 줄이고, UIKit과 분리되어 테스트 가능한 Presenter를 만들어보았습니다.

     

    하지만 여전히 해결하지 못한 문제가 남아있었죠..

     

    바로 Presenter가 View를  알고 있다는 사실입니다.

    아무리 Protocol을 통해 참조를 하고 있더라도 View에 대한 Presenter의 종속성은 남아 있습니다. 

     

    이것마저 해결할 수 있는 방법이 있지 않을까요??

    Presenter가 View를 모르게 하는 방법..

    사실 우리는 MVC를 공부하면서 비슷한 개념을 경험해봤습니다. 감이 오시나요??

     

    차근차근 알아보도록 하겠습니다.


    1. MVC에서?

    Cocoa MVC

    MVC를 학습하면서 위의 그림을 모두 보셨을 겁니다.

     

    위 그림에서 주목해서 봐야할 부분은 봐야 할 부분은 바로..

    이곳입니다.

     

    MVC를 알아볼 때 Model이 Controller를 직접 알면 안 된다라고 공부했습니다.

    따라서 관찰자 패턴을 통해서 해당 문제를 해결했었습니다.

     

    그럼 View랑 Presenter도 비슷한 방법을 사용하면 되지 않을까??

    맞습니다.

    그래서 등장하게 된 것이 바로 MVVM(Model-View-ViewModel)입니다.


    2. MVVM

    Reference - https://tech.olx.com/clean-architecture-and-mvvm-on-ios-c9d167d9f5b3

    MVVM  역시 MVC의 Controller, MVP의 Presenter와 같이 Model과 View의 중간자 역할을 하는 ViewModel이 존재합니다.

     

    하지만 ViewModel은 Controller, Presenter와 다르게 View를 전혀 알지 못합니다.

     

    View와 ViewModel을 연결해 서로를 동기화하는 것을 Data Binding이라고 합니다. 

     

    Data Binding을 하는 방법은 여러가지(Combine, RxSwift)가 있지만, 이번에는 클로저와 didSet을 통해 간단하게 관찰 가능한 객체를 만들어보도록 하겠습니다.


    3. Observable

    class Observable<T> {
        
        var value: T {
            didSet {
                self.listener?(value)
            }
        }
        
        private var listener: ((T) -> Void)?
        
        init(_ value: T) {
            self.value = value
        }
        
        func bind(_ closure: @escaping (T) -> Void) {
            self.listener = closure
            closure(value)
        }
    }

    구조는 매우 간단합니다.

    value의 값이 변경되면 bind()를 통해 전달받은 클로저를 실행시킵니다.

    해당 클로저는 아마 대부분 UI관련 클로저겠죠??


    간단한 앱을 통해서 살펴보겠습니다.

     

     

    + 버튼과 - 버튼을 통해 Label의 값을 변경하는 앱입니다.

     

    - Model

    해당 앱에서 Model은 Label의 표시될 값과 숫자의 증감을 위한 로직입니다.

    class Amount: NSObject {
    
        @objc dynamic var number: Int
        
        override init() {
            self.number = 0
        }
        
        func increase() {
            self.number += 1
        }
        
        func decrease() {
            self.number -= 1
        }
    }

     

     

    - View

    MVVM에서는 MVP와 마찬가지로 ViewController를 View로 취급합니다.

    class ViewController: UIViewController {
        
        private lazy var increaseButton: UIButton = {
            let button = UIButton()
            button.setImage(UIImage(systemName: "plus"), for: .normal)
            button.addTarget(self, action: #selector(increaseButtonTouched(_sender:)), for: .touchUpInside)
            return button
        }()
        
        private lazy var decreaseButton: UIButton = {
            let button = UIButton()
            button.setImage(UIImage(systemName: "minus"), for: .normal)
            button.addTarget(self, action: #selector(decreaseButtonTouched(_sender:)), for: .touchUpInside)
            return button
        }()
        
        private lazy var amountLabel: UILabel = {
            let label = UILabel()
            label.font = label.font.withSize(32)
            return label
        }()
        
        override func viewDidLoad() {
            super.viewDidLoad()
            self.configureUI()
        }
        
        
        func configureUI() {
            // layout 설정
        }
        
        @objc func increaseButtonTouched(_sender: UIButton) {
            //viewModel 메서드 호출
        }
        
        @objc func decreaseButtonTouched(_sender: UIButton) {
            //viewModel 메서드 호출
        }
    }

    아직 ViewModel을 만들지 않았으니 대략적인 뼈대만 작성하겠습니다.

    비즈니스 로직은 전혀 포함하고 있지 않습니다.

     

    - ViewModel

    MVVM에서 View는 ViewModel을 알고 있습니다.

    그래도 의존성을 줄여주기 위해 ViewModel Protocol을 정의하겠습니다.

    ViewModel을 정의하는 방법에는 여러 가지 방법이 있습니다만

    Clean Architecture and MVVM on iOS에서는 Input과 Output을 분리해 정의하고 있습니다.

     

    Input은 ViewModel이 View로부터 전달받을 이벤트들입니다.

    Output은 ViewModel이 이벤트들을 처리한 후의 결과(View과 관찰할 속성)입니다.

    protocol ViewModelInput {
        func viewDidLoad()
        func didIncrease()
        func didDecrease()
    }
    
    protocol ViewModelOutput {
        var amountText: Observable<String> { get }
    }
    
    typealias ViewModel = ViewModelInput & ViewModelOutput

    이번 앱에서는 위와 같이 정의할 수 있겠습니다.

    ViewController의 viewDidLoad()에서 호출될 메서드, 각 버튼을 눌렀을 때 호출될 메서드들이 Input이 되겠죠.

     

    View가 Label에 표시할 값이 Output입니다.

     

    해당 프로토콜을 채택한 구체 타입을 만들어보겠습니다.

     

    class DefaultViewModel: ViewModel {
        
        var amountText: Observable<String>
        
        private var amount: Amount
        private var observation: NSKeyValueObservation?
        
        init(amount: Amount) { // 의존성 주입
            self.amount = amount
            self.amountText = Observable(String(amount.number))
        }
        
        func viewDidLoad() {
            self.observation = self.amount.observe(\.number, options: .new) { object, change in
                guard let number = change.newValue else { return }
                self.amountText.value = String(number)
            }
        }
        
        func didIncrease() {
            self.amount.increase()
        }
        
        func didDecrease() {
            self.amount.decrease()
        }
    }

     

    amount를 생성자에서 초기화해도 되지만 의존성 주입을 통해서 의존성을 줄여주겠습니다.

     

    Model인 amount의 상태변화는 KVO를 통해 관찰하겠습니다.

     

    이제 Binding 작업만 남았습니다.

     

    - Data Binding

    ViewController를 마저 작성하겠습니다.

    //ViewController내에서 작성되는 코드입니다.
    
    private var viewModel: ViewModel?
    
    convenience init(viewModel: ViewModel) {
        self.init(nibName: nil, bundle: nil)
        self.viewModel = viewModel
    }

    ViewController는 ViewModel 프로토콜을 참조하고 있습니다.

    마찬가지로 ViewController를 생성할 때 viewModel을 주입받겠습니다.

     

    //ViewController에서 작성되는 코드입니다.
    
    func bind() {
        self.viewModel?.amountText.bind { [weak self] value in
            self?.amountLabel.text = value
        }
    }

     

    view와 ViewModel을 Binding 할 bind() 메서드입니다.

    ViewModel의 amountText가 변경될 시 Label의 Text를 변경합니다.

     

    override func viewDidLoad() {
        super.viewDidLoad()
        self.configureUI()
        self.bind()
        self.viewModel?.viewDidLoad()
    }
    
    @objc func increaseButtonTouched(_sender: UIButton) {
        self.viewModel?.didIncrease()
    }
    
    @objc func decreaseButtonTouched(_sender: UIButton) {
        self.viewModel?.didDecrease()
    }

    적절한 위치에서 viewModel의 함수를 호출합니다.

     

    마지막으로 ViewController를 생성할 때 Model, ViewModel을 생성해 주입하도록 하겠습니다.

     

    이번 앱에서는 간단히 SceneDelegate에서 처리하도록 하겠습니다.

     

    class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    
        var window: UIWindow?
    
    
        func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
     
            guard let windowScene = (scene as? UIWindowScene) else { return }
            self.window = UIWindow(windowScene: windowScene)
            
            let amount = Amount() // Model
            let viewModel = DefaultViewModel(amount: amount) // ViewModel
            let viewController = ViewController(viewModel: viewModel) // ViewController
            
            self.window?.rootViewController = viewController
            self.window?.makeKeyAndVisible()
        }
        
    }

     

    간단한 앱을 통해 MVVM 패턴을 알아보았습니다.

     

    MVVM 역시 테스트하기 좋은 구조입니다.

    ViewModel이 SwiftUI 또는 UIKit과 같은 UI 관련된 코드를 전혀 포함하기 않기 때문입니다.

     

    또한 ViewModel은 그 어떤 View도 직접 참조하지 않습니다.

    이 말은 동일한 ViewModel을 다른 View에서 사용할 수 있다는 소리입니다.

    View와 ViewModel은 n : 1 관계이다.

    반면에 다른 패턴들에 비해 코드량이 매우 늘어나게 됩니다.

     

    위의 앱은 매우 간단한 구조임에도 불구하고

    Int -> Amount(Model) -> Observable<String>(ViewModel) -> Label(View)로의 긴 변환 과정을 거쳐야 했습니다.

    결과적으로는 모두 같은 데이터임에도 불구하고 말이죠.

     

    또한 이번에 MVVM을 공부하면서 느낀 점은 정형화된 패턴이 없다는 것이었습니다.

    여러 가지 자료를 찾아봤으나 자료마다 조금씩 다르게 MVVM을 설명하고 있었습니다.

    따라서 MVVM 패턴을 사용할 경우 팀원들 간의 조율을 통해 정확히 패턴을 인지하고 가야 할 것 같습니다.

     

    오늘은 여기까지 하겠습니다.

    혹시 제가 잘못 이해하고 있는 부분이 있다면 꼭 알려주세요!!


    Ref

    https://fitzafful.medium.com/data-binding-in-mvvm-on-ios-714eb15e3913

     

    Data Binding in MVVM on iOS

    You’ve come to realize your View Controller in your new project has become very huge…

    fitzafful.medium.com

    https://tech.olx.com/clean-architecture-and-mvvm-on-ios-c9d167d9f5b3

     

    Clean Architecture and MVVM on iOS

    When we develop software it is important to not only use design patterns, but also architectural patterns. There are many different…

    tech.olx.com

    https://medium.com/ios-os-x-development/ios-architecture-patterns-ecba4c38de52

     

    iOS Architecture Patterns

    Demystifying MVC, MVP, MVVM and VIPER

    medium.com

    https://medium.com/@dev.omartarek/mvp-vs-mvvm-in-ios-using-swift-337884d4fc6f

     

    MVP VS MVVM in iOS using swift

    Introduction:

    medium.com

     

    'Swift' 카테고리의 다른 글

    Model-View-Presenter(MVP)  (0) 2022.04.04
    Model-View-Controller(MVC)  (0) 2022.04.03
    Property Wrapper  (0) 2022.03.25
    SOLID 원칙  (0) 2022.01.28
    Codable  (0) 2021.12.28
Designed by Tistory.