[SWIFT] MVVM 디자인 패턴

Ken Song
10 min readSep 18, 2023

--

I. MVVM Design Pattern 의 탄생배경

  • MVC의 한계를 극복하기 위하여 MVP(MVC cocoa라고 해도 할말 없음)를 거쳐 MVVM에 도달하게 되었다.
  • MVP 디자인 패턴 요약

중개자 역할로 Presenter를 추가하였다. 사실 이 presenter가 MVC cocoa의 C라고 해도 무방하다.

사실 누군가는 View에 VC가 포함되었다 라고 하기도 하는데 , VC와 presenter의 경계가 모호하다고 생각한다. view가 VC에 포함된 구조에서는 , VC는 꼭 VC에서 처리해야하는 것들 ( 생명주기, 화면 전환, 콜백 등 )만 처리하게 놔두고, 나머지는 모두 Presenter로 위임하였다.

그렇지만 , presenter가 view를 업데이트 하는 방식에서 presenter가 view를 소유하여 view를 직접 업데이트를 한다. 이는 presenter와 view가 매우 빡센 의존성을 띠고 있는 문제가 발생한다. 이를 개선하고자 고안된 것이 MVVM이다.

  • MVVM의 등장

중계자를 Presenter -> ViewModel로 명명했다.

View를 업데이트하는 코드들은 View들이 가지고, 이 코드들을 트리거하기 위해 Data Binding으로 ViewModel과 연결한다.

MVC패턴과 유사하지만 View와 View사이의 분리를 더 많이 허용하는 느낌.

More testable.

II. MVVM 의 구성요소

  1. Model (모델):

비즈니스 데이터와 관련된 로직을 포함한다. 예를 들어, 데이터베이스에서 데이터를 가져오거나 저장하는 코드가 포함된다.View, ViewModel에 대한 신경은 쓰지 않는다. 데이터를 어떻게 가지고 있을지만 걱정하며, 데이터가 어떻게 보여질 것인지에 대해서는 고려하지 않는다.

→ MVC의 Model과 크게 다르지 않다.

2. View (뷰):

사용자 인터페이스(UI)를 표시하고 사용자 입력을 받는다. 이것은 화면의 구성 요소와 레이아웃을 정의하는 역할을 한다. ViewViewModel로부터 데이터를 가져와서 표현한다. 사용자와 View의 상호 작용을 수신하고 이에 대한 처리를 ViewModel에 부탁한다.

→ 데이터를 보여주고, 사용자와의 상호작용 처리를 다른 객체에게 넘긴다는 점에서 이 부분도 MVC의 View와 비슷해보인다. 하지만 MVC, MVP와는 다르게 MVVM의 View는 보이는 부분에 대한 설정을 스스로 직접 한다.

3. ViewModel (뷰 모델):

뷰와 모델 사이의 중간 계층으로, 비즈니스 로직을 처리하고 뷰에 표시할 데이터를 가공한다. 뷰 모델은 뷰에 데이터를 제공하고, 사용자 입력을 처리하여 모델과 상호작용한다. View로부터 전달받는 요청을 처리할 로직을 담고 있으며 Model에 변화가 생기면 View에 notification을 보낸다. (데이터의 변화를 View가 알아챌 수 있도록 한다고 생각하면 된다) ViewModelViewModel 사이의 중개자 역할을 하며 Presentation Logic을 처리하는 역할을 한다.

++ 여기서 말하는 Presentation Logic이란?

일반적으로 ViewModelModel 클래스의 메서드를 호출하여 Model과 상호 작용한다. 그런 다음 ViewModelModel의 데이터를 가져오고, View가 쉽게 사용할 수 있는 형태로 가공하여 제공한다.

ex) View가 데이터를 더 쉽게 처리할 수 있도록(보여줄 수 있도록) 데이터 형식을 다시 지정 (formatting)

III. MVC의 요소들은 어떻게 쓰이는가

  • 뷰 컨트롤러(View Controller)는 MVVM 패턴에서 주로 뷰(View)에 해당한다. 뷰 컨트롤러는 사용자 인터페이스(UI)를 구성하고 관리하며 사용자 입력을 처리한다. iOS 애플리케이션의 경우, 뷰 컨트롤러는 화면의 구성 요소를 정의하고, 레이아웃을 관리하며, 화면 간의 전환 및 네비게이션을 제어한다.
  • MVVM 패턴에서 뷰(View)는 사용자 인터페이스(UI) 요소를 나타내며, 사용자와 상호 작용하는 부분을 담당한다. 이는 화면에 표시되는 UI 컴포넌트들을 의미하며, 레이아웃과 디자인에 관련된 부분도 여기에 속한다. 뷰 컨트롤러는 이러한 뷰를 관리하고 뷰 모델(ViewModel)과 상호 작용하여 데이터를 표시하거나 데이터를 업데이트할 수 있다.
  • 뷰 모델(ViewModel)은 비즈니스 로직과 뷰(View) 간의 중간 계층으로, 데이터 가공 및 변환, 비즈니스 로직 처리, 모델(Model)과의 상호 작용 등을 담당한다. 뷰(View)와 뷰 모델(ViewModel) 사이에는 데이터 바인딩 또는 데이터 관찰 기능을 통해 연결되어, 뷰의 표시 내용은 뷰 모델의 데이터와 동기화된다.

