๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
๐ŸŽ iOS

[Swift] MVVM ์‘์šฉํŽธ

by ํ‹ด๋”” 2022. 6. 7.
728x90
๋ฐ˜์‘ํ˜•

Matt Hardy  ๋‹˜์˜ ์‚ฌ์ง„, ์ถœ์ฒ˜:  Pexels

RxSwift + MVVM์„ ์‚ฌ์šฉํ•˜๋‹ค๊ฐ€ RxSwift๊ฐ€ ์—†๋Š” MVVM์„ ์‚ฌ์šฉํ•˜๋‹ˆ RxSwift ์˜ ์žฅ์ ์ด ํ™•์‹คํžˆ ๋Š๊ปด์ง€๋Š”๊ฑฐ ๊ฐ™์•„์š” 

์ €๋ฒˆ ํฌ์ŠคํŒ…์—์„œ MVVM์— ๋Œ€ํ•ด ๊ฐ„๋‹จํ•˜๊ฒŒ ์ •๋ฆฌํ•ด ๋‘์—ˆ๋Š”๋ฐ ๐Ÿ‘‡

2022.06.04 - [๐ŸŽ iOS/Architecture Pattern] - [Swift/Architecture] MVVM ์ดํ•ดํ•˜๊ธฐ

MVVM์€ Model - View - ViewModel๋กœ ๋‚˜๋ˆ„๋Š” ๊ฒƒ์ด ๋‹ค๊ฐ€ ์•„๋‹Œ View์™€ ViewModel์˜ ์ƒํ˜ธ ์ž‘์šฉ์— ๋Œ€ํ•ด์„œ ๋” ๊นŠ๊ฒŒ ๋‹ค๋ค„ ๋ณผ ํ•„์š”๊ฐ€ ์žˆ์–ด์„œ ์ •๋ฆฌํ•ด ๋ณด์•˜์Šต๋‹ˆ๋‹ค ๐Ÿฆ‘


MVC์˜ ๋ฌธ์ œ์ 

๊ธฐ์กด MVC์˜ ๊ฒฝ์šฐ Massive ViewController์˜ ์•ฝ์ž์ด๋‹ค~ ๋ผ๋Š” ๋ง์ด ์žˆ์„ ์ •๋„๋กœ ViewController์˜ ์—ญํ™œ์ด ์ปธ์Šต๋‹ˆ๋‹ค

UI๋ฅผ ๋กœ๋“œํ•˜๊ณ  ์‚ฌ์šฉ์ž์˜ ์ด๋ฒคํŠธ๋ฅผ ๋ฐ›๊ณ , ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•˜๊ณ ... ๊ทธ๋Ÿฌ๋‹ค๊ฐ€ ์ด๋ ‡๊ฒŒ ๋ฐฉ๋Œ€ํ•œ ViewController ์—ญํ™œ์˜ ์ผ๋ถ€๋ฅผ ๋‹ด๋‹นํ•˜๋Š” ViewModel์ด ์žˆ๋Š” MVVM ํŒจํ„ด์ด ๋“ฑ์žฅํ•˜๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค

 

MVVM์ด๋ž€

View์—์„œ ์‚ฌ์šฉ์ž์™€ ์ƒํ˜ธ์ž‘์šฉํ•˜์—ฌ ์ด๋ฒคํŠธ๋ฅผ ๋ฐ›์œผ๋ฉด ์ด ์‚ฌ์‹ค์„ ViewModel์— ์•Œ๋ฆฌ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ViewModel์€ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•˜๊ฒŒ ๋˜๋Š”๋ฐ ์—ฌ๊ธฐ์—๋Š” ๋„คํŠธ์›Œํ‚น๋„ ํฌํ•จ๋ฉ๋‹ˆ๋‹ค

