디자인 패턴이란? (개발을 위한) 목표를 잘 달성하기 위한 패턴
목표
- 기술 부채 최소화
- 재사용 및 지속 가능한 코드 만들기
코드 구조에 대한 전략, 어떤식으로 코드를 구조화시켜야하나에 대한 전략
→ 클린 아키텍처, 디자인 패턴
현재 가장 많이 사용되는 디자인 패턴 : MVVM (Model - View - ViewModel)
Model: data (struct)
View: UI요소 (UIView, UIViewController)
ViewModel: 중계자 (ViewModel), 모델의 내용을 뷰에서 사용할 수 있게 전환시켜주는 역할 → class 타입
이전에 많이 사용됐던 패턴 : MVC (Model - View - Controller)
Model : 앱 내에서 사용되는 data (struct 타입으로 표현됨, 아주 가끔 class로 표현)
View: 화면에서 보여지는 요소들 (UI요소, UIView)
Controller: 뷰와 모델의 중계자 (UIViewController), Model에서 data가 바뀐 경우 컨트롤러에게 알려주기도 하고, 컨트롤러에서 모델한테 값을 바꿔야한다고 직접 접근해서 변경하는 경우도 있음. 컨트롤러가 데이터를 가지고 뷰에게 어떻게 보여줘야겠다고 의사소통하는 경우, 뷰가 사용자로부터 받은 인터랙션을 컨트롤러한테 넘겨주기도 함
뷰컨트롤러가 너무 해야할 일이 많아질 경우가 있음 → 기술부채 생성
MVC vs MVVM
: 뷰컨트롤러가 모델에 직접 접근하지 못함
: 뷰컨트롤러 → 뷰모델이라는 클래스를 새로 가짐
: 뷰컨트롤러 자체가 예전에는 컨트롤러 레이어였지만 이제는 뷰레이어에 있음
MVVM 개선점
: 뷰컨트롤러의 오지랖을 줄임^^;; 역할을 줄임 → 할 일이 조금 더 명확해짐 → 수정이 용이하고 유지보수 비용이 줄어듦
: 뷰모델로 역할을 위임
MVVM 실제 구현!
- 뷰컨트롤러는 뷰모델과 뷰를 가지고 있음 ( 뷰와 뷰컨트롤러는 뷰레이어에 속함, 뷰모델은 아님 )
- 뷰모델이 중계자 역할을 함, 뷰, 뷰컨트롤러랑 커뮤니케이션함
- 뷰모델은 모델을 가지고 있음 MVC 패턴에서 뷰컨트롤러가 모델에 직접 접근하는걸 뷰모델이 막고 있음
→ 모델은 뷰레이어(뷰, 뷰컨트롤러)와 직접 소통하지 않음, 모든 것은 뷰모델을 통해서~
MVVM 실전
코드를 수정: 리팩터링
- 중복 삭제
- 단일 책임 갖기 (작게는 method, 크게는 object 단위)
→ 10, 200 rule
method는 10줄 안에, class는 200줄 안에 짜기
→ 대신 30, 400 rule
모델에 맞게 각 화면에서 뭐가 있는지 찾고 패턴에 맞게 코드 수정하기
1) BountyViewController
import UIKit
class BountyViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
//MVVM
//Model
// - BountyInfo (name, bounty)
// > BountyInfo 만들자
//View
// - ListCell
// > ListCell에 필요한 정보를 ViewController가 아닌 ViewModel한테서 받아야겠다
// > ListCell은 ViewModel로 부터 받은 정보로 뷰 업데이트 하기
//ViewModel
// - BountyViewModel
// - BountyViewModel을 만들고 뷰레이어에서 필요한 메서드 만들기
// > 모델 가지고 있기 ,, BountyInfo 가지고 있기
let BountyInfoList:[BountyInfo] = [BountyInfo(name:"brook",bounty:33000000),BountyInfo(name:"chopper",bounty:50),BountyInfo(name:"franky",bounty:4400000),BountyInfo(name:"luffy",bounty:30000000),BountyInfo(name:"nami",bounty:1600000),BountyInfo(name:"robin",bounty:8000000),BountyInfo(name:"sanji",bounty:7700000),BountyInfo(name:"zoro",bounty:12000000)]
// let nameList = ["brook", "chopper", "franky", "luffy", "nami", "robin", "sanji", "zoro"]
// let bountyList = [33000000, 50, 4400000, 30000000, 1600000, 8000000, 7700000, 12000000]
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "showDetail" {
let vc = segue.destination as?DetailViewController
if let index = sender as? Int {
let bountyInfo = bountyInfoList[index]
// vc?.name = nameList[index]
// vc?.bounty = bountyList[index]
// vc?.name = bountyInfo.name
// vc?.bounty = bountyInfo.bounty
vc?.bountyInfo = bountyInfo
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
//return bountyList.count
return bountyInfoList.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as?ListCell else {
return UITableViewCell()
}
// let img = UIImage(named: "\(nameList[indexPath.row]).jpg")
// cell.imgView.image = img
// cell.nameLabel.text = nameList[indexPath.row]
// cell.bountyLabel.text = "\(bountyList[indexPath.row])"
let bountyInfo = bountyInfoList[indexPath.row]
cell.imgView.image = bountyInfo.image
cell.nameLabel.text = bountyInfo.name
cell.bountyLabel.text = "\(bountyInfo.bounty)"
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("---> \(indexPath.row)")
performSegue(withIdentifier: "showDetail", sender: indexPath.row)
}
}
class ListCell: UITableViewCell{
@IBOutlet weak var imgView: UIImageView!
@IBOutlet weak var nameLabel: UILabel!
@IBOutlet weak var bountyLabel: UILabel!
}
struct BountyInfo{
let name:String
let bounty:Int
var image:UIImage?{
return UIImage(named: "\(name).jpg")
}
init(name:String, bounty:Int){
self.name = name
self.bounty = bounty
}
}
2) DetailViewController
import UIKit
class DetailViewController: UIViewController {
//MVVM
//Model
// - BountyInfo (name, bounty)
// > BountyInfo 만들자
//View
// - imgView, nameLabel, bountlabel
// > view들은 viewModel을 통해서 구성되기
//ViewModel
// - DetailViewModel
// 뷰레이어에서 필요한 메서드 만들기
// > 모델 가지고 있기 ,, BountyInfo 가지고 있기
@IBOutlet weak var imgView: UIImageView!
@IBOutlet weak var nameLabel: UILabel!
@IBOutlet weak var bountyLabel: UILabel!
// var name: String?
// var bounty: Int?
var bountyInfo: BountyInfo?
override func viewDidLoad() {
super.viewDidLoad()
updateUI()
}
func updateUI(){
if let bountyInfo = self.bountyInfo {
let img = bountyInfo.image
imgView.image = img
nameLabel.text = bountyInfo.name
bountyLabel.text = "\(bountyInfo.bounty)"
}
// if let name = self.name, let bounty = self.bounty{
// let img = UIImage(named: "\(name).jpg")
// imgView.image = img
// nameLabel.text = name
// bountyLabel.text = "\(bounty)"
// }
}
@IBAction func close(_ sender: Any) {
dismiss(animated: true, completion: nil)
}
}
1) BountyViewController
2) DetailViewController에도 동일하게 적용시켜주면 됨