현재 잇차 앱에서 쓰이고 있는 MVC 구조는 모델의 뷰 적용, 라이프 사이클 이벤트 관리, 콜백 처리 등을 View Controller에서 전부 전담하고 있기 때문에 추가 기능 등이 늘어남에 따라 각 컨트롤러 별 변수, 함수들이 점점 많아지고 있는 상태이다. 이러한 상황이 계속된다면 하나의 View Controller에서 비대한 코드들을 관리해야 될 것이다. 이러한 문제를 해결하기 위해 로직부분을 ViewModel로 옮겨 View Controller의 크기를 줄이고, 테스트에도 용이한 MVVM 구조로의 변환이 필요할 것으로 보인다.
MVVM의 특징
- View Controller가 모델에 직접 접근하지 못한다. (View Model은 Model을 가지고 있고, Model은 View Layer와 소통하지 않는다. View와 ViewModel은 1:n 관계)
- View Controller자체가 mvc에서는 controller, mvvm에서는 view layer에 속해 있어 View Controller의 역할을 축소시킬 수 있다.
- 많은 일을 View Model에 위임했기 때문에 클래스의 할일이 더 명확해진다. 클래스의 할일이 명확해질수록 수정이 용이하고 유지보수에 적은 비용이 드는 장점이 있다.
- View Model은 중계자 역할. 뷰와 뷰 컨트롤러 사이에서 커뮤니케이션 역할을 한다.
- View Model에서는 UIKit 관련된 코드가 없으므로 UI에 독립적인 테스트를 할 수 있다.

Model(모델), View(뷰), ViewModel(뷰모델). Controller를 빼고 ViewModel을 추가한 패턴이다. 여기서 View Controller가 View가 되고, ViewModel이 중간 역할을 한다. View와 ViewModel 사이에 Binding(바인딩-연결고리)가 있어 ViewModel은 Model에 변화를 주고, ViewModel을 업데이트하는데 이 바인딩으로 인해 View도 업데이트된다. ViewModel은 View에 대해 아무것도 모르기 때문에 테스트가 쉽고 바인딩으로 인해 코드 양이 최적화된다는 장점이 있다.
Protocol Oriented MVVM
프로토콜 지향 MVVM 디자인 패턴은 기존에 기능별로 패턴을 쪼개는 것이 어려웠던 MVC 패턴과는 다르게 자신의 할 일을 명확히 나누는 것이 가능하다.
- Model
struct Person {
let firstName: String
let lastName: String
}
- View Model
뷰 모델을 프로토콜로 구성하면 코드가 더 간결해지고 명확해지는 장점이 있다.
// 프로토콜
protocol GreetingViewModelProtocol: class {
var greeting: String? { get }
var greetingDidChange: ((GreetingViewModelProtocol) -> ())? { get set }
init(person: Person)
func showGreeting()
}
// 뷰모델
class GreetingViewModel: GreetingViewModelProtocol {
let person: Person
var greeting: String? {
didSet {
self.greetingDidChange?(self)
}
}
var greetingDidChange: ((GreetingViewModelProtocol) -> ())?
required init(person: Person) {
self.person = person
}
func showGreeting() {
self.greeting = "Hello" + " " + self.person.firstName + " " + self.person.lastName
}
}
- View
class GreetingViewController: UIViewController {
var viewModel: GreetingViewModelProtocol! {
didSet {
self.viewModel.greetingDidChange = { [unowned self] viewModel in
self.greetingLabel.text = viewModel.greeting
}
}
}
let showGreetingButton = UIButton()
let greetingLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
self.showGreetingButton.addTarget(self.viewModel, action: "showGreeting", forControlEvents: .TouchUpInside)
// Assembling of MVVM
let model = Person(firstName: "David", lastName: "Blaine")
let viewModel = GreetingViewModel(person: model)
let view = GreetingViewController()
view.viewModel = viewModel
}
}
RxSwift + MVVM 구조 설계

