
// MARK: - Model
struct User {
let id: Int
let name: String
let email: String
}
// MARK: - ViewModel
class UserViewModel {
// Output
@Published private(set) var userName: String = ""
@Published private(set) var userEmail: String = ""
@Published private(set) var isLoading: Bool = false
// Input
func fetchUserData() {
isLoading = true
// API 호출을 시뮬레이션
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
guard let self = self else { return }
// 실제로는 네트워크 요청을 통해 데이터를 받아옴
let user = User(id: 1, name: "홍길동", email: "hong@example.com")
self.updateUserInfo(with: user)
self.isLoading = false
}
}
private func updateUserInfo(with user: User) {
userName = user.name
userEmail = user.email
}
}
// MARK: - View
class UserViewController: UIViewController {
private let viewModel = UserViewModel()
private var cancellables = Set<AnyCancellable>()
private let nameLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let emailLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let loadingIndicator: UIActivityIndicatorView = {
let indicator = UIActivityIndicatorView(style: .medium)
indicator.translatesAutoresizingMaskIntoConstraints = false
return indicator
}()
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
bindViewModel()
viewModel.fetchUserData()
}
private func setupUI() {
view.backgroundColor = .white
view.addSubview(nameLabel)
view.addSubview(emailLabel)
view.addSubview(loadingIndicator)
NSLayoutConstraint.activate([
nameLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
nameLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -20),
emailLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
emailLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 8),
loadingIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
loadingIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
}
private func bindViewModel() {
// Combine을 사용한 데이터 바인딩
viewModel.$userName
.sink { [weak self] name in
self?.nameLabel.text = name
}
.store(in: &cancellables)
viewModel.$userEmail
.sink { [weak self] email in
self?.emailLabel.text = email
}
.store(in: &cancellables)
viewModel.$isLoading
.sink { [weak self] isLoading in
if isLoading {
self?.loadingIndicator.startAnimating()
} else {
self?.loadingIndicator.stopAnimating()
}
}
.store(in: &cancellables)
}
}
MVVM 패턴의 각 구성요소를 하나씩 살펴보자.
Model 계층
User 구조체로 데이터 모델을 정의했다.
간단히 id, name, email 속성만 가지고 있음
ViewModel 계층
UserViewModel 클래스가 비즈니스 로직을 담당한다.
@Published 프로퍼티래퍼를 사용해서 데이터 변경을 View에 자동으로 알려줌
Input으로 fetchUserData() 메서드를 제공
Output으로 userName, userEmail, isLoading 상태값을 제공
View 계층
UserViewController가 UI를 담당
ViewModel의 데이터를 observing하면서 UI 업데이트
Combine 프레임워크로 데이터 바인딩 구현
SwiftUI에서는 onChange로 감지해도 됨
주요 특징들:
View는 ViewModel만 참조하고 Model을 직접 알지 못함
데이터 흐름이 단방향임 (ViewModel → View)
ViewModel은 View에 대해 전혀 모름 (의존성 분리)
테스트하기 쉬운 구조
실제 사용시에는 더 복잡한 비즈니스 로직이나 네트워크 통신 등이 추가될 수 있어. 하지만 이 기본 구조를 잘 이해하면 확장하기 쉽다.
예를 들어 사용자 정보를 수정하는 기능을 추가하고 싶다면:
// ViewModel에 Input 추가
func updateUserName(_ name: String) {
userName = name
// API 호출 로직
}
// View에서 호출
nameTextField.addTarget(self, action: #selector(nameChanged), for: .editingChanged)
@objc private func nameChanged(_ sender: UITextField) {
viewModel.updateUserName(sender.text ?? "")
}
이런 식으로 기능을 쉽게 확장할 수 있.