
MVP와 MVVM: View를 똑똑하게 만들기
MVC에서 Controller가 너무 뚱뚱해졌습니다. Presenter/ViewModel로 분리하고, Data Binding으로 자동 업데이트하는 현대 프론트엔드의 핵심 패턴.

MVC에서 Controller가 너무 뚱뚱해졌습니다. Presenter/ViewModel로 분리하고, Data Binding으로 자동 업데이트하는 현대 프론트엔드의 핵심 패턴.
내 서버는 왜 걸핏하면 뻗을까? OS가 한정된 메모리를 쪼개 쓰는 처절한 사투. 단편화(Fragmentation)와의 전쟁.

미로를 탈출하는 두 가지 방법. 넓게 퍼져나갈 것인가(BFS), 한 우물만 팔 것인가(DFS). 최단 경로는 누가 찾을까?

이름부터 빠릅니다. 피벗(Pivot)을 기준으로 나누고 또 나누는 분할 정복 알고리즘. 왜 최악엔 느린데도 가장 많이 쓰일까요?

매번 3-Way Handshake 하느라 지쳤나요? 한 번 맺은 인연(TCP 연결)을 소중히 유지하는 법. HTTP 최적화의 기본.

React를 처음 배우던 날, useState를 보고 멘붕이 왔습니다.
const [count, setCount] = useState(0);
setCount(count + 1); // DOM이 자동으로 업데이트됨
jQuery로 3년 넘게 개발했던 저는 화면 업데이트는 이렇게 하는 거라고 배웠거든요.
$('#counter').text(count);
$('#status').html('Updated!');
$('#timestamp').text(new Date());
버튼 하나 눌러도 DOM 요소 10개씩 수동으로 업데이트해야 했습니다. 근데 React는 "변수만 바꿔라, 나머지는 내가 알아서 할게"라고 하는 겁니다. 처음엔 진짜 마법 같았어요.
그때 팀 리드가 "그게 MVVM이야. 요즘 프론트엔드는 다 그렇게 한다"고 하더라고요.
그래서 찾아봤습니다. MVC, MVP, MVVM... 다 Model-View가 들어가는데 정확히 뭐가 다른지 알고 싶었습니다. 특히 제가 3년간 쓰던 jQuery가 왜 "구식"이 된 건지, 그리고 왜 다들 React/Vue로 갈아타는지 정확히 이해하고 싶었어요.
MVC, MVP, MVVM... 다 Model-View로 시작하는데 뒤에 한 글자만 다릅니다. 처음엔 "그냥 이름만 다른 같은 패턴 아닌가?" 싶었어요. Model이랑 View는 다 있으니까요.
가장 헷갈렸던 게 ViewModel이었습니다. "View를 위한 Model"이라는데, 그럼 일반 Model이랑 뭐가 다른 건지 감이 안 왔어요. 둘 다 데이터 아닌가?
// Model
const user = { id: 1, name: 'John' };
// ViewModel
const userViewModel = { name: 'John' };
// 이게 뭐가 달라...?
"Data Binding으로 자동 동기화"라는 말도 추상적이었습니다. "동기화"가 뭘 의미하는 거지? 그냥 변수 대입하는 거랑 뭐가 다른데?
let name = 'Alice';
// 이게 Data Binding인가? 아닌가?
Presenter랑 ViewModel... 둘 다 View와 Model 사이에 있는데 뭐가 다른지 모르겠더라고요. 그림으로 보면:
MVP: View ←→ Presenter ←→ Model
MVVM: View ←→ ViewModel ←→ Model
똑같잖아요? 이름만 다른 거 아닌가 싶었습니다.
선배가 설명해준 비유가 머릿속에 확 들어왔습니다.
MVC: TV(Model)와 리모컨(Controller)이 완전히 분리되어 있습니다.
- 리모컨 버튼 누름 → Controller가 Model에 신호 보냄 → TV 화면 바뀜
- TV 화면만 보고 현재 채널 몰라요. 리모컨에도 채널 번호 안 나옵니다.
MVP: 리모컨(Presenter)이 더 똑똑해졌습니다.
- 버튼 누르면 Presenter가 View한테 "9번 표시해"라고 명령
- 그다음 Model(TV)한테 "9번 채널로 바꿔"라고 명령
- 여전히 수동입니다. Presenter가 하나하나 다 지시해줘야 해요.
MVVM: 리모컨에 작은 화면(ViewModel)이 붙어있습니다.
- 리모컨 화면에 9 누르기 → TV도 자동으로 9번 채널로 변경
- 이게 Data Binding입니다. 리모컨 화면과 TV가 "연결"되어 있어요.
- 한쪽만 바꾸면 다른 쪽도 자동으로 바뀝니다!
이 비유를 듣는 순간 "아, 그래서 React에서 setCount만 하면 화면이 바뀌는 거구나!"라고 이해했습니다.
레스토랑으로도 생각해봤습니다.
MVC (전통 레스토랑)이 비유로 "자동 동기화"의 의미를 정확히 이해했습니다.
iOS 개발자들 사이에서 유명한 농담이 있습니다.
"MVC가 뭐의 약자인지 아세요?" "Model-View-Controller?" "아니요, Massive View Controller요."
진짜 현실이었습니다. 제가 처음 iOS 프로젝트를 맡았을 때 본 ViewController.swift 파일은 2,800줄이었어요.
// ViewController.swift (2,800줄의 악몽)
class ViewController: UIViewController {
// ===== View 관련 =====
@IBOutlet var nameLabel: UILabel!
@IBOutlet var emailTextField: UITextField!
@IBOutlet var profileImageView: UIImageView!
@IBOutlet var saveButton: UIButton!
@IBOutlet var cancelButton: UIButton!
@IBOutlet var loadingSpinner: UIActivityIndicatorView!
// ===== 비즈니스 로직 =====
func validateEmail(_ email: String) -> Bool {
let regex = "^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}$"
// ... 50줄의 validation 로직
}
func validatePassword(_ password: String) -> Bool {
// ... 30줄
}
func hashPassword(_ password: String) -> String {
// ... 20줄
}
// ===== 네트워크 요청 =====
func fetchUser(userId: Int) {
let url = URL(string: "https://api.example.com/users/\(userId)")!
URLSession.shared.dataTask(with: url) { data, response, error in
// ... 100줄의 네트워크 처리
}.resume()
}
func uploadProfileImage(_ image: UIImage) {
// ... 150줄
}
// ===== UI 업데이트 =====
func updateUI() {
nameLabel.text = user.name
emailTextField.text = user.email
profileImageView.image = user.profileImage
saveButton.isEnabled = isFormValid
// ... 50개의 UI 요소 하나하나 수동 업데이트
}
// ===== Delegate 메서드들 =====
func textFieldDidChange(_ textField: UITextField) { ... }
func textFieldDidBeginEditing(_ textField: UITextField) { ... }
func textFieldShouldReturn(_ textField: UITextField) -> Bool { ... }
// ... 30개의 delegate 메서드
}
모든 것이 한 파일에 들어있었습니다.
MVC의 구조를 보면:
View → Controller ← Model
Controller가 View와 Model을 둘 다 알고 있습니다. 그래서 자연스럽게 모든 로직이 Controller로 몰립니다.
결국 Controller가 프로젝트의 쓰레기통이 됩니다.
// 이 함수를 어떻게 테스트하죠?
func saveUser() {
let name = nameTextField.text! // UI에 의존
let email = emailTextField.text!
if validateEmail(email) { // 비즈니스 로직
let user = User(name: name, email: email)
api.save(user) { result in // 네트워크
self.showAlert("Saved!") // 또 UI
}
}
}
이 함수를 테스트하려면:
Unit Test가 불가능합니다. UI, 비즈니스 로직, 네트워크가 전부 뒤섞여 있으니까요.
2. 재사용 불가능같은 User 정보를 다른 화면에도 표시하고 싶다?
// UserProfileViewController.swift
func displayUser() {
nameLabel.text = user.name
}
// UserDetailViewController.swift
func displayUser() {
nameLabel.text = user.name // 똑같은 코드 복붙
}
로직을 재사용할 방법이 없습니다. Controller는 UIViewController를 상속받아서 특정 화면에 강하게 결합되어 있으니까요.
3. 협업 지옥팀원A: ViewController.swift 수정 중 (라인 1~1000)
팀원B: ViewController.swift 수정 중 (라인 1500~2000)
나: ViewController.swift 수정 중 (라인 800~1200)
→ Git Merge Conflict 발생
→ 3시간 동안 충돌 해결...
한 파일에 모든 게 있으니 Git Conflict가 매일 발생했습니다.
이런 문제들을 해결하려고 나온 게 MVP입니다.
View와 Model을 완전히 분리하자. 둘 사이 소통은 무조건 Presenter를 통해서만.
View ←→ Presenter ←→ Model
중요한 규칙:
// Model
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
save() {
return fetch('/api/users', {
method: 'POST',
body: JSON.stringify({ name: this.name, email: this.email })
});
}
}
// View Interface (View가 구현해야 할 메서드 정의)
// Presenter는 이 인터페이스를 통해서만 View와 소통
interface UserView {
getNameInput(): string;
getEmailInput(): string;
displayUser(name: string, email: string): void;
showLoading(): void;
hideLoading(): void;
showError(message: string): void;
}
// Presenter
class UserPresenter {
constructor(view, model) {
this.view = view; // View 인터페이스만 알고 있음
this.model = model;
}
onSaveButtonClick() {
// View에서 데이터 가져오기
const name = this.view.getNameInput();
const email = this.view.getEmailInput();
// Validation (비즈니스 로직)
if (!this.validateEmail(email)) {
this.view.showError('Invalid email');
return;
}
// Model 업데이트
this.model.name = name;
this.model.email = email;
this.view.showLoading();
// 저장
this.model.save()
.then(() => {
this.view.hideLoading();
this.view.displayUser(name, email);
})
.catch(err => {
this.view.hideLoading();
this.view.showError(err.message);
});
}
validateEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
}
// View 구현 (HTML/DOM)
class UserViewImpl {
constructor(presenter) {
this.presenter = presenter;
this.nameInput = document.getElementById('name');
this.emailInput = document.getElementById('email');
this.saveBtn = document.getElementById('save');
this.loadingSpinner = document.getElementById('loading');
this.errorDiv = document.getElementById('error');
// 이벤트 연결
this.saveBtn.onclick = () => {
this.presenter.onSaveButtonClick();
};
}
getNameInput() {
return this.nameInput.value;
}
getEmailInput() {
return this.emailInput.value;
}
displayUser(name, email) {
this.nameInput.value = name;
this.emailInput.value = email;
}
showLoading() {
this.loadingSpinner.style.display = 'block';
}
hideLoading() {
this.loadingSpinner.style.display = 'none';
}
showError(message) {
this.errorDiv.textContent = message;
this.errorDiv.style.display = 'block';
}
}
// 사용
const model = new User('John', 'john@example.com');
const presenter = new UserPresenter(null, model);
const view = new UserViewImpl(presenter);
presenter.view = view;
// Mock View
class MockUserView {
constructor() {
this.displayedName = null;
this.displayedEmail = null;
this.errorMessage = null;
}
getNameInput() { return 'Alice'; }
getEmailInput() { return 'alice@example.com'; }
displayUser(name, email) {
this.displayedName = name;
this.displayedEmail = email;
}
showError(msg) { this.errorMessage = msg; }
showLoading() {}
hideLoading() {}
}
// 테스트
const mockView = new MockUserView();
const model = new User();
const presenter = new UserPresenter(mockView, model);
presenter.onSaveButtonClick();
// 검증
assert(mockView.displayedName === 'Alice');
assert(mockView.displayedEmail === 'alice@example.com');
실제 UI 없이 Presenter 로직을 테스트할 수 있습니다. Mock View만 만들면 되니까요.
2. View 교체 가능같은 Presenter를 웹과 모바일에서 재사용:
// 웹 View
class WebUserView { ... }
// 모바일 View
class MobileUserView { ... }
// 같은 Presenter 사용
const presenter = new UserPresenter(new WebUserView(), model);
// 또는
const presenter = new UserPresenter(new MobileUserView(), model);
Presenter 코드는 하나도 안 바뀝니다. View 인터페이스만 맞추면 되니까요.
3. 관심사 분리View : "버튼 눌렸어요", "이 텍스트 표시해주세요" (UI 담당)
Presenter : "이메일 검증", "저장 로직" (비즈니스 로직 담당)
Model : "데이터 저장", "API 호출" (데이터 담당)
각자 역할이 명확합니다.
// Presenter에서
this.view.displayUser(name, email);
this.view.updateTitle(`Hello, ${name}`);
this.view.updateBadge(name[0]);
this.view.updateStatus('Online');
데이터 바뀔 때마다 View 메서드를 일일이 호출해야 합니다. User 정보가 10곳에 표시된다면? 10개의 메서드를 다 호출해야 합니다.
"이거 자동으로 안 되나?" 싶었습니다. 그게 MVVM입니다.
MVVM의 게임 체인저는 Data Binding입니다.
View ⇄ ViewModel ⇄ Model
(자동 동기화)
ViewModel의 값이 바뀌면 → View가 자동으로 업데이트 View의 입력이 바뀌면 → ViewModel이 자동으로 업데이트
양방향 자동 동기화입니다.
"View를 위한 Model"입니다. 진짜 의미는:
// Model (서버에서 온 날것의 데이터)
const user = {
id: 1,
first_name: 'John',
last_name: 'Doe',
email: 'john.doe@example.com',
created_at: '2023-01-15T08:30:00Z',
role: 'admin',
is_active: true
};
// ViewModel (View에 필요한 형태로 가공)
const userViewModel = {
fullName: `${user.first_name} ${user.last_name}`, // 'John Doe'
email: user.email,
joinDate: new Date(user.created_at).toLocaleDateString(), // '2023. 1. 15.'
isAdmin: user.role === 'admin', // true
statusText: user.is_active ? '활성' : '비활성',
badgeColor: user.is_active ? 'green' : 'gray'
};
Model은 서버/DB 형태, ViewModel은 화면 표시 형태입니다.
Vue.js가 가장 직관적이라서 먼저 보겠습니다.
<template>
<div>
<input v-model="name" />
<p>Hello, {{ name }}!</p>
<button @click="changeName">Change</button>
</div>
</template>
<script>
export default {
data() {
return {
name: '' // ViewModel
};
},
methods: {
changeName() {
this.name = 'Alice'; // 이것만 바꾸면
}
}
};
</script>
무슨 일이 일어나나:
v-model="name": input과 name 변수를 양방향 바인딩
{{ name }}: name 변수를 화면에 표시
<p> 내용도 자동 업데이트this.name = 'Alice' 실행 시:
<input> 값이 'Alice'로 바뀜<p> 내용이 'Hello, Alice!'로 바뀜"어떻게 자동으로 바뀌지?"가 궁금했습니다. Vue 2의 구현을 단순화하면:
function makeReactive(obj, key, value) {
const listeners = []; // 이 값을 구독하는 함수들
Object.defineProperty(obj, key, {
get() {
// 누가 이 값을 읽는지 추적
if (currentListener) {
listeners.push(currentListener);
}
return value;
},
set(newValue) {
value = newValue;
// 값이 바뀌면 모든 구독자에게 알림
listeners.forEach(listener => listener());
}
});
}
// 사용 예시
const viewModel = {};
makeReactive(viewModel, 'name', '');
// View와 연결
let currentListener = () => {
document.getElementById('display').textContent = viewModel.name;
};
currentListener(); // 초기 렌더링
// 이제 값을 바꾸면
viewModel.name = 'Alice'; // setter 호출 → listeners 실행 → DOM 자동 업데이트
Observer Pattern을 사용합니다. 값이 바뀌면 구독자들에게 자동으로 알려주는 거죠.
React는 Vue와 방식이 다릅니다. Data Binding이 아니라 Re-rendering 방식입니다.
function UserProfile() {
const [user, setUser] = useState({
name: '',
email: ''
});
return (
<div>
<input
value={user.name}
onChange={e => setUser({ ...user, name: e.target.value })}
/>
<p>Hello, {user.name}!</p>
<button onClick={() => setUser({ ...user, name: 'Alice' })}>
Change
</button>
</div>
);
}
React의 방식:
setUser 호출Vue처럼 "정확히 어떤 값이 바뀌었는지" 추적하진 않습니다. 대신 컴포넌트 전체를 다시 그립니다 (Virtual DOM 덕분에 빠름).
const [count, setCount] = useState(0); // count = ViewModel
count: ViewModel (화면에 표시할 데이터)setCount: ViewModel 업데이트 → View 자동 업데이트{count}: ViewModel을 View에 바인딩// Before (jQuery)
$('#count').text(count);
$('#double').text(count * 2);
$('#status').text(count > 0 ? 'Positive' : 'Zero');
// After (React)
return (
<>
<div>{count}</div>
<div>{count * 2}</div>
<div>{count > 0 ? 'Positive' : 'Zero'}</div>
</>
);
ViewModel(count)만 바꾸면 나머지는 React가 알아서 처리합니다.
iOS의 SwiftUI는 MVVM을 언어 차원에서 지원합니다.
class UserViewModel: ObservableObject {
@Published var name: String = "" // @Published = "이 값 바뀌면 View 업데이트해"
@Published var email: String = ""
}
struct UserView: View {
@ObservedObject var viewModel: UserViewModel
var body: some View {
VStack {
TextField("Name", text: $viewModel.name) // $ = 양방향 바인딩
Text("Hello, \(viewModel.name)!")
}
}
}
@Published와 @ObservedObject가 Data Binding을 자동으로 처리합니다. viewModel.name이 바뀌면 View가 알아서 다시 그려집니다.
@Composable
fun UserScreen(viewModel: UserViewModel) {
val name by viewModel.name.observeAsState("")
Column {
TextField(
value = name,
onValueChange = { viewModel.updateName(it) }
)
Text("Hello, $name!")
}
}
class UserViewModel : ViewModel() {
private val _name = MutableLiveData("")
val name: LiveData<String> = _name
fun updateName(newName: String) {
_name.value = newName // 이것만 바꾸면 View 자동 업데이트
}
}
LiveData가 Observable입니다. observeAsState로 구독하면 값이 바뀔 때 자동 recomposition(리렌더링)됩니다.
View와 Model이 서로 전혀 모릅니다. Presenter만 압니다.
2. 테스트 용이// Presenter 테스트 (UI 없이)
const mockView = new MockView();
const presenter = new Presenter(mockView);
presenter.onButtonClick();
assert(mockView.displayedData === 'expected');
3. 명확한 제어 흐름
"어떤 메서드가 호출되는지" 명시적으로 보입니다.
presenter.onSaveClick();
→ this.view.showLoading();
→ this.model.save();
→ this.view.hideLoading();
→ this.view.displaySuccess();
// View 인터페이스
interface UserView {
showName(name: string): void;
showEmail(email: string): void;
showAge(age: number): void;
showAddress(address: string): void;
// ... 20개의 필드마다 메서드 정의
}
// Presenter
this.view.showName(user.name);
this.view.showEmail(user.email);
this.view.showAge(user.age);
this.view.showAddress(user.address);
// ... 20번 호출
2. Presenter도 뚱뚱해짐
결국 Presenter가 MVC의 Controller처럼 모든 로직을 다 갖게 됩니다.
3. 수동 동기화 비용값 하나 바뀔 때마다 View 메서드를 일일이 호출해야 합니다.
// MVP
presenter.updateUser(newUser);
view.showName(newUser.name);
view.showEmail(newUser.email);
view.showStatus(newUser.status);
// MVVM
viewModel.user = newUser; // 끝!
2. 코드 간결
// MVP (100줄)
class UserPresenter {
updateName(name) {
this.model.name = name;
this.view.showName(name);
this.view.showGreeting(`Hello, ${name}`);
this.view.showInitial(name[0]);
this.view.showNameLength(name.length);
}
}
// MVVM (10줄)
class UserViewModel {
name = ''; // 이거 하나만 바꾸면 모든 곳이 자동 업데이트
}
3. 선언형 UI
// 명령형 (MVP)
if (user.isAdmin) {
view.showAdminPanel();
view.hideUserPanel();
} else {
view.hideAdminPanel();
view.showUserPanel();
}
// 선언형 (MVVM)
<div>{user.isAdmin ? <AdminPanel /> : <UserPanel />}</div>
"어떻게(How)" 대신 "무엇을(What)" 표시할지만 선언합니다.
// 누가 이 값을 바꿨지?
console.log(viewModel.count); // 예상: 0, 실제: 42
자동 바인딩이라 추적이 어렵습니다. Vue Devtools, React Devtools 없이는 고생합니다.
2. 성능 오버헤드// 모든 값을 관찰(observe)하는 비용
const viewModel = reactive({
field1: '',
field2: '',
// ... 100개 필드
});
Observable을 만들고 유지하는 데 메모리와 CPU가 듭니다. 특히 Vue 2의 Object.defineProperty는 성능 이슈가 있었습니다 (Vue 3에서 Proxy로 개선).
// 초보자: "왜 자동으로 바뀌지? 무섭다..."
const [count, setCount] = useState(0);
setCount(count + 1); // 마법같음
"자동으로 바뀐다"는 게 편하지만, 처음엔 이해하기 어렵습니다.
4. 메모리 누수 위험// Observer를 제대로 정리 안 하면
useEffect(() => {
const subscription = observable.subscribe(data => {
setData(data);
});
// ⚠️ cleanup 안 하면 메모리 누수
return () => subscription.unsubscribe();
}, []);
| 항목 | MVC | MVP | MVVM |
|---|---|---|---|
| View-Model 관계 | View가 Model 직접 참조 가능 | Presenter가 중재 (완전 분리) | Data Binding으로 자동 동기화 |
| UI 업데이트 | Controller가 수동 업데이트 | Presenter가 수동 업데이트 | ViewModel 바뀌면 자동 업데이트 |
| 테스트 | 어려움 (View-Model 결합) | 쉬움 (Presenter 독립) | 쉬움 (ViewModel 독립) |
| 보일러플레이트 | 적음 | 매우 많음 | 중간 |
| 학습 곡선 | 낮음 | 중간 | 높음 (Reactivity 이해 필요) |
| 디버깅 | 쉬움 (명시적 흐름) | 쉬움 (명시적 흐름) | 어려움 (자동 바인딩) |
| 대표 프레임워크 | Ruby on Rails, Django | Android (초기), .NET (WPF) | React, Vue, Angular, SwiftUI, Compose |
| 현재 트렌드 | 백엔드에서 주로 사용 | 레거시 코드에서 발견 | 프론트엔드 표준 |
제가 2년 전에 맡았던 안드로이드 프로젝트는 MVP였습니다.
Before (MVP)// UserPresenter.kt
class UserPresenter(private val view: UserView) {
fun loadUser(userId: Int) {
view.showLoading()
repository.getUser(userId) { user ->
view.hideLoading()
view.showName(user.name)
view.showEmail(user.email)
view.showProfileImage(user.imageUrl)
view.showJoinDate(formatDate(user.createdAt))
view.showBadge(if (user.isPremium) "Premium" else "Free")
// ... 20개 필드
}
}
}
// UserActivity.kt
class UserActivity : AppCompatActivity(), UserView {
override fun showName(name: String) {
nameTextView.text = name
}
override fun showEmail(email: String) {
emailTextView.text = email
}
// ... 20개 메서드 구현
}
문제: 필드 하나 추가할 때마다
// UserViewModel.kt
class UserViewModel : ViewModel() {
private val _user = MutableLiveData<User>()
val user: LiveData<User> = _user
private val _loading = MutableLiveData<Boolean>()
val loading: LiveData<Boolean> = _loading
fun loadUser(userId: Int) {
_loading.value = true
repository.getUser(userId) { user ->
_loading.value = false
_user.value = user // 이것만!
}
}
}
// UserActivity.kt
class UserActivity : AppCompatActivity() {
private val viewModel: UserViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.user.observe(this) { user ->
nameTextView.text = user.name
emailTextView.text = user.email
profileImageView.load(user.imageUrl)
// 필요한 곳에서 user.xxx 사용
}
viewModel.loading.observe(this) { isLoading ->
progressBar.visibility = if (isLoading) View.VISIBLE else View.GONE
}
}
}
변화:
코드가 40% 줄었습니다.
더 극적이었던 건 jQuery → React 전환이었습니다.
Before (jQuery)// 800줄짜리 user.js
let currentUser = null;
$('#loadBtn').click(() => {
$('#loading').show();
$.get('/api/user/123', (user) => {
currentUser = user;
// 수동 DOM 업데이트 (50줄)
$('#name').text(user.name);
$('#email').text(user.email);
$('#greeting').text(`Hello, ${user.name}!`);
$('#initials').text(getInitials(user.name));
$('#memberSince').text(formatDate(user.createdAt));
$('#profileImage').attr('src', user.imageUrl);
$('#status').text(user.isOnline ? 'Online' : 'Offline');
$('#statusDot').css('background-color', user.isOnline ? 'green' : 'gray');
$('#postCount').text(user.postCount);
$('#followerCount').text(user.followerCount);
// ... 계속
$('#loading').hide();
});
});
$('#editBtn').click(() => {
$('#editModal').show();
$('#editName').val(currentUser.name);
$('#editEmail').val(currentUser.email);
});
$('#saveBtn').click(() => {
const newName = $('#editName').val();
const newEmail = $('#editEmail').val();
$.post('/api/user/123', { name: newName, email: newEmail }, () => {
currentUser.name = newName;
currentUser.email = newEmail;
// 또 수동 업데이트 (20줄)
$('#name').text(newName);
$('#email').text(newEmail);
$('#greeting').text(`Hello, ${newName}!`);
$('#initials').text(getInitials(newName));
// ...
$('#editModal').hide();
});
});
After (React)
function UserProfile() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
const [editMode, setEditMode] = useState(false);
const loadUser = async () => {
setLoading(true);
const data = await fetch('/api/user/123').then(r => r.json());
setUser(data);
setLoading(false);
};
const saveUser = async (updatedUser) => {
await fetch('/api/user/123', {
method: 'POST',
body: JSON.stringify(updatedUser)
});
setUser(updatedUser); // 이것만!
setEditMode(false);
};
if (loading) return <Spinner />;
if (!user) return <button onClick={loadUser}>Load</button>;
return (
<div>
<img src={user.imageUrl} />
<h1>{user.name}</h1>
<p>{user.email}</p>
<p>Hello, {user.name}!</p>
<p>{getInitials(user.name)}</p>
<p>Member since {formatDate(user.createdAt)}</p>
<StatusBadge isOnline={user.isOnline} />
<Stats posts={user.postCount} followers={user.followerCount} />
<button onClick={() => setEditMode(true)}>Edit</button>
{editMode && (
<EditModal user={user} onSave={saveUser} onCancel={() => setEditMode(false)} />
)}
</div>
);
}
변화:
처음엔 "이게 정말 작동해?" 싶었는데, 작동했습니다. 그것도 훨씬 안정적으로.
jQuery 시절엔 이런 버그가 흔했습니다:
// 버그: 한 곳만 업데이트 깜빡함
$('#saveBtn').click(() => {
currentUser.name = newName;
$('#name').text(newName); // ✅ 업데이트
$('#greeting').text(`Hello, ${newName}!`); // ✅ 업데이트
// ❌ $('#headerName')은 업데이트 안 함 → 버그!
});
같은 데이터를 표시하는 곳이 10군데인데, 9군데만 업데이트하면 버그입니다.
React는 이런 버그가 구조적으로 불가능합니다:
// user 상태 하나만 바꾸면
setUser({ ...user, name: newName });
// {user.name}이 들어간 모든 곳이 자동 업데이트됨
// 깜빡할 수가 없음
이게 MVVM의 진짜 가치라고 이해했습니다. 실수를 방지하는 구조.
사실 새 프로젝트에선 거의 안 씁니다. MVVM이 더 나으니까요.
2025년 기준 프론트엔드 표준입니다.
이 공부를 하면서 정확히 이해한 것들:
View와 Model이 서로 전혀 모릅니다. Presenter가 중재자 역할. 테스트하기 좋지만 코드가 많습니다.
Data Binding 덕분에 ViewModel만 바꾸면 View가 자동 업데이트. 수동 DOM 조작이 사라집니다.
const [state, setState] = useState(initialValue);
state: ViewModelsetState: ViewModel 업데이트 → 자동 리렌더링React, Vue, Angular, Svelte, SwiftUI, Compose... 전부 MVVM 기반입니다.
이게 MVVM의 본질입니다. 처음엔 마법 같았는데, 이제는 당연하게 느껴집니다.
jQuery 시절로 돌아가라고 하면 거부할 것 같아요. $('#element').text(value) 이런 거 일일이 하기 싫거든요.
비전공자 창업자 입장에서, MVP/MVVM을 이해하니까 프레임워크 문서가 이해되기 시작했습니다.
"왜 React는 이렇게 디자인됐지?" → "아, MVVM이니까" "왜 Vue는 v-model이 있지?" → "아, Data Binding이니까" "왜 SwiftUI는 @Published를 쓰지?" → "아, Observable 패턴이니까"
디자인 패턴은 "암기"가 아니라 "이해"하는 거라고 받아들였습니다. 이해하니까 새로운 프레임워크도 빨리 배울 수 있더라고요.