๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
iOS

[iOS] Rx์—†์ด ๊ตฌํ˜„ํ•˜๋Š” MVVM๊ธฐ์ดˆ

by lizzydev 2023. 2. 9.

MVVM ํŒจํ„ด์„ ์‚ฌ์šฉํ•˜์—ฌ ์„œ๋ฒ„(API)์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์™€ ํ…Œ์ด๋ธ” ๋ทฐ์— ํ‘œ์‹œํ•˜๋Š” ์˜ˆ์ œ

์„œ๋ฒ„์—์„œ ์ง์› ๋ฐ์ดํ„ฐ ๋ชฉ๋ก์— ๋Œ€ํ•œ ๋ฆฌ์Šคํฐ์Šค(response)๋ฅผ ์ œ๊ณตํ•˜์—ฌ ์ด ๋ชฉ๋ก์„ ํ…Œ์ด๋ธ” ๋ทฐ์— ํ‘œ์‹œํ•œ๋‹ค.

MVVM ๊ตฌ์„ฑ ์š”์†Œ์˜ ๊ฐœ์š”์™€ ์—ญํ• 

  • ๋ทฐ ์ปจํŠธ๋กค๋Ÿฌ (View Controller): UI ๊ด€๋ จ ์ž‘์—…๋งŒ ์ˆ˜ํ–‰ํ•œ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด ์ •๋ณด ํ‘œ์‹œ ๋ฐ ๊ฐ€์ ธ์˜ค๊ธฐ ๋“ฑ์˜ ์ž‘์—…์ด ์žˆ๋‹ค. ๋ทฐ ์ปจํŠธ๋กค๋Ÿฌ๋Š” ๋ทฐ ๋ ˆ์ด์–ด์˜ ์ผ๋ถ€์ด๋‹ค.
  • ๋ทฐ๋ชจ๋ธ (View Model): ๋ทฐ ์ปจํŠธ๋กค๋Ÿฌ๋กœ๋ถ€ํ„ฐ ์ •๋ณด๋ฅผ ์ˆ˜์‹ ํ•˜๊ณ  ์ด ๋ชจ๋“  ์ •๋ณด๋ฅผ ์ฒ˜๋ฆฌํ•œ ๋’ค ๋ทฐ ์ปจํŠธ๋กค๋Ÿฌ๋กœ ๋‹ค์‹œ ๋ณด๋‚ธ๋‹ค.
  • ๋ชจ๋ธ (Model): MVC์—์„œ์™€ ๊ฐ™์€ ๋ชจ๋ธ์ด๋‹ค. ๋ทฐ๋ชจ๋ธ์ด ์‚ฌ์šฉํ•˜๊ณ  ๋ทฐ๋ชจ๋ธ์ด ์ƒˆ ์—…๋ฐ์ดํŠธ ์ •๋ณด๋ฅผ ๋ณด๋‚ผ ๋•Œ๋งˆ๋‹ค ๋ชจ๋ธ๋„ ์—…๋ฐ์ดํŠธํ•œ๋‹ค.

์˜ˆ์ œ ๋™์ž‘ ๋ฐฉ์‹

  1. ๋ทฐ ์ปจํŠธ๋กค๋Ÿฌ๊ฐ€ ํ˜ธ์ถœ๋˜๋ฉด ๋ทฐ๋Š” ๋ทฐ ๋ชจ๋ธ์— ๋Œ€ํ•œ ์ฐธ์กฐ๋ฅผ ๊ฐ–๊ฒŒ ๋œ๋‹ค.
  2. ๋ทฐ๋Š” ์‚ฌ์šฉ์ž์˜ ์•ก์…˜(user action)์„ ์ˆ˜ํ–‰ํ•˜๊ณ  ๋ทฐ๋ชจ๋ธ์„ ํ˜ธ์ถœํ•œ๋‹ค.
  3. ๋ทฐ๋ชจ๋ธ์€ APIService๋ฅผ ์š”์ฒญํ•˜๊ณ  APIService๋Š” ๋ทฐ๋ชจ๋ธ์— ์‘๋‹ต(response)์„ ๋ณด๋‚ธ๋‹ค.
  4. ๋ฆฌ์Šคํฐ์Šค(์‘๋‹ต)๋ฅผ ๋ฐ›์œผ๋ฉด ๋ทฐ๋ชจ๋ธ์€ ๋ฐ”์ธ๋”ฉ์„ ํ†ตํ•ด ๋ทฐ์— ์•Œ๋ฆฐ๋‹ค.
  5. ๋ทฐ๋Š” ๋ฐ์ดํ„ฐ๋กœ UI๋ฅผ ์—…๋ฐ์ดํŠธํ•œ๋‹ค.

