MVVM이란?

ellyheetov·2021년 4월 24일
6
post-thumbnail

Model-View-ViewModel(MVVM)이란?

MVC가 Model-View-Controller 3가지의 그룹으로 이루어져 있다면, MVVM은 Model-View-ViewModel 3가지 그룹으로 이루어져 있는 패턴이다.

각 그룹은 다음과 같은 역할로 나눌 수 있다.

  • Model : 다루게 될 데이터
  • Views : 시각적인 요소. 전형적으로 UIView의 서브 클래스 들을 의미
  • View Models: Model이 가지고 있는 정보를 View에 보여지는 값들로 변경

MVVM을 한마디로 정의하자면, Model의 데이터를 가공하는 ViewModel과, 그 ViewModel을 보여주는 View로 이루어진 패턴이라고 정의할 수 있다.

Screen Shot 2021-04-24 at 9 16 57 PM

MVC VS MVVM

MVVM 패턴은 MVC 패턴과 유사하다. 하지만, 가장 큰 차이점은 View Contoller의 책임이 줄어드는 데에 있다.

MVC 패턴에서

Controller는
① Model의 데이터를 View에 맞게 뿌려주는 역할과,
② Business Logic을 수행하는 역할 두 가지를 했다면,

MVVM 패턴에서

② 역할을 ViewModel에게 넘겨준다.

이를 통해 View Controller의 역할을 줄일 수 있게 된 것이다.

MVVM을 언제 사용할까??

주로 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이 무엇인지 코드로 알아보자.

Model

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을 추가해보자.

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"
    }
  }
}
  1. view Model에서는 Pet 객체를 가지고 있고, pet, calendar를 매개 변수로 받아 초기화 한다.
  2. name과 image를 위한 변수를 선언한다. pet의 이름과 이미지를 각각 불러낼 수 있다.
  3. ageText라는 변수를 선언하였는데, 여기서는 pet의 생일과 오늘 날짜의 차이를 계산하여 나이를 반환해준다. string formatting없이 바로 값을 받아 보여줄 수 있게 된다.
  4. 마지막으로, adoptionFeeText를 선언하였는데, 이것은 pet의 입양 가격을 나타낸다

View

이제 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
}

Controller

이제 이렇게 만들어진 Model-View-ViewModel을 사용할 차례이다.

  1. Model객체를 생성하고
let birthday = Date(timeIntervalSinceNow: (-2 * 86400 * 366))
let image = UIImage(named: "stuart")!
let stuart = Pet(name: "Stuart",
                 birthday: birthday,
                 rarity: .veryRare,
                 image: image)
  1. ViewModel을 생성하고
let viewModel = PetViewModel(pet: stuart)
  1. View를 생성하고
let frame = CGRect(x: 0, y: 0, width: 300, height: 420)
let view = PetView(frame: frame)
  1. View와 ViewModel을 연결한다.
view.nameLabel.text = viewModel.name
view.imageView.image = viewModel.image
view.ageLabel.text = viewModel.ageText
view.adoptionFeeLabel.text = viewModel.adoptionFeeText
  1. 지금은 Playground이지만 Controller에서 지정해주면 끝이다.
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)

MVVM Diagram

Screen Shot 2021-04-24 at 9 06 52 PM

예시를 토대로, MVVM은 다음과 같은 구조를 갖는다.

Controller는 PetViewModel과 PetView를 가지고 있다. PetViewModel은 내부에 Pet에 대한 정보를 가지고 있고, PetView는 PetViewModel의 값을 이용하여 화면에 뿌려주게 된다.

MVVM 장점

  • MVVM의 관점에서 봤을 때 ViewModel은 View로부터 독립적이며, View가 필요로 하는 데이터만을 소유한다.
  • View와의 의존성을 분리할 수 있다.
  • View Controller가 거대해지는 것을 방지할 수 있으며, 이는 유지보수, 재사용성 그리고 테스트 등을 용이하게 만들어 준다.

MVVM 단점

  • ViewModel과 View의 양방향 binding 과정이 필요하다.
  • 단순한 UI의 경우, MVVM은 지나칠 수 있다.(overkill)
  • 앱이 커질 수록, data binding 과정이 복잡해 진다.

MVVM 주의 할 점

만약 앱이 많은 model-to-view에 변화가 필요한 경우 MVVM은 아주 좋은 디자인 패턴이 될 수 있다.

하지만, 모든 객체들이 Model, ModelView, View의 카테고리에 맞게 사용되지 않을 수도 있다.

앱의 요구사항이 바뀌고, 바뀐 요구사항을 기반으로 다른 디자인 패턴을 골라야 하는 경우도 존재한다. 때문에, 프로젝트 초기시에 MVVM으로 설계하는 것은 좋지 않을 수 있다.

MVVM은 다른 디자인 패턴과 같이 사용하는 것도 고려해 볼 만 하다.

참고

https://www.raywenderlich.com/34-design-patterns-by-tutorials-mvvm

profile
 iOS Developer 좋아하는 것만 해도 부족한 시간

1개의 댓글

comment-user-thumbnail
2023년 1월 11일

정리하신 글 잘봤습니다. 감사합니다!

답글 달기