iOS 개발 — MVVM 패턴이란? UIKit의 MVC와의 비교

Heechan
HcleeDev
Published in
12 min readJul 24, 2021

--

Photo by Cookie the Pom on Unsplash

지난 주에는 MVC 패턴에 대해 다루었는데, 자연스럽게 이번주에는 MVVM 패턴에 대해 얘기해보고자 한다. 평소에 만드는 앱도 SwiftUI를 이용한 MVVM 구조를 기반으로 만들고 있어 익숙하기도 하고, MVC와 어떤 차이가 있는지 알아보고 싶기도 해 이번 기회에 다뤄보고자 한다.

Model, View, ViewModel

MVVM 패턴 또한 MVC와 마찬가지로 애플리케이션 개발에 주로 사용되는 디자인 패턴이다. 이름만 보면 알 수 있지만, MVC와 다르게 Controller가 아닌 ViewModel 계층을 가지고 있다. ViewModel 또한 Controller처럼 View와 Model의 중간 계층 역할을 하고 있다.

Model

Model은 MVC에서의 Model과 마찬가지로 데이터와 관련된 코드를 담고 있다. 데이터를 담아두기 위한 구조체들은 물론, 네트워크 로직, JSON 파싱 코드를 담고 있다.

struct Person {
let name: String
var age: Int
init(json: JSON) {
name = json["name"].stringValue
age = json["age"].intValue
}
}

이런 식으로 Person에 대한 데이터를 담을 수 있는 구조체를 만들고, 네트워크를 통해 JSON 정보를 받았을 때 이를 이용해 구조체에 파싱까지 할 수 있는 init 함수까지 포함할 수 있다. 전형적인 Model 계층에 해당하는 파일이라고 볼 수 있다. (위 JSON 파싱 방식은 SwiftyJSON 라이브러리를 사용한 경우다.)

Model은 View, ViewModel 계층을 전혀 신경쓰지 않아도 된다. 데이터를 어떻게 가지고 있을 지만 생각하면 되고, 그 이상의 비즈니스 로직이나 View에서 프로퍼티들을 어떻게 보여주는지에 굳이 맞출 필요는 없다.

View

View는 앱의 UI에 대한 코드를 담고 있는 계층이다. View의 각 컴포넌트에 대한 정보를 담고, 어느 위치에 어떻게 배치될지 작성되어있다. View는 디자인적인 요소도 있지만, ViewModel로부터 데이터를 가져와 어떻게 배치할지, 특정 상황에 따라 ViewModel의 어떤 메서드를 이용할지에 대해서도 가지고 있다.

MVC와 마찬가지로 View는 재사용성이 강조되며, 컴포넌트를 적당히 잘 나누어 중복된 코드를 줄이는 것이 중요하다.

struct ContentView: View {
let personList: [Person] = [Person(name: "Lee", age: 23), Person(name: "Jeon", age: 28)]

var body: some View {
ScrollView {
ForEach(personList, id: ./self) { person in
PersonListItemView(person: person)
}
}
}
}
struct PersonListItemView: View {
let person: Person

var body: some View {
VStack(alignment: .leading) {
Text("Name: \(person.name)")
Text("Age: \(person.age)")
}
.padding(16)
}
}

View는 이런 식으로 재사용할 수 있는 View를 따로 만들어 같은 코드를 여러번 작성하지 않고 재사용성을 살릴 수 있다. 위 코드는 SwiftUI로 UI를 구성한 경우로, 각 컴포넌트들이 어떻게 배치될지 코드를 통해 짐작할 수 있다.

하지만 이 예시 코드에서는 간단하게 보여주기 위해 ViewModel을 사용하지 않고 Model 계층을 직접 정의했는데, 사실은 MVVM 패턴에서 View는 Model을 직접 소유하지 않아야 한다. ViewModel로부터 받아와서 View에 정보를 집어넣어주는 방식이 일반적이다.

ViewModel

ViewModel은 앱의 핵심적인 비즈니스 로직을 담고 있는 코드의 계층이다. MVC 패턴의 Controller와 비슷한 역할을 하고 있다. View와 Model의 사이에서 View의 요청에 따라 로직을 실행하고, Model의 변화에 따라 View를 refresh하는 등, 유사한 점이 아주 많다.

Model에 뭔가 변화가 생기면 View에게 notification을 보내주는 역할을 한다. 또한, View로부터 전달받는 요청을 해결할 비즈니스 로직들을 담고 있다. ViewModel은 UI 관련 코드로부터 완전히 분리되어있고, 따라서 ViewModel 파일에는 SwiftUI같은 UI 프레임워크를 import할 이유조차 없다.

