[Design Pattern] MVVM(Model - View - ViewModel) ν¨ν΄μ΄λ? (feat. Swift)
μλ νμΈμ Foma π» μ λλ€!
μ€λμ μ λ§ μ€λλ§μ λμμΈ ν¨ν΄μ μ 리νκ² λμλλ°μ.
κ·Έ μ€μμ κ°μ₯ μΈκΈ°(?)μκ³ ν«ν MVVM λμμΈ ν¨ν΄μ λ€λ€λ³΄λ €κ³ ν©λλ€.
(SwiftUI λν κΈ°λ³Έ λμμΈ ν¨ν΄μΌλ‘ MVVMμ μ¬μ©ν©λλ€.)
λ°λ‘ μμν κ²μ~
MVVM(Model - View - ViewModel)ν¨ν΄μ΄λ? π§
MVVMμ νλμ μννΈμ¨μ΄ μν€ν μ² ν¨ν΄ μΌλ‘ GUI μ½λλ‘ κ΅¬ννλ κ·Έλν½ μ¬μ©μ μΈν°νμ΄μ€(λ·°)μ κ°λ°μ λΉμ¦λμ€ λ‘μ§ λλ λ°±μλ λ‘μ§(λͺ¨λΈ)λ‘λΆν° λΆλ¦¬μμΌμ λ·°κ° μ΄λ νΉμ ν λͺ¨λΈ νλ«νΌμ μ’ μλμ§ μλλ‘ ν΄μ€λ€. MVVMμ λ·° λͺ¨λΈμ κ° λ³νκΈ°μΈλ°, μ΄λ λ·° λͺ¨λΈμ΄ λͺ¨λΈμ μλ λ°μ΄ν° κ°μ²΄λ₯Ό λ ΈμΆ(λ³ν)νλ μ± μμ μ§κΈ° λλ¬Έμ κ°μ²΄λ₯Ό κ΄λ¦¬νκ³ νννκΈ°κ° μ¬μμ§λ€λ κ²μ μλ―Ένλ€. μ΄λ¬ν μ μμ, λ·° λͺ¨λΈμ λ·° 보λ€λ λ λͺ¨λΈμΈ κ²μ΄λ©°, λͺ¨λ λ·°λ€μ λμ€νλ μ΄ λ‘μ§μ μ μΈν λλΆλΆμ κ²λ€μ μ²λ¦¬νλ€. - μν€ λ°±κ³Ό -
μ¦, νλ©΄μ λ§λλ μ½λμ λ€μ λ°μ΄ν°λ₯Ό μ²λ¦¬νλ μ½λλ₯Ό λΆλ¦¬ν΄μ νλ κ²μ΄ λ°λ‘ MVVMμ΄ ν΅μ¬μ΄μ£ !
λ§μ΄ν¬λ‘μννΈμ μΌ μΏ νΌμ ν λ νΌν°μ€μ μν΄ λ°λͺ λμμΌλ©° μ‘΄ ꡬμ€λ¨Όμ΄ 2005λ μ²μμΌλ‘ λΈλ‘κ·Έμ λ°ννλ€κ³ νλ€μ!
(κ·Έλλ μ΅κ·Όμ λ§λ€μ΄μ‘λ€κ³ μκ°νλλ° 16λ μ΄λ λμλ€μ..)
κ° μν μ κ·Έλ¦ΌμΌλ‘ μ΄ν΄λ³΄λ©΄ μλμ κ°μ΅λλ€.
νΉμ§
λ°μ΄ν° λ°μΈλ©μ μ¬μ©νμ¬ λ·°κ° λ·°λͺ¨λΈμ κ°μ κ΄μ°°νμ¬ λ³νλ₯Ό λ°μν©λλ€.
MVVMμ μ₯λ¨μ ππ»
μ₯μ
- λ·° λ‘μ§κ³Ό λΉμ§λμ€ λ‘μ§μ λΆλ¦¬νμ¬ μμ°μ±μ λν μ μλ€. (UIκ° λμ€μ§ μμλ κ°λ° κ°λ₯)
- ν μ€νΈκ° μμν΄μ§λ€. (μμ‘΄μ±μ΄ μκΈ° λλ¬Έ)
- λ·°μ λ·°λͺ¨λΈμ΄ 1:n κ΄κ³μ΄κΈ° λλ¬Έμ μ€λ³΅λλ λ‘μ§μ λͺ¨λν ν΄μ μ¬λ¬ λ·°μ μ μ©ν μ μλ€. (μ½λ μ¬μ¬μ© κ°λ₯)
- λ§μ κΈ°μ λ€μ΄ μ μ©νλ λμμΈ ν¨ν΄μ΄λ€.
λ¨μ
- μ€κ³νκΈ°κ° λ³΅μ‘νλ€. (Rx,λ°μ΄ν° λ°μΈλ©μ λν μ§μ νμ)
- λ·°λͺ¨λΈμ΄ λΉλν΄μ§ μ μλ€.
- λ°μ΄ν° λ°μΈλ©μΌλ‘ μΈν λ©λͺ¨λ¦¬ μλͺ¨κ° μ¬νλ€.
μ½λ ꡬν
μ€λμ κ°λ¨νκ² Rxλ₯Ό μ¬μ©νμ§ μκ³ MVVMμ ꡬνν΄λ³΄κ² μ΅λλ€.
μ΄μ μ MVC,MVPλ₯Ό ꡬννλ μΊλ¦ν° νλ©΄μ κ·Έλλ‘ μ°κ² μ΅λλ€.
(κ°λ¨νκ² κ³Όμ μ μ€λͺ λ리면 μλ Previousμ Next λ²νΌμ λλ₯΄λ©΄ μΊλ¦ν° λͺ¨λΈμ΄ μ΄μ λλ λ€μ λͺ¨λΈλ‘ λ°λκ² λκ³
λ·°λ λ°λ λͺ¨λΈμ λ°λΌ λμ°λ μ λ³΄κ° λ°λκ² λ©λλ€.)
Observable
MVVMμ ν΅μ¬μΈ μ΅μ λ²λΈμ ꡬννκ² μ΅λλ€.
λ¨Όμ Listenerλ₯Ό μ΅λͺ ν΄λ‘μ λ‘ λ§λ€μ΄λμ΅λλ€.
valueλ didSetμ μ΄μ©ν΄μ 리μ€λμ μ΄λ€ κ°μ΄ λ€μ΄μ€λ©΄ κ·Έμ λ§κ² λ³ννλλ‘ λ§λ€μ΄λμ΅λλ€.
bind λ©μλλ₯Ό ν΅ν΄μ νμ¬ Observableμ 리μ€λλ₯Ό μ μ©νκ³ λ¦¬μ€λλ κ°μ΄ λ³κ²½λλ©΄ λ°λ‘ ν΄λ‘μ Έλ₯Ό μ€νν©λλ€.
import Foundation
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)
}
}
Model
μΊλ¦ν°λ₯Ό ꡬμ±νλ λͺ¨λΈμ λλ€.
import UIKit
struct MVVM_Character {
//νλ©΄μ ꡬμ±ν λ°μ΄ν°
let image:UIImage
let name:String
let gender:String
let country:String
//μ΄κΈ°ν
init(name:String,image:UIImage,gender:String,country:String) {
self.name = name
self.image = image
self.gender = gender
self.country = country
}
}
View
λ·°λ μλμ κ°μ΄ λ·°λͺ¨λΈμ μμ νκ³ μμ΅λλ€.
λ·°λͺ¨λΈμ νλ‘νΌν°λ€μ λ·°μ νλ‘νΌν°μ λ°μΈλ©μ μμΌμ€λλ€. (μ΄κ²μ΄ λ°λ‘ λ°μ΄ν° λ°μΈλ©μ λλ€.)
μ¦, λ·°λͺ¨λΈμ κ΄μ°°νκ³ λ°λλ κ°μ λ°λΌμ λ·°λ₯Ό λ³νμμΌμ€λ€κ³ μ€μ νλ κ²μ λλ€.
μ μ μ μΈν°λμ μ λ°κ² λλ©΄ λ·°λͺ¨λΈμ νΉμ λ©μλλ₯Ό μ€νμμΌ λ·°λͺ¨λΈ κ°μ λ°κΏμ€λλ€.
νλ§λλ‘ λ·°μμ μ΄λ€ λ²νΌμ΄ λ리면 -> λ·°λͺ¨λΈμ κ°μ΄ λ³ννκ³ -> κ°μ΄ λ³ννλ©΄ λ·°κ° κ°μ λ§κ² λ³ννλ κ²μ λλ€.
μ΄λ κ² λ³΄λ©΄ λ·°μ μ½λκ° MVCμ λΉν΄ ν¨μ¬ κ°κ²°ν΄μ§ κ²μ λ³Ό μ μμ£ ?
import UIKit
class MVVM_CharacterViewController: UIViewController {
//MARK:- IBOutlets
@IBOutlet weak var country: UILabel!
@IBOutlet weak var name: UILabel!
@IBOutlet weak var gender: UILabel!
@IBOutlet weak var image: UIImageView!
@IBOutlet weak var nextButton: UIButton!
@IBOutlet weak var previousButton: UIButton!
//MARK:- Properties
private let viewModel:CharacterViewModel = CharacterViewModel()
//MARK:- Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
bind()
}
//MARK:- Functions
func bind() {
viewModel.image.bind { [weak self] image in
self?.image.image = image
}
viewModel.name.bind { [weak self] name in
self?.name.text = name
}
viewModel.gender.bind { [weak self] gender in
self?.gender.text = gender
}
viewModel.country.bind { [weak self] country in
self?.country.text = country
}
}
//MARK:- IBActions
@IBAction func tapPreviousButton(_ sender: Any) {
viewModel.tapButton(isPrevious: true)
}
@IBAction func tapNextButton(_ sender: Any) {
viewModel.tapButton(isPrevious: false)
}
}
ViewModel
μ°μ μ 체 λͺ¨λΈ λ°μ΄ν°λ μμλ‘ λ―Έλ¦¬ λ§λ€μ΄λμμ΅λλ€.
λ·°μ νμν νλ‘νΌν°λ€μ Observable νμ μΌλ‘ λ§λ€μ΄λμ΅λλ€.
λ·°μμ μΌμ΄λλ μΈν°λμ μ λ°λΌ κ°μ΄ λ³ννλλ‘ νλ λΉμ§λμ€ λ‘μ§μ μμ±ν©λλ€.
import UIKit
class CharacterViewModel {
//MARK:- Properties
let image:Observable<UIImage?> = Observable(nil)
let name:Observable<String?> = Observable(nil)
let gender:Observable<String?> = Observable(nil)
let country:Observable<String?> = Observable(nil)
var index:Int = 0
init() {
self.image.value = characters[0].image
self.name.value = characters[0].name
self.gender.value = characters[0].gender
self.country.value = characters[0].country
}
func tapButton(isPrevious:Bool) {
if isPrevious {
index = index > 0 ? index-1 : 0
}else {
index = index < characters.count - 1 ? index + 1 : characters.count - 1
}
self.image.value = characters[index].image
self.name.value = characters[index].name
self.gender.value = characters[index].gender
self.country.value = characters[index].country
}
}
μ€ννλ©΄
λλμ
μ§κΈκΉμ§ MVVM ν¨ν΄μ μ¬μ©ν΄μ μ±μ ꡬννλ©΄μ μκ°νλ λ°©μμ΄ λ³ννλ κ²μ λκΌλ€.
μλ MVCλ‘ κ΅¬ννλ€λ©΄ "λ²νΌμ λλ₯΄λ©΄ λ·°κ° μ΄λ κ² λ³ννλ©΄ λκ² λ€"λΌλ©΄
MVVMμΌλ‘λ "λ²νΌμ λλ₯΄λ©΄ λ·°λͺ¨λΈμκ² μλ €μ£Όκ³ λ·°λͺ¨λΈμ κ°λλ‘ λ·°κ° λ³νκ² λ€."λ‘ λ°λμλ€.
λμ μ°¨μ΄μ μ MVCλ λ·°λ₯Ό λ΄κ° μ§μ λΉ λ₯΄κ² λ°κΎΈλ λλμ΄κ³ MVVMμ 미리 μμ±ν΄λμ λ°©μλλ‘ λ·°κ° μμμ λ°λλ λλμ΄μλ€.
νμ§λ§ νμ€ν μ₯λ¨μ μ μμλ€.
MVVMμ μ€μ λ‘ λΉμ§λμ€ λ‘μ§μ΄ λ·°λͺ¨λΈμ μμ΄μ μ΄λ€ μ½λλ₯Ό μ°Ύκ±°λ μ½μ λ ν¨μ¬ μμνλ€.
λν 미리 μμ±ν΄λμ μ½λλ₯Ό μ¬μ¬μ©ν μλ μμ΄μ μμ°μ μΈ ν¨μ¨μ μ§μ μ μΌλ‘ λ§μ΄ λκΌλ€.
νμ§λ§ κ°λ¨ν νλ©΄μ λ§λ€ λμ‘°μ°¨ λ¨Έλ¦¬κ° λ³΅μ‘ν΄μ§κ³ μκ°λ μ€λ κ±Έλ Έλ€...
MVVM ν¨ν΄μ μ μ©νμΌλ©΄ μ무리 κ°λ¨ν νλ©΄μ΄λΌλ λλ νλλΆν° μ΄κΉμ§ MVVMμΌλ‘ ꡬνν΄μΌλΌ!
λΌκ³ μκ°νκ³ μκ°μ λ§μ΄ μΌλ κ² κ°λ€.
νμ§λ§ κ°μ₯ μ’μ건 MVCμ μ ν©ν νλ©΄μ MVCλλ‘ MVVMμ λ§λ νλ©΄ MVVMλλ‘ μ ν©ν λμμΈ ν¨ν΄μ μ¬μ©ν΄κ°λ©΄μ
μμ°μ±μ΄λ νμ μ ν¨μ¨μ μ¬λ €μ£Όλ λμμΈ ν¨ν΄μ μ μ©ν΄μ κ°λ°νλ κ²μ΄ μ’λ€κ³ λκΌλ€.
Source Code
κ° λμμΈ ν¨ν΄μ μμ€μ½λλ μλ κΉνμ μ 리ν΄λκ² μ΅λλ€!
Reference
μλ μ¬μ΄νΈλ₯Ό μ°Έκ³ νμ΅λλ€.