View와 ViewModel은 RxSwift, RxCocoa 라이브러리를 사용하여 two way binding을 한다. ViewModel은 binding해야 될 데이터를 정의해야하며, DataBinding Interface를 통해 바인딩 전략을 구현해야한다.
주로 ViewModel에 Input, Output 프로토콜 채택하는 방식으로 많이 쓰인다.
RxSwift와 같이 이용 시, Rective 클래스에서 만들어지는 observable만을 이용하여 입력을 만들고, 그 입력을 통해 데이터를 조작하고 난 이후의 데이터들을 bind 또는 drive 시키는 형태의 구조로 사용할 수 있다. 그러나 Reactive를 사용하게 되면 코드가 혼잡해지기 쉬워지고, 디버깅 시 시간이 상당히 많이 걸린다는 단점이 있다.
ex) 예시 1.
protocol RepoViewModelInputs {
func viewWillAppear()
func didSelect(index: IndexPath)
func didSearch(query: String)
}
protocol RepoViewModelOutputs {
var loading: Driver<Bool> { get }
var repos: Driver<[RepoViewModel]> { get }
var selectedRepoId: Driver<Int> { get }
}
protocol ReposViewModelType {
var inputs: RepoViewModelInputs { get }
var outputs: RepoViewModelOutputs { get }
}
final class ReposViewModel: ReposViewModelType, RepoViewModelInputs, RepoViewModelOutputs { ... }
ex) 예시 2.
- Input, Output을 프로토콜의 형태로 만들어준다.
- transform을 이용하여 입력을 넣어주고 출력을 받는다.
이후, viewModel에 적용을 해주면 자신이 정한 클래스 타입에 대한 In, Out을 만들어 줄 수 있다.
ViewController에서는 Input에 대한 바인딩, ViewModel에서는 transform에서 비즈니스 로직을 처리하여 Output으로 반환한다.
protocol ViewModelType {
associatedtype Input
associatedtype Output
func transform(input: Input) -> Output
}
final class ViewModel: ViewModelType {
struct Input {
let click: ControlEvent<Void>
}
struct Output {
let text: Driver<String>
}
func transform(input: Input) -> Output {
let text = input.click
.map { _ in return "Hello world!" }
.asDriver(onErrorJustReturn: "")
return Output(text: text)
}
}
- ViewController
ViewController에서 발생한 click event를 전달받게 되면 map을 이용하여 output에 넣어줄 데이터를 만들고 이것을 return 해준다. 그 결과, 입출력이 구분되어서 ViewController는 단순히 View의 역할만 하게 된다.
final class ViewController: UIViewController {
@IBOutlet weak var actionButton: UIButton!
@IBOutlet weak var printLabel: UILabel!
private let viewModel = ViewModel()
private let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
bindViewModel()
}
private func bindViewModel() {
let input = ViewModel.Input(click: actionButton.rx.tap)
let output = viewModel.transform(input: input)
output.text
.drive(printLabel.rx.text)
.disposed(by: disposeBag)
}
}
ex) 예시 3.
아래와 같이 구성할때 문제점 혹은 어려운 부분
- input에 대한 output이 init에서 모두 구성하거나, 별도의 observer type을 생성 해야 한다는점
- input이 Action인지 Property의 업데이트인지 명확하지 않은 점
- Output의 초기값이 있는지, 없는지 혹은 ObserverType으로 Output을 구성했을때 외부에서 값을 변경할 수 있는 점
- Rx의 의존도가 매우 상승하게 된다는 점
final class ReposViewModel {
// Inputs
let viewWillAppearSubject = PublishSubject<Void>()
let searchQuerySubject = BehaviorSubject(value: "")
// Outputs
var repos: Driver<[RepoViewModel]>
let networkingService: NetworkingService
init(networkingService: NetworkingService) {
self.networkingService = networkingService
let initialRepos = self.viewWillAppearSubject
.asObservable()
.flatMap { _ in
networkingService
.searchRepos(withQuery: "swift")
}
.asDriver(onErrorJustReturn: [])
let searchRepos = self.searchQuerySubject
.asObservable()
.filter { $0.count > 2 }
.distinctUntilChanged()
.flatMapLatest { query in
networkingService.searchRepos(withQuery: query)
}
.asDriver(onErrorJustReturn: [])
let repos = Driver.merge(initialRepos, searchRepos)
self.repos = repos.map { $0.map { RepoViewModel(repo: $0)}}
}
}
- View Controller
private func bindViewModel() {
rx.viewWillAppear
.asObservable()
.bind(to: viewModel.viewWillAppearSubject)
.disposed(by: disposeBag)
viewModel.repos
.drive(tableView.rx.items(cellIdentifier: "Cell", cellType: UITableViewCell.self)) { row, element, cell in
cell.textLabel?.text = element.name
}
.disposed(by: disposeBag)
}
'iOS' 카테고리의 다른 글
Fastlane을 활용한 프로젝트 배포 자동화 구축 (0) | 2022.05.06 |
---|---|
AWS S3에 이미지 업로드 하기 (0) | 2022.03.25 |
Apple Enterprise 와 Apple Business Manager (0) | 2021.10.20 |
iOS Framework 만들어보기 (0) | 2021.07.10 |
스위프트 패키지 매니저(Swift Package Manager) 라이브러리 만들기 (0) | 2021.07.09 |
현재 잇차 앱에서 쓰이고 있는 MVC 구조는 모델의 뷰 적용, 라이프 사이클 이벤트 관리, 콜백 처리 등을 View Controller에서 전부 전담하고 있기 때문에 추가 기능 등이 늘어남에 따라 각 컨트롤러 별 변수, 함수들이 점점 많아지고 있는 상태이다. 이러한 상황이 계속된다면 하나의 View Controller에서 비대한 코드들을 관리해야 될 것이다. 이러한 문제를 해결하기 위해 로직부분을 ViewModel로 옮겨 View Controller의 크기를 줄이고, 테스트에도 용이한 MVVM 구조로의 변환이 필요할 것으로 보인다.
MVVM의 특징
- View Controller가 모델에 직접 접근하지 못한다. (View Model은 Model을 가지고 있고, Model은 View Layer와 소통하지 않는다. View와 ViewModel은 1:n 관계)
- View Controller자체가 mvc에서는 controller, mvvm에서는 view layer에 속해 있어 View Controller의 역할을 축소시킬 수 있다.
- 많은 일을 View Model에 위임했기 때문에 클래스의 할일이 더 명확해진다. 클래스의 할일이 명확해질수록 수정이 용이하고 유지보수에 적은 비용이 드는 장점이 있다.
- View Model은 중계자 역할. 뷰와 뷰 컨트롤러 사이에서 커뮤니케이션 역할을 한다.
- View Model에서는 UIKit 관련된 코드가 없으므로 UI에 독립적인 테스트를 할 수 있다.

