MVC가 Model-View-Controller 3가지의 그룹으로 이루어져 있다면, MVVM은 Model-View-ViewModel 3가지 그룹으로 이루어져 있는 패턴이다.
각 그룹은 다음과 같은 역할로 나눌 수 있다.
MVVM을 한마디로 정의하자면, Model의 데이터를 가공하는 ViewModel과, 그 ViewModel을 보여주는 View로 이루어진 패턴이라고 정의할 수 있다.
MVVM 패턴은 MVC 패턴과 유사하다. 하지만, 가장 큰 차이점은 View Contoller의 책임이 줄어드는 데에 있다.
MVC 패턴에서
Controller는
① Model의 데이터를 View에 맞게 뿌려주는 역할과,
② Business Logic을 수행하는 역할 두 가지를 했다면,
MVVM 패턴에서
② 역할을 ViewModel에게 넘겨준다.
이를 통해 View Controller의 역할을 줄일 수 있게 된 것이다.
주로 MVVM 패턴은 데이터를 가공할 필요가 있는 경우에 사용하게 된다.
예를 들어, 강아지(Model)가 태어난 시간을 Date 타입으로 가지고 있다고 해보자.
강아지가 몇 살인지 알고싶다. MVC라면 강아지가 몇 살인지 계산하는 로직을 View Controller에서 수행하지만, MVVM 패턴에서는 ViewModel이 수행하게 된다.
MVC는 Model-View-Controller의 약자이지만, 한 편으로는 Massive View Controller라고 부르기도한다. View Controller의 작업이 무거워짐에 따라 생겨난 이름이다. View Controller는 Life Cycle에 대한 작업과, Model의 데이터를 화면에 뿌려주는 작업, IBActions
를 이용해서 View의 Callback을 다루는 등 여러가지 역할을 하게된다. 이러한 View Controller의 역할을 줄이기 위해 나타난 패턴이 MVVM이다.
MVVM이 무엇인지 코드로 알아보자.
Pet
이라는 Model을 하나 정의한다.
import UIKit
// MARK: - Model
public class Pet {
public enum Rarity {
case common
case uncommon
case rare
case veryRare
}
public let name: String
public let birthday: Date
public let rarity: Rarity
public let image: UIImage
public init(name: String,
birthday: Date,
rarity: Rarity,
image: UIImage) {
self.name = name
self.birthday = birthday
self.rarity = rarity
self.image = image
}
}
모든 Pet은 name
, bitthday
, rarity
, image
를 가진다. 이러한 정보들을 view에 뿌려주는 작업이 필요하다. 하지만 birthday
나 rarity
의 경우 바로 보여질 수 없다(String 값이 아니기 때문에).
이런 경우 view model로 먼저 변경될 필요가 있다. 그렇다면 View Model을 추가해보자.
// MARK: - ViewModel
public class PetViewModel {
// 1
private let pet: Pet
private let calendar: Calendar
}
Pet 객체를 가지고 있고, 생일을 나타내기 위한 Calendar 객체를 가지고 있다. 초기화시 pet과 calendar를 넘겨주도록 하자. 이때 Pet은 private
로 만들고, petViewModel.pet.name
과 같이 접근하지 않기 위하여 View Model 내부에 name
이라는 변수를 하나 선언한다.
// MARK: - ViewModel
public class PetViewModel {
// 1
private let pet: Pet
private let calendar: Calendar
public init(pet: Pet) {
self.pet = pet
self.calendar = Calendar(identifier: .gregorian)
}
// 2
public var name: String {
return pet.name
}
public var image: UIImage {
return pet.image
}
// 3
public var ageText: String {
let today = calendar.startOfDay(for: Date())
let birthday = calendar.startOfDay(for: pet.birthday)
let components = calendar.dateComponents([.year],
from: birthday,
to: today)
let age = components.year!
return "\(age) years old"
}
// 4
public var adoptionFeeText: String {
switch pet.rarity {
case .common:
return "$50.00"
case .uncommon:
return "$75.00"
case .rare:
return "$150.00"
case .veryRare:
return "$500.00"
}
}
}
이제 View Model에서 생성한 값을 보여줄 차례이다.
PetView를 생성한다. PetView는 Pet의 이미지를 나타낼 imageView
, 이름을 나타낼 nameLabel
, 나이를 나타내는 ageLabel
, 가격을 나타내는 adoptionFeeLabel
를 가진다.
view의 init()
에서는 해당 값들을 초기화 해주면 되는 것이다. (자세한 코드는 생략)
// MARK: - View
public class PetView: UIView {
public let imageView: UIImageView
public let nameLabel: UILabel
public let ageLabel: UILabel
public let adoptionFeeLabel: UILabel
}
이제 이렇게 만들어진 Model-View-ViewModel을 사용할 차례이다.
let birthday = Date(timeIntervalSinceNow: (-2 * 86400 * 366))
let image = UIImage(named: "stuart")!
let stuart = Pet(name: "Stuart",
birthday: birthday,
rarity: .veryRare,
image: image)
let viewModel = PetViewModel(pet: stuart)
let frame = CGRect(x: 0, y: 0, width: 300, height: 420)
let view = PetView(frame: frame)
view.nameLabel.text = viewModel.name
view.imageView.image = viewModel.image
view.ageLabel.text = viewModel.ageText
view.adoptionFeeLabel.text = viewModel.adoptionFeeText
PlaygroundPage.current.liveView = view
View를 생성하는 작업 따로, View Model을 생성하는 작업 따로 수행하였지만, PetViewModel에서 view와 binding 하는 작업을 진행할 수 있다.
extension PetViewModel {
public func configure(_ view: PetView) {
view.nameLabel.text = name
view.imageView.image = image
view.ageLabel.text = ageText
view.adoptionFeeLabel.text = adoptionFeeText
}
}
viewModel.configure(view)
(개인적인 견해이므로 정확하지 않을 수 있습니다.)
View는 일반적으로 ViewModel과 1:1 관계를 가진다. 하지만 N:1 관계를 가질 수도 있다.
ViewModel은 View를 전혀 모른다. View만 ViewModel을 인스턴스로 갖고 있고, 데이터 바인딩을 통해 ViewModel의 데이터를 View에 표시한다. 때문에 굳이 1:1 관계로 묶여 있지 않고 필요에 따라 N:1 관계도 가능하다. 그렇기 때문에 중복 로직을 줄일 수 있고 결합도를 낮출 수 있다.
(앞서 본 예시에 따르면, ViewModel에서는 매개변수로 넘어온 하나의 View에 대해서 데이터 바인딩 작업을 통해 데이터를 View에 표시한다. 여러개의 view를 매개변수로 넘겨, 여러번의 configure를 수행할 수 있으나, view는 하나의 ViewModel을 보여주는 작업 밖에는 할 수 없다.)
Model은 ViewModel을 전혀 모른다. 하지만, ViewModel는 Model을 프로퍼티로 가지고 있으므로, 이 둘은 강한 결합도를 가지고 있다. 하나의 ViewModel에는 하나의 Model만이 매칭 될 수 있으므로 1:1 관계로 볼 수 있다. 하지만, Model은 여러개의 ViewModel를 만들어도 무방하다.
예시를 토대로, MVVM은 다음과 같은 구조를 갖는다.
Controller는 PetViewModel과 PetView를 가지고 있다. PetViewModel은 내부에 Pet에 대한 정보를 가지고 있고, PetView는 PetViewModel의 값을 이용하여 화면에 뿌려주게 된다.
만약 앱이 많은 model-to-view에 변화가 필요한 경우 MVVM은 아주 좋은 디자인 패턴이 될 수 있다.
하지만, 모든 객체들이 Model, ModelView, View의 카테고리에 맞게 사용되지 않을 수도 있다.
앱의 요구사항이 바뀌고, 바뀐 요구사항을 기반으로 다른 디자인 패턴을 골라야 하는 경우도 존재한다. 때문에, 프로젝트 초기시에 MVVM으로 설계하는 것은 좋지 않을 수 있다.
MVVM은 다른 디자인 패턴과 같이 사용하는 것도 고려해 볼 만 하다.
https://www.raywenderlich.com/34-design-patterns-by-tutorials-mvvm
정리하신 글 잘봤습니다. 감사합니다!