View๋Š” UI์™€ ์œ ์ €๊ฐ„์˜ ์ƒํ˜ธ์ž‘์šฉ์„ ์œ„ํ•œ ์ฝ”๋“œ๊ฐ€ ํฌํ•จ๋˜๊ณ  ViewModel์€ ์ด ์ด๋ฒคํŠธ์— ๋งž๋Š” ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์ž‘์„ฑํ•˜๊ฒŒ ๋˜๋Š” ๊ฒƒ์ด์ฃ !

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ViewModel์ด ์ด์ „์— ViewController์—์„œ ์ž‘์„ฑํ–ˆ๋˜ ์ฝ”๋“œ๋ฅผ ์ผ๋ถ€ ๊ฐ€์ ธ๊ฐ€๋ฉด์„œ UI์™€ ๋…๋ฆฝ์ ์ด๊ธฐ ๋•Œ๋ฌธ์— ํ…Œ์ŠคํŠธ๋„ ๋” ์‰ฌ์›Œ ์ง‘๋‹ˆ๋‹ค

ViewModel์€ Model์—๊ฒŒ ์—…๋ฐ์ดํŠธ๋ฅผ ์š”์ฒญํ•˜๊ณ  ๋ฐ์ดํ„ฐ ๋ชจ๋ธ์€ ๋ณ€๊ฒฝ๋˜๋ฉด ์ด ์‚ฌ์‹ค์„ ViewModel์—๊ฒŒ ์•Œ๋ฆฝ๋‹ˆ๋‹ค (์ด๊ฑด ์ฝ”๋“œ๋กœ ๋ณด๋Š”๊ฒŒ ๋” ์ข‹์Šต๋‹ˆ๋‹ด)

 

MVVM ์—์„œ ์ƒ๊ฐํ•ด ๋ณผ ๊ฒƒ ๐Ÿค”

View ๊ฐ€ ViewModel์„ own ํ•˜๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์— View์—์„œ ViewModel์— ์ ‘๊ทผ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค

๊ทธ๋Ÿฌ๋‚˜! ViewModel์€ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•˜๊ณ  UI๋ฅผ ์—…๋ฐ์ดํŠธ ํ•˜๊ธฐ ์œ„ํ•ด์„œ ์–ด๋–ป๊ฒŒ View์—๊ฒŒ ์•Œ๋ ค์ฃผ์–ด์•ผ ํ• ๊นŒ์š”?

ViewModel๋„ View๋ฅผ own ํ•˜๊ฒŒ ๋˜๋ฉด ์ˆœํ™˜ ์ฐธ์กฐ๊ฐ€ ๊ฑธ๋ฆดํ…๋ฐ ๋ง์ด์—์š”

์ด๋•Œ ์‚ฌ์šฉํ•ด ์ค„ ์ˆ˜ ์žˆ๋Š”๊ฒŒ ํด๋กœ์ €, notification, delegate๋ฅผ ์‚ฌ์šฉํ•ด ์ค„ ์ˆ˜๋„ ์žˆ๋Š”๋ฐ Observable ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค์–ด์„œ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ์žˆ์Šต๋‹ˆ๋‹ค (Combine์ด๋‚˜ RxSwift๋ฅผ ํ•ด๋ณด์…จ๋‹ค๋ฉด ์ต์ˆ™ํ•˜์‹ค ๊ฑฐ์—์š”!)


ํŒŒ์ผ ๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ ๋ฏธ๋ฆฌ๋ณด๊ธฐ

๋ชจ๋“  ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๊ณ  ๋‚˜๋ฉด ์ด๋Ÿฐ ๊ตฌ์กฐ๋ฅผ ๊ฐ€์ง€๊ฒŒ ๋ฉ๋‹ˆ๋‹ค! ์šฐ์„  Service ๋ถ€ํ„ฐ ์ง„ํ–‰ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค!

