✨ MVVM 패턴 탄생 배경
- MVC가 간단한 프로젝트에는 용이하지만, 프로젝트가 거대해지고 개발자간의 협업이 늘어날수록 Controller의 규모도 거대해진다는 단점이 존재했다.
- Controller의 역할 : 레이아웃 코드들, 유저 입력 프로세싱, 비지니스 로직, 데이터 변환, 화면 전환, 생명주기, 콜백처리(델리게이트 등), 네트워크 통신 등.
- Controller의 역할을 덜어주기 위해 새로운 디자인 패턴 MVP (Model View Presenter)를 고안해냈다.
- ViewController를 View로 취급, Model은 Model, 중개자 역할로 Presenter 추가.
- 하지만 Presenter와 View가 서로를 소유하여 높은 의존성을 띄고 있었고 View가 하는 일이 거의 없었다. -> 개선 여지 O
- Presenter가 weak로 View를 소유하고, View에 있는 요소에 직접 접근하여 업데이트한다.
- VC는 꼭 VC에서 처리해야하는 것들 ( 생명주기, 화면 전환, 콜백처리 등 )만 처리하고, 나머지는 모두 Presenter로 위임하였다.
- Presenter가 weak로 View를 소유하고, View에 있는 요소에 직접 접근하여 업데이트한다.
- 이렇게 MVVM 디자인 패턴이 탄생했다. (Model, View, ViewModel)
- View를 업데이트하는 코드들은 View에 두고, 이 코드들을 트리거하기 위해 Data Binding으로 ViewModel과 연결해준다.
- 그 결과, 뷰 로직과 비즈니스 로직이 분리되었다.
- Binding으로 VM과 V가 데이터를 공유하고, V에서는 뷰 로직을, VM에서는 비즈니스 로직을 담당한다.
- View를 업데이트하는 코드들은 View에 두고, 이 코드들을 트리거하기 위해 Data Binding으로 ViewModel과 연결해준다.
✨ MVVM 요소
- Model
- 데이터, 네트워크 로직, 비즈니스 로직, 데이터 캡슐화
- View, ViewModel과 완전한 분리 -> 다른 거 신경쓰지 않고, 데이터가 어떻게 보여질지만 신경쓴다.
- MVC의 Model과 비슷
- View
- VM으로부터 데이터 가져와서 View를 직접 그린다. (MVC와의 차이점)
- 비즈니스 로직과 UI 로직 구분할 수 있다. (View에서 UI로직을 담당하기 때문에)
- 유저 action 수신해서 이에 대한 처리는 VM에 부탁한다.
- ViewModel 참조한다 (소유한다)
- ViewModel
- View가 요청한 사항 처리하는 로직을 담고 있다.
- Model에 변화가 생기면 View에 notification 보낸다.
- View ~ Model 사이의 중재자 역할이라고 보면 된다.
- View없이 테스트 가능하다.
- ViewModel은 변경사항을 직접 View에게 알리지 않고 발표한다. (publish)
- 이를 보고 있는 View가 알아차리고 View를 다시 그리는 등의 행동을 수행한다.
✨ MVVM 동작 흐름
View에 들어온 Event를 VM에 알림 -> VM이 처리 (Model에 알림) -> Model 변화 -> VM이 알아차림 -> 바인딩 되어있는 View 업데이트
✨ MVC vs MVVM
❗️행동 결정권자 차이 (어떤 행동을 할지)
- MVC
- 유저 상호작용이 발생했음을 View -> Controller에게 알리고 어떤 행동을 할지 Controller가 다 정한다. (Controller가 View를 그리는 과정이 포함된다.)
- MVVM
- VM은 로직만 가지고 있고, 어떤 행동을 할지는 View가 정한다. (VM을 View가 소유하면서 행동 결정한다.)
❗️데이터 변경 상황 차이
- MVC
- Model -> Controller 에게 알린다.
- 상황에 따라 View를 어떻게 그릴지 Controller가 판단한다.
- MVVM
- Model -> VM 에게 알린다. (VM이 Model의 변경 사항 알아차린다.)
- 그러면 VM -> View 에게 알리고 (View가 VM의 변경 사항 알아차린다.)
- 상황에 따라 View를 어떻게 그릴지 View가 판단한다.
❗️기준 관점 차이
- UIKit - MVC : event driven
- 이벤트에 따라 특정 로직 실행하고, 이에 따라 View 변경한다.
- SwiftUI - MVVM : data driven
- 데이터 변경에 따라 특정 로직 실행하고, 이에 따라 View 변경한다.
✨ MVVM 예시 코드
❗️View & ViewModel
- LoginView (LoginViewController + LoginViewModel)
// LoginViewController.swift
// View of MVVM
import UIKit
class LoginViewController: UIViewController {
@IBOutlet weak var emailField: UITextField!
@IBOutlet weak var passwordField: UITextField!
private let viewModel = LoginViewModel()
override func viewDidLoad() {
super.viewDidLoad()
setupBinders()
}
// 뉴스 채널 구독한다는 함수
// bind 라는 게 View와 VM을 연결해주는 건데, 여기서 뭘 연결해주는데? 라고 이해함 나는.
// 근데 이 함수를 보면, error를 가져오고, 그에 대한 로직까지 같이 담는중.
// 아하, VM에 있는 error를 가져오는 구나
// 가져오는 거 + 이에 대한 로직까지. (View 그리는)
private func setupBinders() {
viewModel.error.bind { [weak self] error in
if let error = error {
print(error)
} else {
self?.goToHome()
}
}
}
private func goToHome() {
let vc = storyboard?.instantiateViewController(withIdentifier: "HomeViewController") as! HomeViewController
present(vc, animated: true, completion: nil)
}
@IBAction func loginBtnClicked(_ sender: UIButton) {
guard let email = emailField.text, let password = passwordField.text else {
print("please enter email and password")
return
}
viewModel.login(email: email, password: password)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.view.endEditing(true)
}
}
// LoginViewModel.swift
// ViewModel of MVVM (LoginViewController 라는 뷰의 뷰모델)
import Foundation
final class LoginViewModel {
// error라는 것을 ObservableObject 타입으로 만드는 이유
// MVC와 달리, 로그인 실패 시 하는 행동을, 여기서 바로 View를 수정할 수 없다.
// 그렇기 떄문에 error라는 것을 만들어서 bind할 수 있는 ObservableObject 타입으로 지정해주고 View가 이걸 보게끔 한다.
var error: ObservableObject<String?> = ObservableObject(nil)
func login(email: String, password: String) {
NetworkService.shared.login(email: email, password: password) { [weak self] success in
self?.error.value = success ? nil : "Invalid Credentails"
}
}
}
- HomeView (HomeViewController + HomeViewModel)
// HomeViewController
import UIKit
// 목표 : 로그인한 유저 정보 화면에 띄우기
// 1. 유저 정보 가져오기 (VM -> V)
class HomeViewController: UIViewController {
@IBOutlet weak var welcomeLbl: UILabel!
private var viewModel = HomeViewModel()
override func viewDidLoad() {
super.viewDidLoad()
viewModel.getUser()
setupBinders()
}
// private func getUser() {
// if let user = NetworkService.shared.user {
// welcomeLbl.text = "Welcome, \(user.firstName) \(user.lastName)"
// } else { return }
// }
// private func loadUser() {
// viewModel.getUser()
// welcomeLbl.text = viewModel.welcomeMessage.value!
// }
private func setupBinders() {
viewModel.welcomeMessage.bind { [weak self] message in
self?.welcomeLbl.text = message
}
}
}
// HomeViewModel.swift
// ViewModel of MVVM (HomeViewController 라는 뷰의 뷰모델)
import Foundation
final class HomeViewModel {
var welcomeMessage: ObservableObject<String?> = ObservableObject(nil)
func getUser() {
if let user = NetworkService.shared.user {
welcomeMessage = ObservableObject("Welcome, \(user.firstName) \(user.lastName)")
} else { return }
}
}
- ViewModel 보조 수단들 : NetworkService & ObservableObject
// ObservableObject.swift
// View - ViewModel 데이터 바인딩을 가능케하는 클래스
import Foundation
class ObservableObject<T> {
// 뉴스
var value: T {
didSet {
listener?(value)
}
}
private var listener: ((T) -> Void)?
init(_ value: T) {
self.value = value
}
// 다른 사람들에게 뉴스를 알리는 함수
func bind(_ listener: @escaping (T) -> Void) {
// listener에게 뉴스를 알린다 (listener들이 기다릴 필요 없이 변화 바로 알아차린다)
listener(value)
self.listener = listener
}
}
// NetworkService.swift
import Foundation
final class NetworkService {
static let shared = NetworkService()
var user: User?
private init() {}
func login(email: String, password: String, completion: @escaping (Bool) -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
[weak self] in
if email == "test@test.com" && password == "password" {
self?.user = User(firstName: "K", lastName: "Park", email: "test@test.com", age: 27)
completion(true)
} else {
self?.user = nil
completion(false )
}
}
}
func goToHome() {
}
}
❗️Model
// User.swift
// Model of MVVM
import Foundation
struct User {
let firstName, lastName, email : String
let age: Int
}
✨ MVVM 예시 코드 결과
출처
'iOS' 카테고리의 다른 글
[iOS] 의존성 주입(Dependency Injection, DI)이 객체의 결합도를 낮춰주는 이유 (Feat. Car) (0) | 2023.03.08 |
---|---|
[iOS] MVC 패턴이란? (0) | 2022.10.09 |
iOS App States (0) | 2022.04.29 |