Model : Employees, EmployeeData

๋ชจ๋ธ์€ ๋‹จ์ˆœ ๋ฐ์ดํ„ฐ๋ฅผ ๋‚˜ํƒ€๋‚ธ๋‹ค. ๋‹จ์ˆœํžˆ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด์œ ํ•˜๊ณ  ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๊ณผ ์•„๋ฌด ๊ด€๋ จ์ด ์—†๋‹ค. ์šฐ๋ฆฌ๊ฐ€ API์—์„œ ๊ธฐ๋Œ€ํ•˜๋Š” ๋‹จ์ˆœํ•œ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ์ด๋‹ค.

API URL์— ๋Œ€ํ•œ ๋ฆฌ์Šคํฐ์Šค๋ฅผ ํ™•์ธํ•œ ๋’ค ์ด ๋ฆฌ์Šคํฐ์Šค์— ๋Œ€์‘ํ•˜๋Š” ๋ชจ๋ธ ํด๋ž˜์Šค๋ฅผ ์ƒ์„ฑ

// MARK: - Employee
struct Employees: Decodable {
    let status: String
    let data: [EmployeeData]
}
// MARK: - EmployeeData
struct EmployeeData: Decodable {
    let id, employeeName, employeeSalary, employeeAge: String
    let profileImage: String
    enum CodingKeys: String, CodingKey {
        case id
        case employeeName = "employee_name"
        case employeeSalary = "employee_salary"
        case employeeAge = "employee_age"
        case profileImage = "profile_image"
    }
}

ViewModel : EmployeesViewModel (์ถ”๊ฐ€์ „)

๋ทฐ ๋ชจ๋ธ์€ MVVM์˜ ์ฃผ์š” ๊ตฌ์„ฑ ์š”์†Œ์ด๋‹ค.

๋ทฐ๋ชจ๋ธ์€ ๋ทฐ๊ฐ€ ๋ฌด์—‡์ธ์ง€ ๋˜๋Š” ๋ทฐ๊ฐ€ ๋ฌด์—‡์„ ํ•˜๋Š”์ง€ ๊ฒฐ์ฝ” ์•Œ์ง€ ๋ชปํ•œ๋‹ค!

์ด๋ฅผ ํ†ตํ•ด ๋”์šฑ ํ…Œ์ŠคํŠธ๊ฐ€ ๊ฐ€๋Šฅํ•˜๋„๋ก ๋งŒ๋“ค๊ณ  ๋ทฐ์—์„œ ๋ณต์žก์„ฑ์„ ์ œ๊ฑฐํ•œ๋‹ค.

๋ทฐ๋ชจ๋ธ(EmployeesViewModel)์—์„œ APIService ํด๋ž˜์Šค๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ์„œ๋ฒ„์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค.

import Foundation

class EmployeesViewModel : NSObject {
    
    private var apiService : APIService!

	/*
     ๋ฐ”์ธ๋”ฉ ์ฝ”๋“œ ์ถ”๊ฐ€ ๋  ๊ณณ

   */
    
    override init() {
        super.init()
        self.apiService =  APIService()
        callFuncToGetEmpData()
    }
    
    func callFuncToGetEmpData() {
        self.apiService.apiToGetEmployeeData { (empData) in
            self.empData = empData
        }
    }
}

API Service

APIService ํด๋ž˜์Šค๋Š” URLSession ํด๋ž˜์Šค๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ง์› ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๊ฐ„๋‹จํ•œ ํด๋ž˜์Šค์ด๋‹ค. ์—ฌ๊ธฐ์—์„œ ๋ชจ๋“  ๋„คํŠธ์›Œํ‚น ๋ชจ๋ธ์„ ์‚ฌ์šฉํ•˜์—ฌ ์„œ๋ฒ„์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋‹ค! ๋ทฐ๋ชจ๋ธ ํด๋ž˜์Šค์—์„œ ์ด APIService ํด๋ž˜์Šค๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค.