์ „์ฒด ์ฝ”๋“œ๋Š” ์ด ๋งํฌ๋ฅผ ๋ˆ„๋ฅด์‹œ๋ฉด ๋ฉ๋‹ˆ๋‹ค ๐Ÿ‘‰๐Ÿป  ๊นƒํ—ˆ๋ธŒ ๋งํฌ

 

API์™€ ๋„คํŠธ์›Œํ‚น

์šฐ์„  ์‚ฌ์šฉํ•  api๋ฅผ ์†Œ๊ฐœํ•˜๊ฒ ์ˆฉ๋‹ˆ๋‹ค

https://jsonplaceholder.typicode.com ๋ฅผ ์‚ฌ์šฉํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค๐Ÿ™Œ๐Ÿป

๋„คํŠธ์›Œํ‚น -> ํ†ต์‹  ์„ฑ๊ณต -> json decoding ์ด๋ ‡๊ฒŒ ํ•ด์ค„ ๊ฒ๋‹ˆ๋‹ค! 

/users/2๋ฅผ ํ•˜๋ฉด user id 2์˜ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ json์œผ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค 

๊ตฌ์กฐ๋ฅผ ๋ฏธ๋ฆฌ๋ณด๋ฉด ์ด๋ ‡์Šต๋‹ˆ๋‹ค! ๋งŽ์€ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ด ์ฃผ์ง€๋งŒ MVVM์ด ์ฃผ์ธ๊ณต์ด๊ธฐ ๋•Œ๋ฌธ์— UI์™€ ์‚ฌ์šฉํ•  ๋ฐ์ดํ„ฐ๋Š” ๊ฐ„๋‹จํ•˜๊ฒŒ ์ž‘์„ฑํ•ด ์ฃผ๊ฒ ์Šต๋‹ˆ๋‹ค

์ด๋ฒˆ ํฌ์ŠคํŒ…์—์„œ๋Š” 'username', 'email'๋งŒ ์‚ฌ์šฉํ•ด ์ค๋‹ˆ๋‹ค!

 

import Foundation

class NetworkManager {
    static let shared = NetworkManager()
    private init() { }
    
    func getUser(completion: @escaping (Bool, User?) -> ()) {
        guard let url = URL(string: "https://jsonplaceholder.typicode.com/users/2") else {
            return
        }
        
        URLSession.shared.dataTask(with: url) { data, response, error in
            guard error == nil else {
                completion(false, nil)
                print("error ::: \(error!)")
                return
            }
            
            guard let response = response as? HTTPURLResponse, (200 ..< 300) ~= response.statusCode else {
                completion(false, nil)
                return
            }
            
            do {
                let model = try JSONDecoder().decode(User.self, from: data!)
                completion(true, model)
            } catch {
                completion(false, nil)
            }
        }.resume()
    }
}

์ด๋ฒˆ ํฌ์ŠคํŒ…์—์„œ๋Š” ํ•˜๋‚˜์˜ api ๋งŒ ์‚ฌ์šฉํ•ด ์ฃผ๊ธฐ ๋•Œ๋ฌธ์— ๋ฒ”์šฉ์ ์œผ๋กœ ์ž‘์„ฑํ•˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค

NetworkManager๋Š” Singleton ์œผ๋กœ ๋„คํŠธ์›Œํฌ ํ†ต์‹  ํ›„ ์„ฑ๊ณตํ•˜๋ฉด json์„ User ๊ฐ์ฒด๋กœ ๋””์ฝ”๋”ฉ ํ•ฉ๋‹ˆ๋‹ค. ์ด์ œ User์„ ์ž‘์„ฑํ•ด ์ฃผ๊ฒ ์Šต๋‹ˆ๋‹ค

 

Model ์ž‘์„ฑํ•˜๊ธฐ

์œ„ ์ •๋ณด์—์„œ 'username'๊ณผ 'email'๋งŒ ์‚ฌ์šฉํ•  ๊ฒƒ์ด๊ธฐ ๋•Œ๋ฌธ์— ๊ฐ„๋‹จํ•˜๊ฒŒ Model์„ ์ž‘์„ฑํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค

import Foundation

struct User: Codable {
    enum CodingKeys: String, CodingKey {
    	case userName = "username"
        case email
    }
    
    let userName: String
    let email: String
}

์—ฌ๊ธฐ์„œ ์ „์ฒด ๋ชจ๋“  ๋ฐ์ดํ„ฐ์— ๋Œ€ํ•ด ๋ชจ๋ธ ํด๋ž˜์Šค๋ฅผ ์ž‘์„ฑํ•  ํ•„์š”๋Š” ์—†์ง€๋งŒ

ํ˜น์‹œ ์ „์ฒด ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ์— ๊ด€ํ•ด Model์„ ์ž‘์„ฑํ•˜๊ณ  ์‹ถ์œผ์‹  ๊ฒฝ์šฐ ์•„๋ž˜ ์ฝ”๋“œ๋ฅผ ์ฐธ๊ณ ํ•ด ์ฃผ์„ธ์š”!

// โญ๏ธ api ๋ชจ๋“  ์ •๋ณด์— ๋Œ€ํ•œ ๋ชจ๋ธ ํด๋ž˜์Šค

import Foundation

struct User: Codable {
    enum CodingKeys: String, CodingKey {
        case id, name, email, address, phone, website, company
        case userName = "username"
    }
    
    let id: Int
    let name: String
    let userName: String
    let email: String
    let address: Address
    let phone: String
    let website: String
    let company: Company
}

struct Address: Codable {
    enum CodingKeys: String, CodingKey {
        case street, suite, city, geo
        case zipCode = "zipcode"
    }
    
    let street: String
    let suite: String
    let city: String
    let zipCode: String
    let geo: Geo
    
}

struct Geo: Codable {
    enum CodingKeys: String, CodingKey {
        case lat, lng
    }
    
    let lat: String
    let lng: String
}

struct Company: Codable {
    enum CodingKeys: String, CodingKey {
        case name, catchPhrase, bs
    }

    let name: String
    let catchPhrase: String
    let bs: String
}

๋„คํŠธ์›Œํ‚น์— ์„ฑ๊ณตํ•˜๋ฉด json api๋Š” ๋ชจ๋ธ ํด๋ž˜์Šค์ธ User๋กœ ๋””์ฝ”๋”ฉ ๋ฉ๋‹ˆ๋‹ค. ์ด Model์ด ์—…๋ฐ์ดํŠธ ๋˜์–ด ViewModel์—๊ฒŒ ์ „๋‹ฌ ๋˜๋Š” ๊ฒƒ!

์ด์ œ View๋ฅผ ์ž‘์„ฑํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค

 

View ์ž‘์„ฑํ•˜๊ธฐ

import UIKit

class UserViewController: UIViewController {
    
    let nameLabel = UILabel()
    let emailLabel = UILabel()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        nameLabel.backgroundColor = .gray
        emailLabel.backgroundColor = .yellow
        
        nameLabel.text = "์ด๋ฆ„์ž…๋‹ˆ๋‹ค"
        emailLabel.text = "์ด๋ฉ”์ผ์ž…๋‹ˆ๋‹ค"

        attachUI()
    }

    func attachUI() {
        view.addSubview(nameLabel)
        view.addSubview(emailLabel)
        
        nameLabel.translatesAutoresizingMaskIntoConstraints = false
        nameLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20).isActive = true
        nameLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20).isActive = true
        nameLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
        
        emailLabel.translatesAutoresizingMaskIntoConstraints = false
        emailLabel.leadingAnchor.constraint(equalTo: nameLabel.leadingAnchor).isActive = true
        emailLabel.trailingAnchor.constraint(equalTo: nameLabel.trailingAnchor).isActive = true
        emailLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor).isActive = true
    }
}