IV. 일반적인 동작흐름

  1. View에 들어온 Event를 View Model에게 알려주면 View Model은 Model을 업데이트 시킨다.

→ 이 동작흐름은 “단방향”

이 부분에서 의문이 발생할 수 있다.

그래, View -> ViewModel -> Model 로 이어지는 Event에 대한 반응흐름은 알겠는데, 그 Event에 의해 View에 변화가 생겨야 한다면, View는 어떻게 업데이트가 되는거야?

V. 바인딩??

  • MVVM을 공부하다보면 바인딩이라는 말이 계속 나오는데, 하나의 흐름으로 묶여져서 대응관계가 매핑되어 있는 느낌으로 보면된다.
  • UIKit에서 바인딩을 이해하기 위한 간단한 예제를 보자.
class ButtonViewModel {
var greeting: String = "Hello" {
didSet {
onUpdateUI?()
}
}

var onUpdateUI: (() -> Void)?

func changeGreeting() {
self.greeting = "Hi"
}
}

위의 코드는 UIButton에 대한 간단한 ViewModel을 정의한다.
UIButton에 입력될 String을 greeting 프로퍼티로 가지고 있다.
여기서 한가지 염두해두어야 할 점은, didSet을 이용하여 “프로퍼티 옵저버”를 설치해놓았다는 것이다.
이로써 greeting의 값이 변경될 때마다, onUpdateUI 클로저를 호출한다.
(단, onUpdateUI에 클로저가 할당되어 있어야 한다. 클로저 할당은 ViewModel 외부에서 해줄 것이다.)

class ViewController: UIViewController {
var viewModel = ButtonViewModel()

let changeButton: UIButton = {
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("Change Greeting", for: .normal)
button.setTitleColor(.systemBlue, for: .normal)
return button
}()

override func viewDidLoad() {
super.viewDidLoad()
setupViews()
bind()
}

func setupViews() {
view.addSubview(greetingLabel)
view.addSubview(changeButton)

NSLayoutConstraint.activate([
greetingLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
greetingLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),

changeButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
changeButton.topAnchor.constraint(equalTo: greetingLabel.bottomAnchor, constant: 20)
])

changeButton.addTarget(self, action: #selector(changeGreeting), for: .touchUpInside)
}

func bind() {
self.viewModel.onUpdateUI = {
self.updateUI()
}
// viewModel.onUpdateUI에 클로저를 할당.
}

func updateUI() {
button.setTitle(viewModel.greeting, for: .normal)
}

@objc func changeGreeting() {
viewModel.changeGreeting()
}
}

1. 버튼을 터치(touchUpInside)하면 viewModel.changeGreeting()이 호출된다.
2. viewModel의 greeting 프로퍼티가 변경된다.
3. greeting프로퍼티에 선언된 didSet에 의해 onUpdateUI()가 호출된다.
4. 이때, onUpdateUI?에 할당된 클로저가 있다면 해당 클로저를 실행한다.
5. 우리는 bind() 에서 클로저를 할당해 주었으니, updateUI()가 호출된다.
6. button.setTitle(viewModel.greeting, for: .normal)에 의해 UI가 업데이트 된다.

결국 “바인딩” 이라는 이름의 구조로 묶여있다고 표현하는 것들은 , 두 요소 사이에 observing 관계가 끼어 있으며, 그 observing 관계 때문에 View(명령하는놈, 구독자 입장)에서는 ViewModel(관찰 당하는놈)의 내부에서 바뀐 내용에 따라 UI가 업데이트 된다.

VI. MVVM의 단점

단점 1)

View와 ViewModel의 바인딩을 통한 흐름은, 엄밀하게 살펴보면 “단방향" 흐름이라고 생각하기 어렵다.

event가 Input 되었을때,

View -> ViewModel 의 방향의 흐름에서 , 결국에 View를 업데이트 하기위해 변경된 viewModel의 데이터가 view에 반영되어야 한다. 이 과정에서 옵저버나 델리게이트를 이용한다 하더라도 Binded 상태의 두 요소사이에서의 반대흐름이 필요해진다.

이러한 양방향 흐름은 권장되지 않는다.

단점 2)

바인딩 관계를 편하게 만드려면 , 반응형 프레임워크를 이용하면 된다. 그렇기 때문에 MVVM과 Rx나 Combine같은 반응형 프레임워크의 궁합이 좋다고 알려져 있다.

이 부분에 대해 동의는 하지만, 어쩌면 반응형 프레임워크에 생각이 갇히는 상황이 발생할 수 있다고 생각한다.

단점 3)

MVC의 Massive Controller 문제를 해결하기 위해 MVVM이 고안되었다고 하는데, MVVM에서는 Massive ViewModel 문제가 생길 수 있다.

참고 : https://velog.io/@ictechgy/MVVM-디자인-패턴 https://medium.com/@misha.gajdan/mvvm-binding-in-uikit-project-d541aeb49260

--

--

Ken Song

Sejong Univ. Seoul, BS in Electronic / Information communication engineering