import Foundation

class APIService :  NSObject {
    
    private let sourcesURL = URL(string: "<http://dummy.restapiexample.com/api/v1/employees>")!
    
    func apiToGetEmployeeData(completion : @escaping (Employees) -> ()){
        
        URLSession.shared.dataTask(with: sourcesURL) { (data, urlResponse, error) in
            if let data = data {
                
                let jsonDecoder = JSONDecoder()
                
                let empData = try! jsonDecoder.decode(Employees.self, from: data)
            
                    completion(empData)
            }
            
        }.resume()
    }
    
}

์ด๋ ‡๊ฒŒ ๋ทฐ๋ชจ๋ธ ํด๋ž˜์Šค์—์„œ API ๋ฆฌ์Šคํฐ์Šค๋ฅผ ๋ฐ›์„ ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ๋Š”๋ฐ. ์ด์ œ ๋ทฐ ์ปจํŠธ๋กค๋Ÿฌ์™€ ๋ทฐ๋ชจ๋ธ์„ ๋ฐ”์ธ๋”ฉํ•  ์ฐจ๋ก€์ด๋‹ค.

MVVM Bindings : Model Reference์— didset ์ถ”๊ฐ€ ๋ฐ bindํ•จ์ˆ˜ ์ถ”๊ฐ€

MVVM ๋ฐ”์ธ๋”ฉ์€ MVVM์—์„œ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ์—ญํ• ์„ ํ•œ๋‹ค.

๋ทฐ๋ชจ๋ธ๊ณผ ๋ทฐ ์ปจํŠธ๋กค๋Ÿฌ ๊ฐ„์— ํ†ต์‹ ํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ์ค‘์š”ํ•œ๋ฐ ์—ฌ๋Ÿฌ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์œผ๋กœ ๋ฐ”์ธ๋”ฉ์„ ํ•  ์ˆ˜ ์žˆ๋‹ค.

import Foundation

class EmployeesViewModel : NSObject {
    
    private var apiService : APIService!
//๋ฐ”์ธ๋”ฉ์„ ์œ„ํ•ด ์ถ”๊ฐ€๋œ ์ฝ”๋“œ ์—ฌ๊ธฐ์„œ ๋ถ€ํ„ฐ 
    private(set) var empData : Employees! {
        didSet {
            self.bindEmployeeViewModelToController()
        }
    }
    
    var bindEmployeeViewModelToController : (() -> ()) = {}
// ์—ฌ๊ธฐ๊นŒ์ง€    

    override init() {
        super.init()
        self.apiService =  APIService()
        callFuncToGetEmpData()
    }
    
    func callFuncToGetEmpData() {
        self.apiService.apiToGetEmployeeData { (empData) in
            self.empData = empData
        }
    }
}

bindEmployeeViewModelToController๋ผ๋Š” ์ด๋ฆ„์œผ๋กœ ๋ทฐ๋ชจ๋ธ ํด๋ž˜์Šค์— ํ”„๋กœํผํ‹ฐ๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.

์ด ํ”„๋กœํผํ‹ฐ๋Š” ๋ทฐ ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ํด๋ž˜์Šค์—์„œ ํ˜ธ์ถœํ•ด์•ผ ํ•œ๋‹ค.

APIService์—์„œ ํ•ด๋‹น ๊ฒฐ๊ณผ๋ฅผ ๊ฒ€์ƒ‰ํ•˜๊ณ  ๋ทฐ์— ๋ณ€๊ฒฝ ์‚ฌํ•ญ์ด ์žˆ์Œ์„ ๋‹ด์„ ์ง์›(๋ชจ๋ธ) ์œ ํ˜•์˜ empData๋ผ๋Š” ์ด๋ฆ„์œผ๋กœ ๋ทฐ๋ชจ๋ธ ํด๋ž˜์Šค์— ์ƒ์„ฑํ•œ๋‹ค.

empData๋Š” API ์„œ๋น„์Šค์—์„œ ๋ฐ›์€ ์‘๋‹ต์ด ๋‹ด๊ธฐ๊ฒŒ ๋œ๋‹ค.

