iOS) MVVM 패턴, 그거 어떻게 하는 건데 ?

영모·2022년 4월 13일
0

MVVM 패턴이란 ?


View에서 ViewModel을 통해 Model을 전달받는 구조다.
View에서는 오직 UI만 구성하고, UI에 띄울 Data는 ViewModel에서 받아오기만 하면 된다.

MVC 패턴과 비교해보자

UIKit은 MVC 패턴을 권장하고, SwiftUI에서는 MVVM 패턴을 권장한다.

View를 분리하긴 했지만, ViewController(VC)에서 View를 사용해야하고 Model도 가져와야 한다. VC가 무거워질 수 밖에 없는 구조이다.
MVVM 패턴은 무거워진 VC를 해결해준다.

디렉토리 구조 비교

MVC 패턴

  • 🗂 Model
    • 📒 Model.swift
  • 🗂 View
    • 📒 MainHearderView.swift
    • 📒 MainContentView.swift
    • 📒 MainFooterView.swift
  • 🗂 ViewController
    • 📒 MainViewController.swift

MVVM 패턴

  • 🗂 Model
    • 📒 Model.swift
  • 🗂 ViewModel
    • 📒 MainViewModel.swift
  • 🗂 View
    • 📒 MainHearderView.swift
    • 📒 MainContentView.swift
    • 📒 MainFooterView.swift
  • 🗂 ViewController
    • 📒 MainViewController.swift

ViewModel 폴더가 생겼는데, 오직 뷰컨을 위한 뷰모델을 만들어 주어야 한다.
Model를 관리하는 일은 이제 ViewModel에서 처리하고, ViewController에서는 View만 사용하면 된다.

그래서 ViewModel이 뭐야 ?

디렉토리 구조 비교에서 ViewModel의 차이가 있었다.
ViewModel은 어떤 역할을 하고 있을까 ?
이건 아마도 코드를 보면서 이해하는게 더 좋을 것 같다.

좀 더 자세한 내용이 필요했었는데 ..

ViewModel (Service -> Repository -> Entity)

ViewModel을 도와주는 Service, Repository 생성하기.

RecordEntity

서버에서 받은 Json을 파싱할 구조체를 선언합니다. 참고로 Class로 선언된 이유는 API 연동을 안했고, 내부 저장소인 Realm을 사용하기 때문입니다. 패턴 참고용으로 설명하겠습니다.

class RecordEntity: Object {
    @objc dynamic var id: Int = 0
    @objc dynamic var type: String = ""
    @objc dynamic var tag: String = ""
    @objc dynamic var item: String = ""
    @objc dynamic var price: Double = 0.0
    @objc dynamic var count: Double = 0.0
    @objc dynamic var date: Date = Date()
    @objc dynamic var memo: String = ""
    
    override static func primaryKey() -> String? {
        return "id"
    }
}

RecordRepository

서버에서 API를 호출하고 Entity로 파싱해서 가져오는 클래스입니다.

class RecordRepository {
    let instance = try! Realm()
    
    func postRecordEntity(recordEntity: RecordEntity) {
        var id = 0
        if let lastRecord = instance.objects(RecordEntity.self).last {
            id = lastRecord.id + 1
        }
        
        recordEntity.id = id
        try? instance.write {
            self.instance.add(recordEntity)
        }
    }
}

RecordService

Repository로 Entity를 가져왔다면, Model로 다시 파싱해주어야 하는데 RecordService가 그 역할을 합니다.

class RecordService {
    
    let recordRepository = RecordRepository()
    
    func getRecords() -> [Record] {
        let recordEntities = recordRepository.getAllRecordEntity()
        var records: [Record] = []
        
        for recordEntity in recordEntities {
            records.append(parseToRecord(recordEntity: recordEntity))
        }
        
        return records
    }
    
    func postRecord(record: Record) {
        let recordEntity = parseToRecordEntity(record: record)
        recordRepository.postRecordEntity(recordEntity: recordEntity)
    }
    
    private func parseToRecord(recordEntity: RecordEntity) -> Record {
        let record = Record(id: recordEntity.id,
                            type: recordEntity.type,
                            tag: recordEntity.tag,
                            item: recordEntity.item,
                            price: recordEntity.price,
                            count: recordEntity.count,
                            date: recordEntity.date,
                            memo: recordEntity.memo)
        
        return record
    }
}

CalendarViewModel

View모델에서는 Service를 사용해서 Model을 가져오면 끝 입니다.

class CalendarViewModel: ViewModel {
    let recordService = RecordService()
    
    var disposeBag = DisposeBag()
    
    var input = Input()
    var output = Output()
    
    struct Input {
        let nowDate = BehaviorSubject<Date>(value: Date())
        let isClickedDatePickerButton = BehaviorSubject(value: false)
        let cellData = PublishSubject<(Date, [Record])>()
        let indexPath = BehaviorRelay(value: IndexPath())
    }
    
    struct Output {
        let records = PublishRelay<[Record]>()
        let dates = PublishRelay<[Date]>()
        let datePickerOpen = PublishRelay<Bool>()
        let dateLabel = BehaviorRelay(value: "")
        let cellDatas = BehaviorRelay(value: [(Date(), [Record()])])
    }
    
    init() {
        setBind()
        output.records.accept(recordService.getRecords())
        output.dates.accept(getDatesOfMonth(date: try! input.nowDate.value()))
    }
}
profile
iOS를 좋아하고, 제품을 만들어가는 과정과 실패를 소중하게 여깁니다.

0개의 댓글

관련 채용 정보