iOS/Swift

MVVM 패턴 (Model - View - ViewModel)

밤새는 탐험가89 2024. 5. 30. 06:42

 

  1. 사용자의 Action들은 View를 통해 들어옵니다.
  2. View에 Action이 들어오면 ViewModel에 Action을 전달합니다.
  3. ViewModel은 Model에게 데이터를 요청합니다.
  4. Model은 ViewModel에게 요청받은 데이터를 응답합니다.
  5. ViewModel은 응답 받은 데이터를 가공하여 저장합니다.
  6. View는 Data Binding을 이용해 UI를 갱신시킵니다.

 

🟥 MVVM 패턴이란?

Model, View, ViewModel 로 구성된 패턴이다. 

 

  • Model: 애플리케이션의 데이터를 나타낸다. 비즈니스 로직과 관련된 데이터를 처리한다. 
  • View: 사용자 인터페이스를 나타낸다. 사용자에게 정보를 보여주고, 사용자 입력을 받는다. 
  • ViewModel: View와 Model 사이의 중간자 역할을 한다. View를 위한 데이터를 준비하고, View에서 발생한 이벤트를 Model에 전달한다. 

 

✅ MVVM 패턴은 swiftUI에 주로 사용된다.

✅ 아래 예시는 UIKIT에서 사용할 경우이다.

1️⃣ Model

  • User라는 구조체는 사용자의 이름과 나이를 속성으로 가지고 있다. 
struct User {
    let name: String
    let age: Int
}

 

2️⃣ View

import UIKit

class UserListView: UIView {
    let tableView = UITableView()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupTableView()
        setupConstraints()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private func setupTableView() {
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "UserCell")
        addSubview(tableView)
    }
    
    private func setupConstraints() {
        tableView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            tableView.topAnchor.constraint(equalTo: topAnchor),
            tableView.bottomAnchor.constraint(equalTo: bottomAnchor),
            tableView.leadingAnchor.constraint(equalTo: leadingAnchor),
            tableView.trailingAnchor.constraint(equalTo: trailingAnchor)
        ])
    }
}

 

3️⃣ ViewController (UIViewController 및 UITableView)

  • UserListViewController는 UITableView를 사용하여 사용자 목록을 표시한다.
  • 데이터 소스 및 델리게이트 메서드를 통해 ViewModel과 상호작용한다.
import UIKit

class UserListViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
    private var viewModel = UserViewModel()
    private var userListView: UserListView {
        return view as! UserListView
    }
    
    override func loadView() {
        view = UserListView()
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        userListView.tableView.dataSource = self
        userListView.tableView.delegate = self
        
        viewModel.fetchUsers { [weak self] in
            self?.userListView.tableView.reloadData()
        }
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return viewModel.numberOfUsers
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "UserCell", for: indexPath)
        let user = viewModel.user(at: indexPath.row)
        cell.textLabel?.text = "\(user.name), Age: \(user.age)"
        return cell
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
    }
}

 

4️⃣ ViewModel

  • UserViewModel 클래스는 사용자 목록을 관리하며, 사용자 데이터를 비동기로 가져오는 메서드를 포함한다.
  • ViewModel은 View에 필요한 데이터를 제공하며, View에서 발생하는 이벤트를 처리한다.
import Foundation

class UserViewModel {
    private var users: [User] = []
    
    var numberOfUsers: Int {
        return users.count
    }
    
    func user(at index: Int) -> User {
        return users[index]
    }
    
    func fetchUsers(completion: @escaping () -> Void) {
        // Simulating network call
        DispatchQueue.global().async {
            // Simulated delay
            sleep(2)
            
            // Simulated fetched data
            self.users = [
                User(name: "Alice", age: 30),
                User(name: "Bob", age: 25),
                User(name: "Charlie", age: 35)
            ]
            
            DispatchQueue.main.async {
                completion()
            }
        }
    }
}

 

 

5️⃣ SceneDelegate (iOS 13이상)

import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let windowScene = (scene as? UIWindowScene) else { return }
        window = UIWindow(windowScene: windowScene)
        window?.rootViewController = UINavigationController(rootViewController: UserListViewController())
        window?.makeKeyAndVisible()
    }
    
    ...
}

 

 

 

🟧 MVC 패턴과 비교

 

🟧 MVC 구성요소

 

  • Model: 애플리케이션의 데이터를 나타내며, 비즈니스 로직과 데이터 처리를 담당한다.
  • View: 사용자 인터페이스를 나타내며, 사용자에게 정보를 보여주고, 사용자 입력을 받는다.
  • Controller: Model과 View를 연결하는 역할을 하며, View에서 발생한 사용자 입력을 Model에 전달하고, Model의 변경 사항을 View에 반영한다.

 

1️⃣ Model

struct User {
    let name: String
    let age: Int
}

 

 

 

2️⃣ View

import UIKit

class UserListView: UIView {
    let tableView = UITableView()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupTableView()
        setupConstraints()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private func setupTableView() {
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "UserCell")
        addSubview(tableView)
    }
    
    private func setupConstraints() {
        tableView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            tableView.topAnchor.constraint(equalTo: topAnchor),
            tableView.bottomAnchor.constraint(equalTo: bottomAnchor),
            tableView.leadingAnchor.constraint(equalTo: leadingAnchor),
            tableView.trailingAnchor.constraint(equalTo: trailingAnchor)
        ])
    }
}

 

 

3️⃣ Controller

import UIKit

class UserListViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
    private var users: [User] = []
    private var userListView: UserListView {
        return view as! UserListView
    }
    
    override func loadView() {
        view = UserListView()
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        userListView.tableView.dataSource = self
        userListView.tableView.delegate = self
        
        fetchUsers()
    }
    
    private func fetchUsers() {
        DispatchQueue.global().async {
            sleep(2)
            self.users = [
                User(name: "Alice", age: 30),
                User(name: "Bob", age: 25),
                User(name: "Charlie", age: 35)
            ]
            DispatchQueue.main.async {
                self.userListView.tableView.reloadData()
            }
        }
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return users.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "UserCell", for: indexPath)
        let user = users[indexPath.row]
        cell.textLabel?.text = "\(user.name), Age: \(user.age)"
        return cell
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
    }
}

 

 

✅ 차이점 요약

🟥 MVC 패턴

  • View와 Controller가 명확히 구분되지 않는 경우가 많다.
  • Controller는 Model을 직접 조작하고, View를 업데이트한다.
  • View는 Controller와 긴밀히 연결되어 있다.

🟥 MVVM 패턴

  • View와 ViewModel은 완전히 분리되어 있다.
  • ViewModel은 Model을 조작하고, View에 필요한 데이터를 제공한다.
  • View는 ViewModel과 바인딩하여 데이터를 표시하고 업데이트한다.
  • ViewModel은 View에 대한 참조를 가지지 않으며, 이는 테스트와 재사용성을 높인다.

 

✅ MVVM 패턴은 View와 비즈니스 로직의 명확한 분리를 제공하여 코드의 유지보수성과 테스트 용이성을 향상시킨다.

✅ MVC 패턴은 구조가 단순하지만, View와 Controller 간의 강한 결합으로 인해 복잡한 애플리케이션에서는 코드 관리가 어려울 수 있다.

'iOS > Swift' 카테고리의 다른 글

Combine (MVVM 패턴)  (0) 2024.05.30
MVVM 패턴 - Binding 개념  (0) 2024.05.30
고차함수 (Map, Filter, Reduce)  (0) 2024.05.26
MVC 패턴 (Model - View - Controller)  (0) 2024.05.25
확장 (Extension)  (0) 2024.05.24