iOS 개발이 MVC에서 MVVM으로 넘어가는 이유는

기존에 많이 사용한 프레임워크인 UIKit은 MVC 패턴을 기반으로 만들어졌다. 일단 ViewController 이름부터가 Controller가 붙어있으니… 하지만 비교적 최근에 나온 애플의 야심작 SwiftUI는 MVC가 아닌 MVVM 패턴을 기반으로 한다. 이론 상으로는 Controller 계층과 ViewModel 계층이 맡은 역할이 크게 다르지 않아 보이는데, iOS 개발에서는 MVC와 MVVM이 어떤 큰 차이를 가지고 있을까?

우선 UIKit에서는 ViewController가 주인공이고, SwiftUI에서는 View가 주인공이다. 사람에 따라 다르게 볼 수는 있겠지만, 일단 나는 그렇게 생각하고 있다. 왜냐하면 한 화면을 구성하는 기준이 아예 다르다고 생각하기 때문이다.

UIKit의 MVC에서는 ViewController가 거의 모든 역할을 하고 있었다. ViewController 단위로 한 화면(scene)이 구성되었다. Controller는 View 계층, Model 계층을 모두 소유하고 있으며, Model의 notification도, View가 유저 상호작용을 전달하는 방식도 모두 Delegation 방식을 통해 VC가 떠맡고 있었다.

하지만 SwiftUI의 MVVM에서는 View가 ViewModel을 소유하고, ViewModel이 Model을 소유하는 방식이다. Controller 단위로 화면이 구성되는 것이 아닌, 해당 화면을 주도하는 것은 View이다. 각 View가 필요한 비즈니스 로직(ViewModel)들을 가져와 사용하는 구조다. 따라서 View와 Model을 모두 알고 있었어야 하는 Controller와 달리, ViewModel은 View에 대해서는 알고 있을 필요가 없다.

또한, UIKit의 ViewController는 온전히 Controller 역할만을 하지 못하고 있었다.

ViewController는 비즈니스 로직 말고도 View에 대한 설정, 수정 등 코드를 가지고 있다. 이럴 경우 View와 Controller 계층을 제대로 분리하지 못한다는 단점이 있다. 안그래도 View, Model의 역할까지 떠안고 있는데, View 계층에 해당하는 코드까지 일부 담당해야 하니 ViewController의 길이가 길어질 수 밖에 없다.

하지만 ViewModel은 비즈니스 로직만 가지고 있도록 깔끔하게 분리된 계층이기 때문에, ViewController만큼이나 코드의 길이가 길어질 이유가 없다. View 입장에서는 필요한 비즈니스 로직을 담은 ViewModel을, 해당 화면의 니즈에 따라 골라서 사용하기만 하면 되는 입장이 된다.

이렇게 각 계층이 더욱 모듈화되면 테스트가 보다 용이해진다. 원래는 View와 Controller가 너무 밀접해 테스트를 하기 어려웠다면, 아예 이렇게 분리해둘 경우 mock하기도 편하고, 로직을 테스트하기가 쉬워지는 장점이 있다.

MVVM도 당연히 완벽한 디자인 패턴은 아니다. 하지만 iOS 개발에 있어 MVC와 MVVM은 위와 같은 차이가 있고, SwiftUI를 반응형으로 설계한 만큼 MVC보다는 MVVM을 채택해 구조적인 이득을 챙겨오기 위한 것으로 보인다.

MVVM 계층의 연결

바로 위에서 말했듯 SwiftUI는 변화에 따라 데이터를 전달하고, 그에 반응해 적절한 이벤트를 실행하는 반응형 프로그래밍, Reactive Programming 패러다임을 취하고 있다.

이를 더욱 잘 수행할 수 있는 MVVM의 핵심은 Data Binding, 데이터 바인딩이다. 데이터 바인딩은 데이터를 제공하는 자와 그 데이터를 사용하는 자를 연결시켜 동기화되도록 하는 방식이다.

SwiftUI의 View는 ViewModel을 소유하고 있으며, 이 ViewModel은 View에 의해 Observed되고 있다. 여기서 ViewModel의 특정 프로퍼티(Published되고 있는 프로퍼티)에 변화가 생기면, 그 데이터의 변화를 유저들에게 보여주기 위해 View가 다시 그려진다.

이를 이해하기 위해 간단한 예시를 한번 만들어보자.

Model은 위에서도 사용한 Person을 그대로 사용하겠다.