Model(모델), View(뷰), ViewModel(뷰모델). Controller를 빼고 ViewModel을 추가한 패턴이다. 여기서 View Controller가 View가 되고, ViewModel이 중간 역할을 한다. View와 ViewModel 사이에 Binding(바인딩-연결고리)가 있어 ViewModel은 Model에 변화를 주고, ViewModel을 업데이트하는데 이 바인딩으로 인해 View도 업데이트된다. ViewModel은 View에 대해 아무것도 모르기 때문에 테스트가 쉽고 바인딩으로 인해 코드 양이 최적화된다는 장점이 있다.
Protocol Oriented MVVM
프로토콜 지향 MVVM 디자인 패턴은 기존에 기능별로 패턴을 쪼개는 것이 어려웠던 MVC 패턴과는 다르게 자신의 할 일을 명확히 나누는 것이 가능하다.
- Model
struct Person {
let firstName: String
let lastName: String
}
- View Model
뷰 모델을 프로토콜로 구성하면 코드가 더 간결해지고 명확해지는 장점이 있다.
// 프로토콜
protocol GreetingViewModelProtocol: class {
var greeting: String? { get }
var greetingDidChange: ((GreetingViewModelProtocol) -> ())? { get set }
init(person: Person)
func showGreeting()
}
// 뷰모델
class GreetingViewModel: GreetingViewModelProtocol {
let person: Person
var greeting: String? {
didSet {
self.greetingDidChange?(self)
}
}
var greetingDidChange: ((GreetingViewModelProtocol) -> ())?
required init(person: Person) {
self.person = person
}
func showGreeting() {
self.greeting = "Hello" + " " + self.person.firstName + " " + self.person.lastName
}
}
- View
class GreetingViewController: UIViewController {
var viewModel: GreetingViewModelProtocol! {
didSet {
self.viewModel.greetingDidChange = { [unowned self] viewModel in
self.greetingLabel.text = viewModel.greeting
}
}
}
let showGreetingButton = UIButton()
let greetingLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
self.showGreetingButton.addTarget(self.viewModel, action: "showGreeting", forControlEvents: .TouchUpInside)
// Assembling of MVVM
let model = Person(firstName: "David", lastName: "Blaine")
let viewModel = GreetingViewModel(person: model)
let view = GreetingViewController()
view.viewModel = viewModel
}
}
RxSwift + MVVM 구조 설계