์œ„ ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•˜๋ฉด ์ด๋ ‡๊ฒŒ ํ—ˆ์ ‘ํ•œ UI๊ฐ€ ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค ๐Ÿฅฒ

๊ฐ„๋‹จํ•˜๊ฒŒ ์ฝ”๋“œ๋กœ ์ž‘์„ฑํ•ด ๋‘์—ˆ์œผ๋‹ˆ ๋ณต์‚ฌํ•ด์„œ ๋ถ™์—ฌ๋„ฃ์œผ์‹œ๋ฉด ๋ฉ๋‹ˆ๋‹ค

UserViewController.swift ์—์„œ๋Š” UI์— ๋Œ€ํ•œ ์ฝ”๋“œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋•Œ ๋„คํŠธ์›Œํ‚น ํ•ด์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ง€๊ณ  ์˜ค๊ณ  ์ด ๋ฐ์ดํ„ฐ๋ฅผ UI์— ๋ณด์—ฌ์ฃผ๋ ค๋ฉด ์ด์ „์— ์ž‘์„ฑํ•ด ๋‘์—ˆ๋˜ NetworkManager ์„ ํ˜ธ์ถœํ•ด์„œ ๋„คํŠธ์›Œํ‚น์„ ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค

์ด ๋„คํŠธ์›Œํ‚น์€ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์œผ๋กœ MVVM์—์„œ๋Š” UI๋ฅผ ๋‹ด๋‹นํ•˜๊ณ  ์œ ์ €์™€์˜ ์ธํ„ฐ๋ ‰์…˜์„ ๋‹ด๋‹นํ•˜๋Š” View์˜ ์—ญํ™œ์ด ์•„๋‹Œ ViewModel์˜ ์—ญํ™œ์ž…๋‹ˆ๋‹ค

์ด์ œ ViewModel์„ ์ž‘์„ฑํ•ด ์ฃผ๊ฒ ์Šต๋‹ˆ๋‹ค

 

ViewModel ์ž‘์„ฑํ•˜๊ธฐ

import Foundation

final class UserViewModel {
    
    func fetchData() {
        NetworkManager.shared.getUser { [weak self] isSuccess, data in
            if isSuccess {
            
            }
        }
    }
}

UserViewModel.swift ํŒŒ์ผ์— ์œ„ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค

ViewModel์€ ๋„คํŠธ์›Œํ‚น์„ ์‹œ์ž‘ํ•˜๋Š” ํ•จ์ˆ˜์ธ fetchData()๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค

private let viewModel = UserViewModel()

UserViewController.swift์— viewModel ํ”„๋กœํผํ‹ฐ๋ฅผ ์ถ”๊ฐ€ํ•ด์ค๋‹ˆ๋‹ค. ์ด์ œ UserViewController์˜ viewDidLoad์—์„œ attachUI() ์•„๋ž˜์— ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค

viewModel.fetchData()

attachUI() ํ›„ viewModel์— ์žˆ๋Š” fetchData()ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ด ๋„คํŠธ์›Œํ‚น์„ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค

ViewController๋Š” ViewModel์— ์ ‘๊ทผ์ด ๊ฐ€๋Šฅํ•˜๊ธฐ ๋•Œ๋ฌธ์— ViewModel์˜ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์‹คํ–‰ํ–ˆ์ง€๋งŒ ViewModel์€ ์–ด๋–ป๊ฒŒ ViewController์—๊ฒŒ ๋„คํŠธ์›Œํ‚น์„ ์„ฑ๊ณต์ ์œผ๋กœ ๋๋‚ด๊ณ  ์—…๋ฐ์ดํŠธ ํ•œ Model์„ ์ „๋‹ฌ ํ•  ์ˆ˜ ์žˆ์„๊นŒ์š”?

 

