MVVM 패턴은 MVC와 마찬가지로 자주 사용되는 디자인 패턴입니다.
(MVC 포스팅은 여기에서 볼 수 있습니다.)
UIViewController의 View와 Controller가 분리되기 어렵다는 단점을 극복하기 위해
사용되는 디자인 패턴이기도 합니다.
또한, SwiftUI에서는 MVVM을 기반으로 동작합니다.
이렇듯 MVVM은 iOS 개발에서 자주 사용되는 디자인 패턴입니다.
오늘은 MVVM 패턴에 대해 알아보고
RxSwift, Combine을 사용하지 않는 간단한 MVVM 구현을 해보도록 하겠습니다.
MVVM
MVVM 패턴은
마이크로소프트 개발자에 의해 발명되었고 2005년에 발표되었다고 하네요.
최근에 나왔다고 생각했는데 저는 이걸 17년이나 지나서 공부하고 있습니다... ㅎ;
아무튼,
MVVM은 Model - View - ViewModel로 나뉩니다.
화면을 만드는 코드와 데이터를 처리하는 코드를 분리하는 것이 MVVM의 핵심으로,
데이터 바인딩을 사용하여 View가 ViewModel 값을 관찰하여 변화를 반영합니다.
예를 들어,
버튼 클릭을 했을 때 다음 이미지가 나오는 동작을 한다고 합시다.
MVC는 버튼을 눌리면 이미지를 "바꾼다"의 개념이라면
MVVM은 버튼을 눌렀을 때 뷰모델의 데이터가 바뀌고,
데이터가 바뀌니 이미지도 "자연스럽게 바뀌어진다"는 개념입니다.
그래서 View가 ViewModel의 값을 관찰한다는 말을 한 것이고,
이를 도와주는 프레임워크가 Combine, 라이브러리가 RxSwift 입니다.
MVVM의 핵심에 대해서는 알아봤으니
Model, View, ViewModel에 대해 하나씩 알아보도록 합시다.
Model
MVVM의 Model은 데이터 구조를 정의하기만 하면 됩니다.
struct Person {
let name: String
var age: Int
}
Model은 다른 것은 신경쓸 필요가 없어요.
View
MVVM의 View는 사용자와의 상호작용을 통해 이벤트가 일어나면
ViewModel에게 이벤트가 일어났다고 알려주고,
ViewModel이 업데이트 요청한 데이터를 보여줍니다.
MVVM에서는 ViewController가 View 역할을 하고,
디자인적인 요소뿐만 아니라
ViewModel로부터 데이터를 가져오고 ViewModel의 어떤 메서드를 이용할지에 대해서도
구현되어야 합니다.
당연히 코드를 잘 나누고 중복을 피해서 재사용성도 챙겨야 하고요.
ViewModel
ViewModel은 앱의 핵심적인 비즈니스 로직을 가지고 있습니다.
사용자와의 상호작용을 View가 보내주면 그에 맞는 이벤트를 처리하고,
Model의 데이터 읽기, 쓰기(업데이트), 삭제를 진행합니다.
ViewModel은 View가 무엇인지, View가 무엇을 하는지 알면 안 됩니다.
ViewModel이 View에 대해 모르면 테스트가 용이해지기 때문입니다.
MVVM 장점
MVVM은 View 로직과 비즈니스 로직을 분리할 수 있어 생산성도 높습니다.
ViewModel에서 View를 몰라도 되니 UI가 나오지 않아도 개발이 가능해요.
의존성이 없기 때문에 테스트가 수월해지고
View와 ViewModel이 1:N 관계이므로 중복되는 로직을 모듈화해서 여러 View에 재사용할 수도 있습니다.
이런 장점때문에 현재 많은 기업에서 채택하여 사용 중입니다.
채용 공고를 보시면 우대사항에 MVVM은 꼭 들어가 있는 거 같아요 ㅎㅎ.. ㅠ
MVVM 단점
MVVM은 MVC에 비해 설계가 복잡합니다.
데이터 바인딩이라는 개념을 모르면 아예 시도조차 할 수 없겠죠.
그래서 간단한 프로젝트는 오히려 MVC보다 생산성이 떨어질 수 있습니다.
또한 ViewModel에 모든 비즈니스 로직이 들어가기 때문에
ViewModel이 비대해집니다.
데이터 바인딩으로 인한 메모리 소모도 심하다고 합니다.
MVVM 구현
MVVM을 직접 구현해봅시다. (전체 코드는 여기로)
View가 ViewModel을 관찰하는 데이터 바인딩을 어떻게 하느냐가 핵심일 거 같은데요!
보통은 Combine이나 RxSwift를 사용하지만
이번 포스팅에서는 위 두 개를 사용하지 않고 구현해보도록 하겠습니다.
Next 버튼을 누르면 이미지와 이름이 바뀌는 동작을
MVVM으로 구현해보겠습니다.
Observable
Observable은 View가 ViewModel을 관찰하는데 사용됩니다.
Listener는 익명 클로저로 구현하고
value는 프로퍼티 옵저버인 didSet을 이용해 값이 들어오면 Listener가 동작하게 합니다.
final class Observable<T> {
typealias Listener = (T) -> Void
var listener: Listener?
var value: T {
didSet {
listener?(value)
}
}
init(_ value: T) {
self.value = value
}
func bind(listener: Listener?) {
self.listener = listener
listener?(value)
}
}
View에서 bind()의 Listener에 변경되었을 때 진행할 동작을 전달합니다.
이렇게 등록된 listener는 value의 didSet에 의해 value가 변경될 때마다 수행됩니다.
Model
모델은 정말 간단합니다.
struct Human {
let face: UIImage
let name: String
}
얼굴 이미지와 이름에 대한 변수만 있습니다.
View
View는 ViewController를 이용합니다.
MVVM 순서로 적고 있는데 View에 ViewModel이 쓰이니
이해가 힘드시면 ViewModel을 먼저 보는 것도 좋을 거 같아요.
class ViewController: UIViewController {
//MARK: - IBOutlet
@IBOutlet weak var faceImageView: UIImageView!
@IBOutlet weak var nameLabel: UILabel!
//MARK: - Properties
private let humanViewModel: HumanViewModel = HumanViewModel()
//MARK: - Life Cycles
override func viewDidLoad() {
super.viewDidLoad()
bind()
}
//MARK: - Methods
private func bind() {
humanViewModel.faceImage.bind { faceImage in
print("[View] change faceImage!")
self.faceImageView.image = faceImage
}
humanViewModel.name.bind { name in
print("[View] change name!")
self.nameLabel.text = name
}
}
//MARK: - Actions
@IBAction func clickedNextButton(_ sender: UIButton) {
print("[View] Click!!")
humanViewModel.clickedNextButton()
}
}
viewDidLoad()가 호출되면 각 View를 bind 해줍니다.
bind는 매개변수가 클로저이므로 위처럼 축약해서 쓸 수 있어요.
faceImage의 value가 변경되면 "self.faceImageView.image = faceImage"가 수행되고,
name의 value가 변경되면 "self.nameLabel.text = name"이 수행됩니다.
이 value의 변경은 clickedNextButton 액션에서 호출하는 ViewModel의 메서드에서 수행됩니다.
이 부분이 View가 ViewModel에게 사용자 이벤트를 알리는 역할이에요.
View가 이벤트를 받음 -> ViewModel에서 처리 -> 데이터 변경 -> View 변경 흐름입니다.
ViewModel
비즈니스 로직이 포함되는 ViewModel 입니다.
편의상 전체 데이터는 ViewModel에 넣어놨어요.
final class HumanViewModel {
//전체 데이터
let humans: [Human] = [
Human(face: UIImage(named: "face1")!, name: "제니"),
Human(face: UIImage(named: "face2")!, name: "시리"),
Human(face: UIImage(named: "face3")!, name: "애플")
]
//MARK: - Properties
let faceImage: Observable<UIImage?> = Observable(nil)
let name: Observable<String?> = Observable(nil)
private var index: Int = 0
init() {
self.faceImage.value = humans[index].face
self.name.value = humans[index].name
}
func clickedNextButton() {
print("[ViewModel] Click!!")
index = (index+1 < humans.count) ? index+1 : 0
self.faceImage.value = humans[index].face
self.name.value = humans[index].name
}
}
faceImage와 name의 Observable을 각 타입에 맞게 선언합니다.
맨 첫 상태는 아무 데이터도 없는 것이니 value를 nil로 초기화 한거에요.
View에서 버튼이 눌렸을 때 ViewModel의 clickedNextButton을 호출합니다.
이 메서드에서는 index를 변경하면서 faceImage와 name의 Observable value를 변경해요.
여기서 value가 변경되면 didSet에 의해 View의 bind { } 가 수행됩니다!
이제 흐름이 이해가 가시나요?
주석으로 흐름 보기
앱이 처음 시작하면 viewDidLoad의 bind()가 호출되면서
1번 이미지로 View가 초기화됩니다.
Next 버튼을 누르면
View가 Click 이벤트를 받고 (1번 주석)
ViewModel의 Click 메서드를 호출하여 이벤트가 왔다는 걸 알리죠. (2번 주석)
ViewModel의 clickedNextButton()에서 index를 변경합니다. (3번 주석)
변경된 index로 새로운 데이터를 가져와서
faceImage의 value와 name의 value를 변경합니다. (4번, 6번 주석)
value가 변경되면 didSet이 호출되고 View에서 등록한 Listener가 수행됩니다. (5번 7번 주석)
데이터가 변경이 되면서 didSet에 의해 자연스럽게 View의 변경이 이루어지는
흐름이 이해가 되시나요!
이것이 바로 MVVM의 핵심입니다!
느낀점
MVVM 쉽지 않다고 생각했었는데 직접 해보니 더 쉽지 않은 거 같아요.
물론 익숙해지면 괜찮아지겠지만 처음 해보는 입장에서는 MVC보다 어렵네요.
MVC에서는 버튼을 누르면 View를 바로 바꾸면 되는데
MVVM은 데이터 바인딩을 통해 ViewModel을 거쳐 View가 변경되니
흐름을 따라가기가 비교적 어려웠습니다.
확실히 이렇게 간단한 프로젝트는 MVVM이 비효율적이라는 것을 느꼈습니다.
(역시 디자인 패턴은 프바프 ㅎㅎ..)
대신 장점도 느껴졌는데요.
View에 대한 비즈니스 로직이 ViewModel에 있어서
코드를 보기가 좋았습니다.
아직 재사용까진 안 해봐서 그점은 모르겠지만... 확실히 읽기는 좋았어요!
또한, View가 보이지 않아도 테스트가 가능하다는 장점은 매우 매력적인 거 같아요.
MVC에서는 View가 제대로 안 나와서 테스트를 못한 적도 있었는데요.
MVVM에서는 UI가 다 잡히지 않더라도 테스트를 할 수 있는 점이
협업과 생산성 효율 향상에 매우 크게 영향을 주는 것 같네요.
감사합니다!
잘못된 점이 있다면 댓글로 알려주시면 감사하겠습니다.
감사합니다.