View와 ViewModel은 RxSwift, RxCocoa 라이브러리를 사용하여 two way binding을 한다. ViewModel은 binding해야 될 데이터를 정의해야하며, DataBinding Interface를 통해 바인딩 전략을 구현해야한다.
주로 ViewModel에 Input, Output 프로토콜 채택하는 방식으로 많이 쓰인다.
RxSwift와 같이 이용 시, Rective 클래스에서 만들어지는 observable만을 이용하여 입력을 만들고, 그 입력을 통해 데이터를 조작하고 난 이후의 데이터들을 bind 또는 drive 시키는 형태의 구조로 사용할 수 있다. 그러나 Reactive를 사용하게 되면 코드가 혼잡해지기 쉬워지고, 디버깅 시 시간이 상당히 많이 걸린다는 단점이 있다.
ex) 예시 1.
protocol RepoViewModelInputs {
func viewWillAppear()
func didSelect(index: IndexPath)
func didSearch(query: String)
}
protocol RepoViewModelOutputs {
var loading: Driver<Bool> { get }
var repos: Driver<[RepoViewModel]> { get }
var selectedRepoId: Driver<Int> { get }
}
protocol ReposViewModelType {
var inputs: RepoViewModelInputs { get }
var outputs: RepoViewModelOutputs { get }
}
final class ReposViewModel: ReposViewModelType, RepoViewModelInputs, RepoViewModelOutputs { ... }
ex) 예시 2.
- Input, Output을 프로토콜의 형태로 만들어준다.
- transform을 이용하여 입력을 넣어주고 출력을 받는다.
이후, viewModel에 적용을 해주면 자신이 정한 클래스 타입에 대한 In, Out을 만들어 줄 수 있다.
ViewController에서는 Input에 대한 바인딩, ViewModel에서는 transform에서 비즈니스 로직을 처리하여 Output으로 반환한다.
protocol ViewModelType {
associatedtype Input
associatedtype Output
func transform(input: Input) -> Output
}
final class ViewModel: ViewModelType {
struct Input {
let click: ControlEvent<Void>
}
struct Output {
let text: Driver<String>
}
func transform(input: Input) -> Output {
let text = input.click
.map { _ in return "Hello world!" }
.asDriver(onErrorJustReturn: "")
return Output(text: text)
}
}
- ViewController
ViewController에서 발생한 click event를 전달받게 되면 map을 이용하여 output에 넣어줄 데이터를 만들고 이것을 return 해준다. 그 결과, 입출력이 구분되어서 ViewController는 단순히 View의 역할만 하게 된다.
final class ViewController: UIViewController {
@IBOutlet weak var actionButton: UIButton!
@IBOutlet weak var printLabel: UILabel!
private let viewModel = ViewModel()
private let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
bindViewModel()
}
private func bindViewModel() {
let input = ViewModel.Input(click: actionButton.rx.tap)
let output = viewModel.transform(input: input)
output.text
.drive(printLabel.rx.text)
.disposed(by: disposeBag)
}
}
ex) 예시 3.
아래와 같이 구성할때 문제점 혹은 어려운 부분
- input에 대한 output이 init에서 모두 구성하거나, 별도의 observer type을 생성 해야 한다는점
- input이 Action인지 Property의 업데이트인지 명확하지 않은 점
- Output의 초기값이 있는지, 없는지 혹은 ObserverType으로 Output을 구성했을때 외부에서 값을 변경할 수 있는 점
- Rx의 의존도가 매우 상승하게 된다는 점
final class ReposViewModel {
// Inputs
let viewWillAppearSubject = PublishSubject<Void>()
let searchQuerySubject = BehaviorSubject(value: "")
// Outputs
var repos: Driver<[RepoViewModel]>
let networkingService: NetworkingService
init(networkingService: NetworkingService) {
self.networkingService = networkingService
let initialRepos = self.viewWillAppearSubject
.asObservable()
.flatMap { _ in
networkingService
.searchRepos(withQuery: "swift")
}
.asDriver(onErrorJustReturn: [])
let searchRepos = self.searchQuerySubject
.asObservable()
.filter { $0.count > 2 }
.distinctUntilChanged()
.flatMapLatest { query in
networkingService.searchRepos(withQuery: query)
}
.asDriver(onErrorJustReturn: [])
let repos = Driver.merge(initialRepos, searchRepos)
self.repos = repos.map { $0.map { RepoViewModel(repo: $0)}}
}
}
- View Controller
private func bindViewModel() {
rx.viewWillAppear
.asObservable()
.bind(to: viewModel.viewWillAppearSubject)
.disposed(by: disposeBag)
viewModel.repos
.drive(tableView.rx.items(cellIdentifier: "Cell", cellType: UITableViewCell.self)) { row, element, cell in
cell.textLabel?.text = element.name
}
.disposed(by: disposeBag)
}
'iOS' 카테고리의 다른 글
Fastlane을 활용한 프로젝트 배포 자동화 구축 (0) | 2022.05.06 |
---|---|
AWS S3에 이미지 업로드 하기 (0) | 2022.03.25 |
Apple Enterprise 와 Apple Business Manager (0) | 2021.10.20 |
iOS Framework 만들어보기 (0) | 2021.07.10 |
스위프트 패키지 매니저(Swift Package Manager) 라이브러리 만들기 (0) | 2021.07.09 |