서론
MVVM의 핵심은 뷰와 비즈니스 로직을 분리하는 것입니다.
SwiftUI에서 뷰와 비즈니스 로직을 엮어버리면 다음과 같은 그림이 됩니다.
struct ContentView: View {
@State private var count = 1
var body: some View {
VStack {
Text("count : \(count)")
.onTapGesture {
/* 비즈니스 로직 */
count += 1
}
}
}
}
이런 코드는 다음과 같은 문제가 있습니다.
1. 테스트의 어려움
비즈니스 로직"만"을 테스트하기 어렵습니다.
2. 재사용 불가능
비즈니스 로직이 묶여있기 때문에 뷰를 유연하게 재활용할 수 없습니다.
똑같이 생긴 뷰를 만들려면 같은 코드를 복붙하고 로직부분만 수정하는 방식으로 해야 합니다. 코드가 많아지고 생산성이 떨어집니다.
MVVM 패턴과 다형성을 활용해서 이를 해결하는 예제를 진행해보겠습니다.
MVVM 패턴 예제

MVVM 아키텍처를 적용해서 이런 기능을 만들어보겠습니다.
- 모델 생성
struct Player {
let name: String
var age: Int
}
- ViewModel 생성
class ViewModel: ObservableObject {
@Published var player = Player(name: "Messi", age: 19)
func incrementAge() {
player.age += 1
}
func reduceAge() {
player.age -= 1
}
}
ObservableObject 프토로콜을 채택해줘야 ViewModel의 변경이 일어났을 때 값을 Publish할 수 있습니다. (참고: Combine 프레임워크에서 제공하는 기능입니다)
- 뷰 생성
struct ContentView: View {
@ObservedObject var viewModel = ViewModel()
var body: some View {
VStack {
Text("선수 이름 : \(viewModel.player.name)")
Text("선수 나이 : \(viewModel.player.age)")
HStack {
Button("나이 증가", action: {
viewModel.incrementAge()
}).padding(.trailing, 20)
Button("나이 감소", action: {
viewModel.reduceAge()
})
}
}
}
}
ObservableObject 프로토콜을 채택해서 값을 Publish하는 ViewModel의 객체에 @ObservedObject Property Wrapper를 달아줘야 합니다.
observable 객체(viewModel)의 published property(player)가 변경되면 SwiftUI는 object에 따라 뷰를 업데이트해줍니다. (공식 문서 참고)
이렇게 데이터 바인딩이 끝납니다.
처음에 이걸 해보고 @ObservedObject는 @State가 아닌데 SwiftUI에서 어떻게 뷰를 업데이트할까? 생각했습니다.
그래서 @State와 @ObservedObject를 보니 두 군데 모두 DynamicProperty라는 프로토콜을 채택하고 있었습니다.


DynamicProperty 프로토콜은 update()라는 함수를 구현하도록 하는데, 뷰의 external property를 변경할 수 있도록 합니다.
즉, DynamicProperty 프로토콜을 채택한 @State와 @ObservedObject는 값의 변경에 따라 뷰를 업데이트합니다.
서론에서 이야기한 어려움이 해결될까?
이렇게 하면 뷰와 비즈니스를 묶었을 때 발생하는 문제를 해결할 수 있을까요?
1. 테스트 관점
ViewModel의 객체만 생성하면 테스트를 수행할 수 있습니다.
UI객체를 만들지 않고도 테스트를 쉽게 할 수 있습니다.
2. 재사용 관점
뷰를 새로 만들 때마다 같은 ViewModel을 갖게 됩니다. 그리고 같은 ViewModel은 같은 동작을 하도록 합니다.
즉, 재사용이 불가능한 코드입니다.
다형성을 활용한 재사용 가능한 뷰 만들기
- 프로토콜 생성 및 채택
protocol ViewModelProtocol: ObservableObject {
var player: Player { get }
func incrementAge()
func reduceAge()
}
class ViewModel: ViewModelProtocol {
@Published var player = Player(name: "Messi", age: 19)
func incrementAge() {
player.age += 1
}
func reduceAge() {
player.age -= 1
}
}
ViewModelProtocol 프로토콜을 만들고 기존 ViewModel이 이를 채택하게 했습니다.
ObservableObject 프로토콜도 ViewModelProtocol로 옮겨서, ViewModelProtocol의 구현체들이 모두 Observed될 수 있도록 했습니다.
- 제약조건
struct ContentView<VM>: View where VM: ViewModelProtocol {
@ObservedObject var viewModel: VM
// 생략
}
뷰에 의존성을 주입하기 위해 ViewModelProtocol을 준수하는 클래스타입을 받도록 했습니다.
사실 viewModel: ViewModelProtocol 이런식으로 만들고 주입하게 하고 싶은데, 이 방식은 지원되지 않습니다 (...)
- 호출부
@main
struct BasicSwiftUIApp: App {
var body: some Scene {
WindowGroup {
ContentView<ViewModel>(viewModel: ViewModel())
}
}
}
적절한 타입과 객체를 넣어줍니다.
재사용해보기
이제 재사용이 가능해졌는지 확인해보고자 새로운 뷰모델 구현체를 만들어보겠습니다.
class NewViewModel: ViewModelProtocol {
@Published var player = Player(name: "OH SEUNGUN", age: 28)
func incrementAge() {
player.age += 2
}
func reduceAge() {
player.age -= 2
}
}
그리고 호출부에서도 이 구현체를 의존하도록 하기만 하면 끝입니다 !!
@main
struct BasicSwiftUIApp: App {
var body: some Scene {
WindowGroup {
// ContentView<ViewModel>(viewModel: ViewModel())
ContentView<NewViewModel>(viewModel: NewViewModel())
}
}
}
새로운 구현체를 만들었고, 호출부에서 ViewModel을 바꿔껴서 다르게 동작하는 뷰를 만들었습니다.
결론
뷰와 비즈니스 로직이 묶여있는 코드의 문제점(테스트, 재사용성)을 MVVM + 다형성을 활용해 해결했습니다.
사실, 이 예제를 진행한 것은 SwiftUI+ MVVM 사용을 왜 멈춰야 하는가? 라는 글을 읽었기 때문입니다.
이 글에 공감하기 위해서는 SwiftUI에서 MVVM을 사용해봐야 하기 때문에, 간단한 예제를 진행해봤습니다.
다음 포스팅에서는 앞서 작성한 예제를 가지고 MVVM 사용을 멈춰야 하는 이유에 대해 마음속 깊이 공감하는 시간을 가져보겠습니다.
참고
[iOS] MVVM 패턴에 대한 고찰 #1 (SwiftUI편)
MVVM 패턴 Swift 답게 설계하기 feat.SwiftUI
velog.io
https://green1229.tistory.com/267
SwiftUI에서 MVVM 사용을 멈춰야 하는가?
안녕하세요. 그린입니다🟢 이번 포스팅에서는 요즘 아니 예전부터 조금 말이 많이 나오고 있던 SwiftUI를 쓰면서 MVVM 아키텍쳐 사용을 지양하는 의견들이 많이 나오고 있습니다. 이에 한 개발자
green1229.tistory.com