์—ฌ๊ธฐ์— ํด๋กœ์ €, ๋ธ๋ฆฌ๊ฒŒ์ดํŠธ, ๋…ธํ‹ฐํ”ผ์ผ€์ด์…˜์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์ง€๋งŒ ObservableObject๋ฅผ ์‚ฌ์šฉํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค

 

ObservableObject ์ž‘์„ฑํ•˜๊ธฐ

RxSwift์— Observable์ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฆ„์—์„œ ์œ ์ถ” ๊ฐ€๋Šฅํ•˜ ๋“ฏ์ด ๋ณ€ํ™” ๊ด€์ธก ๊ฐ€๋Šฅํ•œ ๊ฐ์ฒด ์ž…๋‹ˆ๋‹ค

import Foundation

class ObservableObject<T> {
	//๐Ÿ‘‰๐Ÿป 1
    var value: T {
        didSet {
            listener?(value)
        }
    }
    
    //๐Ÿ‘‰๐Ÿป 2
    private var listener: ((T) -> Void)?
    
    //๐Ÿ‘‰๐Ÿป 3
    init(_ value: T) {
        self.value = value
    }
    
    //๐Ÿ‘‰๐Ÿป 4
    func bind(_ listener: @escaping (T) -> Void ) {
        listener(value)
        self.listener = listener
    }
}

์ฒœ์ฒœํžˆ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค!

์šฐ์„  1๋ฒˆ์ด ์•„๋‹Œ 2๋ฒˆ ๋ถ€ํ„ฐ ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค

private var listener: ((T) -> Void)?

listener๋Š” ํด๋กœ์ €์ž…๋‹ˆ๋‹ค. T ๊ฐ์ฒด๋ฅผ ๊ฐ€์ง€๊ณ  ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค! 

    var value: T {
        didSet {
            listener?(value)
        }
    }

value๋Š” ์ œ๋„ค๋ฆญ์œผ๋กœ ๋‹ค์–‘ํ•œ ํƒ€์ž…์˜ ๊ฐ์ฒด๋ฅผ ๋ฐ›์Šต๋‹ˆ๋‹ค. ์ด๋•Œ value์˜ ๊ฐ’์ด ๋ณ€๊ฒฝ์ด ๋˜๋ฉด didSet ๋‚ด๋ถ€์— ์žˆ๋Š” ์ฝ”๋“œ๊ฐ€ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค

ํด๋กœ์ €์ธ listener์—๊ฒŒ ๋ณ€๊ฒฝ๋œ value ๊ฐ’์„ ์ „๋‹ฌํ•˜๊ณ  listener๋Š” ์ด value๋ฅผ ๊ฐ€์ง€๊ณ  ๋‚ด๋ถ€์˜ ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•˜๊ฒŒ ๋˜๋Š” ๊ฑฐ์ฃ 

์ฆ‰ ์—ฌ๊ธฐ์„œ value๊ฐ€ Observable ์ฆ‰, ๊ด€์ฐฐ ๊ฐ€๋Šฅํ•œ ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค! Model์ด ์—…๋ฐ์ดํŠธ ๋˜๋ฉด -> UI๋ฅผ ์—…๋ฐ์ดํŠธํ•œ๋‹ค

์—ฌ๊ธฐ์„œ T๊ฐ€ Model์ด๊ณ  UI ์—…๋ฐ์ดํŠธ ์ฝ”๋“œ๋Š” listener์— ํฌํ•จ๋˜์–ด ์žˆ๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค

๊ทธ๋ ‡๋‹ค๋ฉด value๋Š” viewModel์—์„œ ๊ทธ ๊ฐ’์„ ์—…๋ฐ์ดํŠธ ํ•ด์ฃผ๊ณ  

UI ์—…๋ฐ์ดํŠธ๋ฅผ ์‹œ๋„ํ•˜๋Š” listener๋Š” View์—์„œ ์ ‘๊ทผํ•˜๋ฉด => Model(์—ฌ๊ธฐ์„œ๋Š” value) ๋ณ€๊ฒฝ ๋˜๋ฉด์„œ UI ์—…๋ฐ์ดํŠธ ์‹คํ–‰

