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

[Design Pattern] MVVM(Model - View - ViewModel) ํŒจํ„ด์ด๋ž€? (feat. Swift)

by Fomagran ๐Ÿ’ป 2021. 10. 19.
728x90
๋ฐ˜์‘ํ˜•

์•ˆ๋…•ํ•˜์„ธ์š” 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

 

๊ฐ ๋””์ž์ธ ํŒจํ„ด์˜ ์†Œ์Šค์ฝ”๋“œ๋Š” ์•„๋ž˜ ๊นƒํ—™์— ์ •๋ฆฌํ•ด๋†“๊ฒ ์Šต๋‹ˆ๋‹ค!

 

 

GitHub - fomagran/Architecture-Example: iOS ์•„ํ‚คํ…์ณ ์˜ˆ์ œ ๋ชจ์Œ ๐Ÿ 

iOS ์•„ํ‚คํ…์ณ ์˜ˆ์ œ ๋ชจ์Œ ๐Ÿ . Contribute to fomagran/Architecture-Example development by creating an account on GitHub.

github.com


Reference

์•„๋ž˜ ์‚ฌ์ดํŠธ๋ฅผ ์ฐธ๊ณ ํ–ˆ์Šต๋‹ˆ๋‹ค.

 

 

Design Patterns by Tutorials: MVVM

Learn how and when to use the architecture-slash-design pattern of MVVM in this free chapter from our new book, Design Patterns by Tutorials!

www.raywenderlich.com

 

728x90
๋ฐ˜์‘ํ˜•

๋Œ“๊ธ€