ํ”„๋กœํผํ‹ฐ ์˜ต์ €๋ฒ„๋ฅผ ์ด์šฉํ•˜์—ฌ API์˜ ์‘๋‹ต์œผ๋กœ empData์— ๊ฐ’์„ ๋ฐ›๋Š” ์ฆ‰์‹œ empData์˜ didSet์„ ํ˜ธ์ถœํ•˜๊ณ  empData์˜ didSet ๋‚ด๋ถ€์—์„œ bindEmployeeViewModelToController()๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค.

๋ทฐ๋ชจ๋ธ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์‹ ํ•˜๋ฉด ์ด์ œ UI๋ฅผ ์—…๋ฐ์ดํŠธํ•  ์ฐจ๋ก€์ด๋‹ค!

View: ViewController

๋ทฐ๋ชจ๋ธ ํด๋ž˜์Šค์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์‹ ํ•˜๋ ค๋ฉด ๋ทฐ ์ปจํŠธ๋กค๋Ÿฌ ํด๋ž˜์Šค ๋‚ด๋ถ€์— ๋ทฐ๋ชจ๋ธ์„ ๊ฐ€์ง€๊ณ  ์žˆ์–ด์•ผ ํ•œ๋‹ค.

import UIKit

class ViewController: UIViewController {
    
    
    @IBOutlet weak var employeeTableView: UITableView!
    
    private var employeeViewModel : EmployeesViewModel!
    
    private var dataSource : EmployeeTableViewDataSource<EmployeeTableViewCell,EmployeeData>!
    

    override func viewDidLoad() {
        super.viewDidLoad()
        callToViewModelForUIUpdate()
    }
    
    func callToViewModelForUIUpdate(){
        
        self.employeeViewModel =  EmployeesViewModel()
        self.employeeViewModel.bindEmployeeViewModelToController = {
            self.updateDataSource()
        }
    }
    
    func updateDataSource(){
        
        self.dataSource = EmployeeTableViewDataSource(cellIdentifier: "EmployeeTableViewCell", items: self.employeeViewModel.empData.data, configureCell: { (cell, evm) in
            cell.employeeIdLabel.text = evm.id
            cell.employeeNameLabel.text = evm.employeeName
        })
        
        DispatchQueue.main.async {
            self.employeeTableView.dataSource = self.dataSource
            self.employeeTableView.reloadData()
        }
    }
    
}

DataSource : EmployeeTableViewDataSource

UI๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๊ธฐ ์œ„ํ•ด ๋ทฐ ์ปจํŠธ๋กค๋Ÿฌ์— ํ…Œ์ด๋ธ” ๋ทฐ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜๋„ ์žˆ์ง€๋งŒ ๋ทฐ ์ปจํŠธ๋กค๋Ÿฌ๊ฐ€ ๋” ๊น”๋”ํ•˜๊ฒŒ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด UITableViewDataSource  ๋ฅผ ๊ตฌํ˜„ํ•œ EmployeeTableViewDataSource ๊ฐ€ ๋งŒ๋“ค์–ด์ ธ ์žˆ๋‹ค.

import Foundation
import UIKit

class EmployeeTableViewDataSource<CELL : UITableViewCell,T> : NSObject, UITableViewDataSource {
    
    private var cellIdentifier : String!
    private var items : [T]!
    var configureCell : (CELL, T) -> () = {_,_ in }
    
    
    init(cellIdentifier : String, items : [T], configureCell : @escaping (CELL, T) -> ()) {
        self.cellIdentifier = cellIdentifier
        self.items =  items
        self.configureCell = configureCell
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        items.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
         let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as! CELL
        
        let item = self.items[indexPath.row]
        self.configureCell(cell, item)
        return cell
    }
}

EmployeeTableViewCell

import UIKit

class EmployeeTableViewCell: UITableViewCell {
    
    
    @IBOutlet weak var employeeIdLabel: UILabel!
    @IBOutlet weak var employeeNameLabel: UILabel!
    
    var employee : EmployeeData? {
        didSet {
            employeeIdLabel.text = employee?.id
            employeeNameLabel.text = employee?.employeeName
        }
    }
    
    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
    }

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)

        // Configure the view for the selected state
    }

}

Reference

MVVM in iOS Swift