์ด๋ ‡๊ฒŒ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค ๐Ÿ™Œ๐Ÿป

listener๋Š” View์—์„œ ์ ‘๊ทผํ•ด์„œ ์„ค์ •ํ•ด ์ฃผ๋ฉด listener๊ฐ€ view ์—…๋ฐ์ดํŠธ ์ฝ”๋“œ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๊ฒ ์ฃ ?

View์—์„œ listener๋ฅผ ๋ฐ›์„ ์ˆ˜ ์žˆ๋„๋ก

    func bind(_ listener: @escaping (T) -> Void ) {
        listener(value)
        self.listener = listener
    }

4๋ฒˆ ์ฝ”๋“œ ๋ธ”๋ก์„ ์ž‘์„ฑํ•ด ์ค์‹œ๋‹ค

    init(_ value: T) {
        self.value = value
    }

3๋ฒˆ ์ฝ”๋“œ ๋ธ”๋ก์€ ViewModel์—์„œ ๊ด€์ฐฐ ๊ฐ€๋Šฅํ•œ ๊ฐ์ฒด๋ฅผ ์„ค์ •ํ•ด ์ค„ ์ˆ˜ ์žˆ๋„๋ก ์ด๋‹ˆ์…œ๋ผ์ด์ €์—์„œ ํ•ด๋‹น ๊ฐ์ฒด๋ฅผ ๋ฐ›๋Š” ๋‹ค๋Š” ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. 

๋‹ค์‹œ ViewModel๋กœ ๋Œ์•„๊ฐ€๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค

 

ViewModel ์™„์„ฑํ•˜๊ธฐ

import Foundation

final class UserViewModel {
    var data: ObservableObject<User?> = ObservableObject(nil)
    
    func fetchData() {
        NetworkManager.shared.getUser { [weak self] isSuccess, data in
            if isSuccess {
                self?.data.value = data
            }
        }
    }
}

์˜ต์ €๋น™ํ•  User ๊ฐ์ฒด ํƒ€์ž…์„ value ํƒ€์ž…์œผ๋กœ ๋ฐ›๋Š” ObservableObject์ธ data ํ”„๋กœํผํ‹ฐ๋ฅผ ์ถ”๊ฐ€ํ•ด ์ค๋‹ˆ๋‹ค

๊ทธ ํ›„ ๋„คํŠธ์›Œํ‚น์— ์„ฑ๊ณตํ•˜๋ฉด ObservableObject์˜ value๋ฅผ ์—…๋ฐ์ดํŠธ ํ•ด์ค๋‹ˆ๋‹ค

์ด ํ”Œ๋กœ์šฐ๋Œ€๋กœ ์‹คํ–‰์ด ๋˜๋ฉด ์ตœ์ข…์ ์œผ๋กœ 

    var value: T {
        didSet {
            listener?(value)
        }
    }

ObservableObject์˜ value์˜ didSet ์ฝ”๋“œ๊ฐ€ ์‹คํ–‰๋˜๋ฉด์„œ listener๊ฐ€ ์‹คํ–‰๋˜๊ฒ ์ฃ ?

์ด์ œ listener์„ ์„ค์ •ํ•ด ์ฃผ๊ฒ ์Šต๋‹ˆ๋‹ค

 

View ์™„์„ฑํ•˜๊ธฐ

import UIKit

class UserViewController: UIViewController {
    private let viewModel = UserViewModel()
    
    let nameLabel = UILabel()
    let emailLabel = UILabel()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        nameLabel.backgroundColor = .gray
        emailLabel.backgroundColor = .yellow
        
        nameLabel.text = "์ด๋ฆ„์ž…๋‹ˆ๋‹ค"
        emailLabel.text = "์ด๋ฉ”์ผ์ž…๋‹ˆ๋‹ค"
        
