# MVVM패턴이란?
Model - ViewModel - View로 이루어진 패턴이다.
ViewModel에서 Model의 값이 변형되면 받아서 View로 띄워주고 View가 변경되면 ViewModel이 받아서 Modeld에 전달해주어 MVC패턴과 비교해 ViewModel(Controller)의 역할이 독립되어 유지보수에 좋다.
패턴에 대한 설명이나 그림은 다른 블로그에서 참고 부탁드리며 본문에선 추상적으로 이해되는 MVVM패턴을 코드로 하나씩 살펴보면서 이해해보는 시간을 가지도록 하겠다.
먼저 값이 변경되는걸 감지해주는 Observable에 대해 알아보자.
MVVM패턴의 기초 - Observable
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
var number1 = 10
var number2 = 3
print(number1 - number2) // 7
number1 = 3
number2 = 1
}
}
다음 코드에선 number1과 number2가 값이 변한다고해서 print가 다시 한 번 되진않는다.
어떻게 해야 값이 변하고나서 다시 한 번 print가 되게 할 수 있을까?
새로운 변수를 만들어 이를 구현해보자.
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
var number3 = Observable(10)
var number4 = Observable(3)
number3.bind { a in
print("Observable", number3.value - number4.value)
}
number3.value = 100
number3.value = 200
number3.value = 50
}
}
/*
bind(_:)
Observable 7
didset 100
Observable 97
didset 200
Observable 197
didset 50
Observable 47
*/
분명 bind후 print는 1번만 작성되어 있는데 값이 바뀔 때마다 출력이 계속이루어지고 있는 것을 볼 수 있다.
바로 Observable 클래스 때문이다.
Observable클래스를 직접 보기 전에 ObservableSample을 만들어 차근차근 이해해보자.
한 번 생각해보자.
값이 바뀔 때 무언가를 출력해주거나 실행해주려면 어떻게 해야할까?
그렇다. 바로 didSet 구문을 사용하면된다. 다음과 같이 말이다
class ObservableSample {
var number: Int {
didSet {
print("number changed from \(oldValue) to \(number)")
}
}
init(number: Int) {
self.number = number
}
}
// In ViewController
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
var number5 = ObservableSample(number: 100)
number5.number = 50000
number5.number = 555
}
}
// number changed from 100 to 50000
// number changed from 50000 to 555
number을 100에서 50000으로, 50000에서 555로 바꾸어주면 didSet을 통해 변화된 값을 print할 수 있다.
이를 listener라는 프로퍼티를 만들어 대입해보자. 이렇게하면 결과는 동일하게 나올 것이다.
또 추가적으로 메소드를 하나 추가해줘서 변화될 때 실행할 수 있는 함수를 만들어보자.
// In ObservableSample
class ObservableSample {
var listener: (_ old: Int, _ new: Int) -> Void = { old, new in
print("number is changed \(old) to \(new)")
}
var number: Int {
didSet {
listener(oldValue, number)
}
}
init(number: Int) {
self.number = number
}
func numberChanged(completionHandler: @escaping () -> Void) {
completionHandler()
}
}
// In ViewController
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
var number5 = ObservableSample(number: 100)
number5.number = 50000
number5.number = 555
number5.numberChanged(completionHandler: { () in
print("number changed")
})
}
}
/*
number is changed from 100 to 50000
number is changed from 50000 to 555
number changed
*/
listener로 값이 변경될 때 print가 실행되게 할 수 있게되고 numberChanged(: _ ) 메소드를 통해 ViewController에서 ObservableSample에 간섭?할 수 있게된다.
numberChanged(: _ ) 활용하면 다른 ViewController( ex. ProfileViewController, LoginViewController) 등에서 사용이 가능해질 것이다.
이제부터 MVVM패턴의 가장 기초적인? 헷갈리지만 핵심이라고 생각하는 부분에 대해 살펴보자.
// In ObservableSample
class ObservableSample {
var listener: (_ old: Int, _ new: Int) -> Void = { old, new in
print("number is changed \(old) to \(new)")
}
var number: Int {
didSet {
listener(oldValue, number)
}
}
init(number: Int) {
self.number = number
}
func numberChanged(completionHandler: @escaping () -> Void) {
completionHandler(100, 200)
✅listenr = completionHandler
}
}
// In ViewController
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
var number5 = ObservableSample(number: 100)
number5.number = 50000
number5.number = 555
number5.numberChanged(completionHandler: { (a, b) in
print("number changed \(a) -> \(b)")
})
✅number5.number = 11
✅number5.number = 22
}
}
/*
number is changed 100 to 50000
number is changed 50000 to 555
number changed 100 -> 200
number changed 555 -> 11
number changed 11 -> 22
*/
가장 핵심적인 부분은 바로 listener = completionHandler
부분이다.
처음엔 completionHandler가 메소드가 출력되면서 number changed 100 -> 200 이 출력된다.
그 후에 listener가 completionHandler가 되어서 print("number is changed \(old) to \(new)")
가 실행되지 않고 print("number changed \(a) -> \(b)")
가 실행되게 된다.
number is changed 555 to 11, number is changed 11 to 22가 출력되지 않고 number changed 555 -> 11, number chnaged 11 -> 22가 출력되는 것이 이와 같은 이유 때문이다.
이렇게하면 완벽히 ObserverSample이 변경된 것과 그 안의 로직들을 완벽하게 ViewController에서 제어할 수 있게되는 것이다.
그러면 처음부터 메소드를 실행한다면 listenr의 기존 클로저 구문은 필요가 없어지게된다. completionHandler가 대신하기 때문이다. 그러면 다음 구문도 실행이 가능하게 된다.
// In ViewController
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
var number5 = ObservableSample(number: 100)
number5.numberChanged(completionHandler: { (a, b) in
print("number changed \(a) -> \(b)")
})
number5.number = 11
number5.number = 22
}
}
// In ObservableSample
class ObservableSample {
var listener: (Int, Int) -> Void = { oldNumber, newNumber in
}
var number: Int {
didSet {
listener(oldValue, number)
}
}
init(number: Int) {
self.number = number
}
func numberChanged(completionHandler: @escaping (Int, Int) -> Void) {
listener = completionHandler
}
}
/*
number changed 100 -> 11
number changed 11 -> 22
*/
이렇게하면 completionHandler @escaping 구문을 통해 외부 즉 ViewController에서 ObservableSample의 제어?가 유동적으로 가능하게 된다.
형식만 같다면 다음과 같이 numberChanged 메소드를 변형하여 사용도 충분히 가능하다.
// In ViewController
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
var number5 = ObservableSample(number: 100)
number5.numberChanged(completionHandler: { (a, b) in
print("oldNumber + newNumber = \(a + b) ")
})
number5.number = 11
number5.number = 22
}
}
/*
oldNumber + newNumber 111
oldNumber + newNumber 33
*/
본문에선 Observable에 대해 간단히 살펴보았다. 다음 포스팅에선 Observable을 Generic화? 시켜 다양한 값들을 받고 인식할 수 있도록 만들어보자.