-
[UIKit] MVVM 패턴iOS 2026. 8. 9. 16:11
먼저 MVVM 패턴은 business logic과 presentation logic을 UI로부터 깔끔하게 분리해 주어 테스트, 유지-보수, 확장을 쉽게 하고 코드 재사용성과 개발자와 디자이너의 협업도 개선해 주기 위해 개발되었다고 한다.
1. MVVM 패턴?
1-1. MVVM 패턴의 구성 요소
MVVM 패턴에는 각자 역할이 다른 세 가지 핵심 구성 요소가 있다.
Model, View, ViewModel.
1-2. MVVM 패턴의 의존성
View는 ViewModel을 "알고", ViewModel은 Model을 "안다" (그 반대는 성립하지 않는다.)이런 과정을 통해 ViewModel이 View를 Model로부터 격리하고, Model이 View와 독립적으로 굴러가도록 한다.
다만, UIKit에서는 ViewModel이 UI 프레임워크를 몰라야 한다. (ViewModel 파일에 import UIKit이 없을 것!!)
1-3. MVVM 패턴의 장점
- 장점
- 기존 Model을 바꾸기 어렵거나 위험할 때, ViewModel이 어댑터 역할을 해서 Model 코드를 크게 건드리지 않게 해 준다.
- View 없이도 ViewModel과 Model에 대한 테스트가 가능하다.
- 로직을 ViewModel로 빼서 테스트한다.
- ViewModel을 그대로 두고 UI만 재디자인 가능하다.
- View 구현만 바꾸면 기존 ViewModel, MODEL은 그대로 동작하다. 같은 ViewModel로 다른 레이아웃, (프레임워크도 바꿔서) 동작할 수 있다.
- 디자이너와 개발자가 각자 컴포넌트를 독립적이고 동시에 작업이 가능하다.
→ 디자이너는 View를, 개발자는 ViewModel/Model을 독립적이고 동시에 작업 가능하다. - 데이터, 로직, 표현이 분리되어 코드 재사용성이 올라간다.
1-4. MVVM 패턴을 UIKit 관점에서 이해하는 방법 (objc.io)
MVC를 통해 MVVM으로 유도하는 것이 가장 좋은 방법이라고 한다.

- 전형적인 iOS 앱은 MVC다. View와 ViewController는 사실상 항상 같이 붙어다니기 때문에, 둘을 하나로 묶어서 ‘View’ 취급을 한다.
- 이렇게 묶어도 ViewController가 뚱뚱해진다는 문제점은 안 풀린다. ViewController 자체에 networking, 데이터 파싱, 포맷 변환 같은 presentation logic이 가득하기 때문이다.
- 따라서 이 presentation logic을 담을 새 객체가 필요하다. → ViewModel을 View와 Model 사이에 두면 해결된다.