        setupBinders() // โญ๏ธ 1
        attachUI()
        
        viewModel.fetchData()
    }
    
    // โญ๏ธ 2
    func setupBinders() {
        viewModel.data.bind { [weak self] user in
            guard let user = user else { return }
            DispatchQueue.main.async {
                self?.nameLabel.text = user.userName
                self?.emailLabel.text = user.email
            }
        }
    }

    func attachUI() {
        // ์ƒ๋žต ...
    }
}

์šฐ์„  2๋ฒˆ ์ฝ”๋“œ์˜ setupBinders() ๋ฉ”์†Œ๋“œ๋ฅผ ์ถ”๊ฐ€ํ•ด ์ค๋‹ˆ๋‹ค. 

์ด ๋ฉ”์†Œ๋“œ์—์„œ๋Š” viewModel์˜ ObservableObject์˜ listener๋ฅผ ์„ค์ •ํ•ด ์ค๋‹ˆ๋‹ค. bind๋ฅผ ์ด์šฉํ•ด์„œ value ๊ฐ’์ด ์—…๋ฐ์ดํŠธ ๋˜๋ฉด ์‹คํ–‰๋  listener ํด๋กœ์ €๋ฅผ ์ž‘์„ฑํ•ด ์ค๋‹ˆ๋‹ค. 

์ด๋•Œ ์ „๋‹ฌ ๋ฐ›๋Š” value ํƒ€์ž…์€ ViewModel์—์„œ ObservableObject์—์„œ ์„ค์ •ํ•ด ์ค€ User ์ž…๋‹ˆ๋‹ค!

 

user๊ฐ€ nil์ด ์•„๋‹ˆ๋ผ๋ฉด UI๋ฅผ ์—…๋ฐ์ดํŠธ ํ•ด์ค๋‹ˆ๋‹ค. ์ด๋•Œ ํด๋กœ์ € ๋‚ด๋ถ€์—์„œ ์‹คํ–‰๋˜๊ธฐ ๋•Œ๋ฌธ์— ๋ฉ”์ธ์Šค๋ ˆ๋“œ๋กœ ๋ณ€๊ฒฝํ•˜๋Š” ์ž‘์—…์ด ํ•„์š” ํ•ฉ๋‹ˆ๋‹ค.

 

1๋ฒˆ ์ฝ”๋“œ setupBinders()๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ๋ฉด ๋ฉ๋‹ˆ๋‹ค!

 

์™„์„ฑ

ํ”Œ๋ ˆ์ด์Šคํ™€๋”๊ฐ€ ์‚ด์ง ๋ณด์˜€๋‹ค๊ฐ€ ๋„คํŠธ์›Œํ‚น์ด ๋๋‚˜๊ณ  UI๊ฐ€ ์—…๋ฐ์ดํŠธ ๋œ ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค!

 

 

์ฐธ๊ณ  ์‚ฌ์ดํŠธ

https://www.youtube.com/watch?v=sbYaWJEAYIY <- ์ œ ๊ธฐ์ค€ ์ •๋ง ์ž˜ ์ •๋ฆฌํ•ด ๋†“์€ ์˜์ƒ์ธ๊ฑฐ ๊ฐ™์Šต๋‹ˆ๋‹ค ๊ผญ ๋ณด์‹œ๊ธธ ์ถ”์ฒœ ๋“œ๋ฆฝ๋‹ˆ๋‹ค ๐Ÿ™Œ๐Ÿป

 

๋ฌธ์ œ๋‚˜ ์ด์ƒํ•œ ์ ์ด ์žˆ๋‹ค๋ฉด ์–ธ์ œ๋“  ๋Œ“๊ธ€ ๋ถ€ํƒ๋“œ๋ ค์š”๐Ÿฆ‘

728x90
๋ฐ˜์‘ํ˜•

๋Œ“๊ธ€