struct Person {
let name: String
var age: Int
}

다음 코드는 ViewModel이다.

SwiftUI에서 ViewModel은 데이터 바인딩을 위해 ObservableObject을 채택하는 경우가 대부분이다. 여기서는 person이라는 프로퍼티에 Published를 붙여줬다. View에서 이 ViewModel의 person 프로퍼티를 보고 있을 경우 person이 변화하면 그 View에게 ‘변화했다’는 사실이 전해져 View가 새로 그려질 것이다.

또한 ViewModel은 Model을 소유하고 있다. 여기서도 ViewModel은 Person(name: "Lee", age: 23) 을 직접 만들어 소유하고 있으며, addAge() 에서는 직접 접근해 값을 변경하기도 한다. 데이터를 만들고, 그 데이터를 변경시키는 앱의 핵심 로직을 ViewModel 계층에서 가지고 있다고 볼 수 있다.

이 ViewModel은 View에 대한 어떤 정보도 가지고 있지 않고 비즈니스 로직만 잘 분리해냈다고 볼 수 있다.

다음은 View이다.

이 View에서는 일단 화면이 어떻게 구성될지 알 수 있다. Text와 Button이 위 아래로 배치될 것이며, Button은 Add an year 라는 문구로 생겼을 것이고, Text는 이름과 나이를 보여주고 있을 것이다.

그리고 View는 ViewModel을 소유하고 있다. ViewModel에 ObservedObject를 달아줬기 때문에 이 View는 PersonViewModel과 바인딩되어있음을 확인할 수 있다.

Text에서 viewModel.person.name , viewModel.person.age 에 접근하고 있다. Model에 직접 접근하면 안되지 않나 싶을 수 있지만, 이 경우는 값을 get할 뿐이고 값을 변경, set하지는 않으므로 패턴을 어겼다고 보기는 어렵다.

실제로 Button을 눌렀을 때 age가 하나씩 늘어나야 하는데, 이는 View에서 직접 viewModel.person.age += 1 을 하는 것이 아니라 PersonViewModel 내부에서 값을 변경할 수 있도록 viewModel.addAge() 를 호출한다. 이는 View가 Model에 직접 접근하지 않고, ViewModel이 접근하도록 하는 MVVM 패턴의 규칙에 맞는 접근이다.

버튼을 눌렀을 때 어떤 일이 벌어지는지 정리해보면…

  • View의 Button이 유저와의 상호작용(버튼 누름)을 인지한다.
  • View 계층에서 유저와의 상호작용이 일어났을 때 하기로 정해둔 행동이 있다. 바로 viewModel.addAge()를 호출하는 것.
  • View 계층에서 ViewModel 계층으로 addAge() 를 실행한다.
  • ViewModel 계층의 addAge() 는 Model 계층의 정보를 변경한다.
  • Published가 달려있는 Model의 정보가 변경되었다는 사실을 ViewModel은 인지하고, 이를 구독하고 있는(지켜보고 있는) 객체들에게 변경되었음을 알린다.
  • View는 이 소식을 듣고 ViewModel로부터 새로운 정보를 받아와 View를 다시 그린다.

대충 이런 순서로 위 움짤에서 보여주는 기능이 실행된다고 보면 된다.

이 예시를 통해 MVC 패턴과 확실히 다른 점들을 알 수 있었다.

MVC에서는 유저 상호작용이 일어났음을 View -> Controller로 알리고 그에 따라 어떤 행동을 할지는 모두 Controller가 정했다면, MVVM의 ViewModel은 로직만 가지고 있고, 어떤 행동을 할지는 View가 정한다.

데이터가 변경되었음을 알리는 방향은 Model -> Controller, ViewModel로 동일하다. 하지만 MVC에서는 그 사실에 따라 View를 어떻게 다시 그릴지 Controller가 전한 반면, MVVM에서 ViewModel은 바로 View에게 그 소식을 전달하는 역할을 하고 View가 알아서 데이터를 받아와 알아서 다시 그린다.

결론

MVVM과 SwiftUI는 너무 익숙해서 쓰면서 그렇게 힘들지는 않았다. 이번 기회를 통해 MVC와 MVVM의 다른 점에 대해 더 확실히 알게 된 느낌…

다음주는 아마도 지금 회사에서 하고 있는 배포 자동화에 대해서 작성하지 않을까… 싶다.

참고한 것

Modern MVVM iOS App Architecture with Combine and SwiftUI (vadimbulavin.com)

--

--

Heechan
HcleeDev

Junior iOS Developer / Front Web Developer, major in Computer Science