MVVM 패턴을 도입하게 된 이유와 구조적 이점
iOS 개발을 하다 보면, UIViewController 파일이 점점 커지고 복잡해지는 상황을 자주 마주하게 됩니다.
기능이 늘어나면서 화면 구성, 사용자 입력 처리, 네트워크 통신, 데이터 가공 등 여러 역할을 한 클래스에서 수행하게 되는데, 이로 인해 코드의 가독성과 유지보수성이 급격히 떨어지게 됩니다.
이러한 문제를 해결하기 위해 등장한 것이 바로 MVVM(Model - View - ViewModel) 패턴입니다.
이번 글에서는 MVVM 패턴의 구성 요소와 각각의 역할, 그리고 실제 예제를 통해 어떻게 ViewController의 책임을 줄이고 관심사를 분리할 수 있는지를 정리해보고자 합니다.
MVVM의 구성 요소
MVVM은 그 이름처럼 Model, View, ViewModel의 세 가지로 구성되어 있으며, 각 요소는 다음과 같은 역할을 수행합니다.
Model
- 앱의 데이터 구조와 비즈니스 로직을 담당합니다.
- 네트워크 통신, 데이터 저장, 파싱 등을 포함하며, UI와는 완전히 분리되어 있습니다.
- 데이터를 변경하고, 이 변경사항을 ViewModel로 전달합니다.
View
- 사용자에게 UI를 보여주는 역할을 하며, 사용자 입력을 ViewModel에 전달합니다.
- UIView나 UIViewController가 이에 해당합니다.
- ViewModel이 전달한 데이터를 바인딩하여 화면을 구성합니다.
- 직접 비즈니스 로직을 처리하지 않고, UI와 관련된 작업만 수행합니다.
ViewModel
- View와 Model 사이에서 중재자 역할을 하며, UI 상태와 관련된 로직을 담당합니다.
- Model에서 데이터를 받아 필요한 형태로 가공하여 View에 전달합니다.
- View에 직접 명령을 내리지 않고, 데이터의 변경을 Publish하고 View는 이를 구독(Subscribe)하여 UI를 갱신합니다.
- RxSwift, Combine, 클로저 등의 방식으로 View와 데이터를 바인딩합니다.
MVC와의 차이점
기존의 MVC 구조에서는 Controller가 View와 Model 사이에서 모든 역할을 처리하게 되는데, 이로 인해 ViewController가 비대해지는 문제가 자주 발생합니다.
반면, MVVM에서는 ViewModel이 등장하면서 Controller는 UI 바인딩만 수행하고, 로직은 ViewModel로 위임됩니다. 이로써 ViewController는 View로만 역할을 제한하고, ViewModel이 비즈니스 로직을 분리해 관리할 수 있게 됩니다.
MVVM 예제
아래는 MVVM 패턴을 적용한 간단한 예제입니다. 사용자가 버튼을 눌렀을 때 리스트를 정렬하고, 정렬된 데이터를 테이블뷰에 출력하는 구조입니다.
ViewController (View 역할)
final class TickerViewController: UIViewController {
private let viewModel: TickerViewModel
private let disposeBag = DisposeBag()
init(viewModel: TickerViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
viewModel.output.sortedItems
.bind(to: tableView.rx.items(cellIdentifier: "Cell")) { _, item, cell in
cell.textLabel?.text = item.name
}
.disposed(by: disposeBag)
sortButton.rx.tap
.bind(to: viewModel.input.sortButtonTapped)
.disposed(by: disposeBag)
}
}
이 구조에서 ViewController는 UI 바인딩과 사용자 입력 전달만 담당하며, 로직 처리나 데이터 가공은 ViewModel에 전적으로 위임하고 있습니다.
ViewModel
final class TickerViewModel {
struct Input {
let sortButtonTapped: PublishRelay<Void>
}
struct Output {
let sortedItems: Observable<[UpbitMarketEntity]>
}
let input = Input(sortButtonTapped: .init())
let output: Output
init() {
let items = BehaviorRelay(value: sampleMarkets)
let sorted = input.sortButtonTapped
.map { items.value.sorted(by: { $0.price > $1.price }) }
output = Output(sortedItems: sorted)
}
}
ViewModel은 Model에서 받아온 데이터를 정렬하고, 이 데이터를 View에 제공하는 역할만 수행합니다. ViewModel은 UIKit을 전혀 모르기 때문에 테스트가 쉽고, 재사용성도 높습니다.
MVVM 패턴의 장점
- 역할 분리를 통해 코드가 간결해지고 가독성이 높아집니다.
- ViewModel은 테스트가 용이하며, 로직이 명확하게 분리되어 유지보수가 쉬워집니다.
- ViewModel은 View에 의존하지 않기 때문에 재사용성과 유연성이 향상됩니다.
- RxSwift나 Combine을 활용하면 UI 업데이트를 자동화할 수 있어 코드가 깔끔해집니다.
MVVM 패턴의 단점
- 프로젝트 초기 구조 설계가 복잡할 수 있으며, 작은 프로젝트에서는 오히려 과한 설계가 될 수 있습니다.
- 바인딩 개념(Rx, Combine 등)에 대한 이해가 필요합니다.
- ViewModel이 Model, View와 양방향으로 연결되기 때문에 잘못 설계하면 오히려 코드 흐름이 불명확해질 수 있습니다.
마무리
MVVM은 모든 프로젝트에 반드시 적합한 패턴은 아닙니다. 하지만 뷰의 복잡도가 높아지고, UI 상태가 점점 많아질수록 그 구조적인 이점이 분명하게 드러납니다.
저는 앞으로 뷰 컨트롤러나 모델 객체를 설계할 때는 클래스를 활용하고, 뷰 간의 데이터 전달이나 UI 상태 제어처럼 명확한 역할이 필요한 경우에는 ViewModel을 도입하여 MVVM 패턴을 적용하고자 합니다.
View는 UI만, ViewModel은 로직만, Model은 데이터만.
관심사의 분리는 결국 유지보수 가능한 구조로 이어집니다.
'iOS > 디자인 패턴' 카테고리의 다른 글
[디자인 패턴] MVC (Model-View-Controller) (0) | 2025.02.06 |
---|