그러므로 MVVM 패턴은 View와 ViewController를 연결하고, presentation logic을 ViewModel로 빼낸 MVC의 확장판이라고 볼 수 있다.
2. 구성 요소
2-1. View
View는 화면에 보이는 것의 구조, 레이아웃, 외관을 담당한다. 그러니까 ‘이것’만 담당하면 된다는 것이다.
판단 없이 표시만 한다.
판단과 로직은 전부 ViewModel에 델리게이트.하라는 이야기다. 그러면 이 ViewModel이 가져다 주는 결과만 반영하면 된다.
ViewModel이 판단할 테니, UIKit에서는 View나 ViewController가 판단하지 않도록 한다는 이야기다.
// 판단이 View에 샘 (이럼 안 됨) button.isEnabled = viewModel.count < 10 // ViewModel이 판단 -> View는 반영만 viewModel.onEnabledChange = { [weak self] enabled in self?.button.isEnabled = enabled2-2. ViewModel
View가 binding할 속성과 명령을 노출하고 상태 변화를 알린다. 어떤 기능을 제공할지는 ViewModel이 정하고, 어떻게 보여 줄지는 View가 정한다.
- presentation logic을 담당한다.
- 네트워킹, 데이터 파싱, 포맷 변환, model 변경 감지
- UI 타입을 노출하지 않는다.
- ex) 색 매핑은 View가 하고 색은 Color가 아니라 enum LoadState 같은 순수 값으로 노출한다.
- 입출력 작업은 main thread를 비동기로 처리한다.
- 오래 걸리는 일은 비동기로 돌려서 화면이 멈추지 않도록 처리한다.
// 동기 — 데이터 올 때까지 화면 멈춤 func load() { let data = service.fetchSync() // 여기서 3초 멈추면 화면도 3초 얼음 self.items = data } // 비동기 — 기다리는 동안 화면은 멀쩡 func load() { Task { let data = try await service.fetch() // 기다려도 UI 자유로움 self.items = data // 결과 오면 갱신 } }-
- ViewModel이 여러 개의 Model을 조합해 쓴다
ㅡ protocol 기반 설계 방법
ViewModel을 구체 타입이 아니라 protocol로 먼저 정의하면, 구현체를 자유롭게 갈아끼울 수 있어 테스트, 확장에 유리하다.
// 1. protocol protocol GreetingViewModel { var name: String { get } } // 2. 구현체 두 개 final class RealGreeting: GreetingViewModel { // 진짜 버전 var name = "주희" } final class FakeGreeting: GreetingViewModel { // 가짜 버전 (테스트용) var name = "테스트" } // 3. View는 protocol만 앎. final class GreetingVC: UIViewController { let label = UILabel() var viewModel: GreetingViewModel? // 진짜? 가짜? 몰라도 됨 func showName() { label.text = viewModel?.name } } // 4. 갈아끼우기 let vc = GreetingVC() vc.viewModel = RealGreeting() // "주희" 표시 vc.showName() vc.viewModel = FakeGreeting() // "테스트" 표시 — VC 코드는 그대로 vc.showName()2-3. Model
data model + business logic, validation logic을 포함한다.
3. View와 ViewModel 연결하기
3-1. 연결 개요
View가 자신의 ViewModel을 갖고, 그 출력에 반응하게 만드는 것
- UIKit에서는 ViewController에서 ViewModel 프로퍼티를 두고, 할당되는 순간 UI를 채운다.
- 이때 UI를 채우는 건 viewDidLoad와 didSet 양쪽에서 불러야 한다. ViewModel이 viewDidLoad 전에 주입될 수도, 후에 주입될 수도 있기 때문.
3-2. 구성 방식 비교
방식 설명 장점 View 먼저 View가 자신이 의존하는 ViewModel을 연결 테스트 용이, 구조 파악 쉬움, iOS navigation과 잘 맞음. ViewModel 먼저 ViewModel 중심 구성 UI 추상화, 논리 구조 집중(대신 복잡) 3-2-2. 주입 방식 비교
1. 외부 주입
부모가 ViewModel을 만들어 자식에게 주입. 의존성을 명시적으로 주입할 수 있어 테스트에 유리하다.
final class GreetingVC: UIViewController { var viewModel: GreetingViewModel? // 빈 자리만 둠 } // 부모 에서 만들어 주입 let vc = GreetingVC() vc.viewModel = RealGreeting() // 여기서 넣어줌2. 내부 생성
ViewController가 자기 안에서 직접 만든다.
final class GreetingVC: UIViewController { let viewModel = RealGreeting() // 직접 만듦. } let vc = GreetingVC() // 그냥 만들면 끝4. 데이터 동기화
4-1. Binding
Model 변경 → ViewModel이 감지 → ViewModel 프로퍼티 갱신 → View가 반영
→ 그런데 UIKit에는 자동 바인딩이 없다!!!!
→ 직접 구현 필요하다.
→ 옛날에는 KVO, Dynamic<T> 등 사용…
→ 현재는… Combine 사용
5. UI 상호작용
사용자 액션에 대한 처리 책임은 모두 ViewModel에 있다!! ViewController에는 로직을 두지 않는다.
- UIKit에서는 선언적으로 command binding이 없어서 addAction/addTarget
- Combinde으로 해결 가능
- ViewController는 호출만 하고 실제 로직은 ViewModel이 수행한다.
// View -> ViewModel: 동작 요청 button.addAction(UIAction { [weak self] _ in self?.viewModel.togglePause() // 판단과 처리는 ViewModel이 함!!! }, for: .touchUpInside)6. UIKit에서의 MVVM
6-1. UIKit에서의 MVVM의 핵심 차이
UIKit에서는 자동 data binding이 없다. (스유에는 body 재실행으로 가능)
따라서 binding을 직접 구현해야 한다.
6-2. binding
Closure Dynamic<T>의존성 없음. 속성 많으면 closure가 너무 많아짐. 가벼움 Combine @Published변경을 자동으로 내보냄. 반응형 Delegate 연속 상태보다는 단발성에 적합. 보조용 - Combine 예시
import Combine // ViewModel final class SwitchViewModel { @Published var isOn = false // 이 값이 바뀌면 자동으로 알림 } // ViewController final class SwitchVC: UIViewController { private let viewModel = SwitchViewModel() private let label = UILabel() private var cancellables = Set<AnyCancellable>() private func bind() { viewModel.$isOn // isOn을 구독 .sink { [weak self] on in self?.label.text = on ? "켜짐" : "꺼짐" // 바뀔 때마다 자동 실행 } .store(in: &cancellables) } }6-3. ViewModel은 꼭 import UIKit 없어야 한다!!
ViewModel이 UIKit/CoreLocation을 import하지 않게 해서 재사용성을 높여야 한다.
- 장점