MVVM
기존 MVC패턴은 View와 Model의 의존성이 존재하고, 앱의 규모가 커질수록 View의 크기가 커지고 Controller에 많은 부하가 걸려 유지보수에 어려움이 있었다. MVVM은 이를 해결하기 위한 디자인 패턴으로 Command 패턴과 Data Binding을 이용하여 View와 Model뿐만 아니라 View와 View Model 사이의 의존성까지 최소화 했다.
Model : 데이터, 네트워크 로직, 비즈니스 로직 등을 담고 있으며, 데이터를 캡슐화하는 역할
➡️ 화면에 그려지기 위한 데이터 / 모델은 하나로 존재하지 않음. 상황에 따라 다른 형태로 존재함.
View : 사용자에게 보여지는 UI 화면
ViewModel : 핵심적인 비즈니스 로직으로, View에서 받은 이벤트를 처리하고 Data Binding을 통해 즉시 View로 전달한다.
✅ MVVM은 ViewController를 View로 취급한다.
✅ View와 Model 사이에 강한 결합이 존재하지 않는다.
✅ 데이터 처리 로직과 UI간 상호 영향이 적어 모듈화를 통해 재사용성을 높이고 역할별로 Unit Test가 용이하다.
MVVM은 View와 View의 상태를 독립적으로 표현하는 것.
ViewModel은 Model의 변경을 일으키고, 변경된 Model을 사용하여 자기 자신의 상태를 갱신한다.
View와 ViewModel 사이에는 데이터 바인딩이 존재하기 때문에 View 역시 갱신된다.
Data Binding
View는 ViewModel을 보유하고, ViewModel은 Model을 보유하고 있다. 이벤트나 다른 요인으로 인해 데이터에 변동이 생기면 ViewModel은 이를 감지하고 DataBinding을 통해 View을 Update한다.
예제
본 예제는 토이프로젝트 "바꿔조"를 설계하는 과정에서 MVVM 패턴을 적용하기 위한 테스트 코드입니다.
환율API를 통해 환율 정보를 받아와 이벤트를 처리하는 과정을 간단하게 MVVM 패턴으로 구현한 코드입니다.
Model
struct Model {
var code: String
let country: String
let basePrice: Double
let currencyCode: String
}
View
import UIKit
class ViewController: UIViewController {
let viewModel = ViewModel() // View는 ViewModel을 보유
let countryLabel: UILabel = {
let label = UILabel()
label.text = "나라 이름"
label.textColor = .black
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let basePriceLabel: UILabel = {
let label = UILabel()
label.text = "기준가"
label.textColor = .black
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let usdButton: UIButton = {
let button = UIButton()
button.setTitle("달러", for: .normal)
button.setTitleColor(.white, for: .normal)
button.translatesAutoresizingMaskIntoConstraints = false
button.backgroundColor = .black
button.addTarget(self, action: #selector(usdButtonTapped), for: .touchUpInside)
return button
}()
let jpyButton: UIButton = {
let button = UIButton()
button.setTitle("엔화", for: .normal)
button.setTitleColor(.white, for: .normal)
button.translatesAutoresizingMaskIntoConstraints = false
button.backgroundColor = .black
button.addTarget(self, action: #selector(jpyButtonTapped), for: .touchUpInside)
return button
}()
let cnyButton: UIButton = {
let button = UIButton()
button.setTitle("위안화", for: .normal)
button.setTitleColor(.white, for: .normal)
button.translatesAutoresizingMaskIntoConstraints = false
button.backgroundColor = .black
button.addTarget(self, action: #selector(cnyButtonTapped), for: .touchUpInside)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .white
self.view.addSubview(self.countryLabel)
self.view.addSubview(self.basePriceLabel)
self.view.addSubview(self.usdButton)
self.view.addSubview(self.jpyButton)
self.view.addSubview(self.cnyButton)
setAutoLayout()
viewModel.onUpdated = { [weak self] in
DispatchQueue.main.async {
self?.countryLabel.text = self?.viewModel.countryString
self?.basePriceLabel.text = self?.viewModel.basePriceString
}
}
viewModel.dataLoad()
}
func setAutoLayout() {
self.countryLabel.centerYAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.centerYAnchor).isActive = true
self.countryLabel.centerXAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.centerXAnchor, constant: -50).isActive = true
self.basePriceLabel.centerYAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.centerYAnchor).isActive = true
self.basePriceLabel.centerXAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.centerXAnchor, constant: 50).isActive = true
self.usdButton.centerYAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.centerYAnchor, constant: 50).isActive = true
self.usdButton.centerXAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.centerXAnchor).isActive = true
self.jpyButton.centerYAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.centerYAnchor, constant: 50).isActive = true
self.jpyButton.centerXAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.centerXAnchor, constant: -100).isActive = true
self.cnyButton.centerYAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.centerYAnchor, constant: 50).isActive = true
self.cnyButton.centerXAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.centerXAnchor, constant: 100).isActive = true
}
@objc func usdButtonTapped(_ sender: UIButton) {
viewModel.usdDataLoad()
}
@objc func jpyButtonTapped(_ sender: UIButton) {
viewModel.jpyDataLoad()
}
@objc func cnyButtonTapped(_ sender: UIButton) {
viewModel.cnyDataLoad()
}
}
ViewModel
import Foundation
class ViewModel {
var currentModel: [Model] = [Model]() // ViewModel은 Model을 보유
var onUpdated: () -> Void = {} // Bind을 위한 closure
var countryString: String = "Loading"
{
didSet {
onUpdated()
}
}
var basePriceString: String = "Loading.."
{
didSet {
onUpdated()
}
}
let service = Service() // API 매니저 호출
func dataLoad() {
service.fetchData { [weak self] Model in
guard let self = self else { return }
self.currentModel = Model
self.countryString = self.currentModel[0].country
self.basePriceString = "\(self.currentModel[0].basePrice)"
}
}
func usdDataLoad() {
let selectCode: [String] = ["USD"]
var arr = [Model]()
for code in selectCode {
let a = self.currentModel.filter { $0.currencyCode == code }
arr.append(contentsOf: a)
}
self.countryString = arr[0].country
self.basePriceString = "\(arr[0].basePrice)"
}
func jpyDataLoad() {
let selectCode: [String] = ["JPY"]
var arr = [Model]()
for code in selectCode {
let a = self.currentModel.filter { $0.currencyCode == code }
arr.append(contentsOf: a)
}
self.countryString = arr[0].country
self.basePriceString = "\(arr[0].basePrice)"
}
func cnyDataLoad() {
let selectCode: [String] = ["CNY"]
var arr = [Model]()
for code in selectCode {
let a = self.currentModel.filter { $0.currencyCode == code }
arr.append(contentsOf: a)
}
self.countryString = arr[0].country
self.basePriceString = "\(arr[0].basePrice)"
}
}
핵심은 View는 ViewModel을 보유하고 있고, ViewModel은 Model을 보유하고 있다는 것이다.
Data Binding은 Observable, RxSwift, Combine 등 있지만 여기서는 가장 기본적인 방법인 closure와 Property Observer를 이용하여 구현하였다.
Binding을 간단하게 설명해보자면, ViewModel은 프로퍼티 옵저버(didSet)를 가지고 있는 countryString과 basePriceString이라는 값을 가지고 있다. 사용자가 View에서 이벤트(여기서는 Button Tapped 이벤트)를 처리하면 프로퍼티 옵저버가 이를 감지하여 View에서 onUpdate 클로저를 통해 UI를 업데이트 한다.
'iOS > Design Pattern' 카테고리의 다른 글
[iOS/Design Pattern] Clean Architecture를 알아보자. (0) | 2024.08.26 |
---|---|
[iOS/Design Pattern] 코디네이터 패턴 (0) | 2024.08.02 |
[iOS/Design Pattern] MVVM에 대한 나의 고찰 (0) | 2024.05.01 |
[iOS/Design Pattern] 예제로 알아보는 MVVM패턴 - 2 (0) | 2023.08.08 |
[iOS/Design Pattern] 예제로 알아보는 MVVM패턴 - 1 (0) | 2023.08.07 |