프론트엔드 UI 아키텍처 패턴 — MVC, MVP, MVVM, Flux
UI 아키텍처 패턴(MVC · MVP · MVVM · Flux)의 등장 계기와 장단점, 비교 분석을 담은 기록
UI 아키텍처 패턴(MVC · MVP · MVVM · Flux)의 등장 계기와 장단점, 비교 분석을 담은 기록
입력한 비밀번호는 비밀 댓글 열람, 수정, 삭제에 사용됩니다.
프론트엔드 프로젝트가 커지다 보면 어느 순간부터 "어떻게 구현하느냐"보다 "어디서 상태를 관리하고 어떻게 흐르게 할 것인가" 가 더 큰 문제가 됩니다. 기능은 동작하는데 상태 변경 흐름이 불분명해지면, 디버깅·확장·협업이 동시에 힘들어지기 시작합니다.
MVC, MVP, MVVM, Flux는 그 문제를 해결하려고 나온 패턴들인데, 각 패턴은 이전 패턴의 문제를 해결하려다 새로운 문제를 만들었고 그 반복이 지금의 흐름으로 이어집니다.
| 패턴 | 해결한 문제 | 생긴 문제 |
|---|---|---|
| MVC | 역할 분리 | Controller 비대화 |
| MVP | View 단순화, 테스트 용이 | Presenter 비대화 |
| MVVM | 생산성, 선언적 UI | 상태 추적 어려움 |
| Flux | 예측 가능한 상태 흐름 | 코드 복잡도 증가 |
1970년대 Smalltalk에서 처음 등장한 패턴입니다. Model(데이터·비즈니스 로직), View(화면 렌더링), Controller(사용자 입력 처리) 세 역할로 나눕니다.
Controller가 Model을 업데이트하고 View에 데이터를 넘기면, View가 그걸 받아서 스스로 처리합니다. 요청을 받아 처리하고 HTML을 반환하는 서버 사이드 구조와 잘 맞아서 Rails, Django, Spring MVC가 모두 이 방식을 씁니다.
문제는 Controller가 너무 많은 걸 알아야 한다는 점입니다. 사용자 입력, Model 상태, View에 뭘 넘길지까지 전부 Controller 안에 모입니다. 화면이 많아질수록 Controller가 비대해지고, View와 Model 사이 의존 관계도 복잡해집니다. 클라이언트 사이드로 넘어오면서 이 문제가 더 두드러졌습니다.
MVC의 Controller 문제를 해결하려고 나온 패턴입니다. Model(데이터·로직), View(렌더링만), Presenter(모든 UI 로직 담당) 구조입니다.
MVC와 결정적으로 다른 점은 View가 렌더링만 한다는 것입니다. MVC에서 View는 데이터를 받아 스스로 처리했지만, MVP에서 View는 사용자 입력을 Presenter에 전달하고 Presenter가 지시하는 대로만 그립니다. 로직이 전혀 없습니다.
View가 완전히 수동적이기 때문에 Presenter는 UI 없이도 단독으로 테스트할 수 있습니다. "입력이 들어왔을 때 Model이 올바르게 업데이트되는가"를 View 없이 검증할 수 있다는 뜻입니다. 명확한 테스트가 필요하거나 View를 철저히 단순하게 유지해야 할 때 지금도 유용한 패턴입니다.
Android 초기 개발에서 많이 쓰인 건 이 테스트 용이성 때문이었습니다. Android는 Activity/Fragment가 View와 Controller 역할을 동시에 하는 구조라 비대해지기 쉬웠는데, MVP로 Presenter를 분리하면 Activity는 렌더링만 담당하고 로직은 JUnit으로 빠르게 테스트할 수 있었습니다.
반면 iOS는 UIKit 시절 MVC를 공식 패턴으로 밀었고, 실제로는 Massive View Controller 문제가 심각해서 MVVM과 RxSwift 조합이 많이 쓰였습니다. 지금은 SwiftUI로 넘어오면서 MVVM이 사실상 표준이 됐습니다.
다만 모든 흐름이 Presenter에 집중되다 보니 결국 Presenter가 비대해지는 같은 문제가 반복됩니다.
Microsoft가 WPF(Windows Presentation Foundation, Windows 데스크탑 앱 UI 프레임워크)를 위해 고안한 패턴입니다. Model(데이터·로직), View(화면), ViewModel(View에 필요한 상태 보유) 구조입니다.
"Presenter가 View에게 직접 지시한다"는 방식 대신, 상태를 바꾸면 View가 알아서 반응하도록 만든 게 핵심입니다. ViewModel은 View에 필요한 상태를 들고 있고, View는 그 상태를 바인딩해서 자동으로 갱신됩니다.
"View를 어떻게 갱신할까"를 직접 제어하는 대신 "상태가 이렇게 바뀌어야 한다"고 선언하는 방식입니다. React, SwiftUI, Jetpack Compose가 모두 이 아이디어를 기반으로 합니다.
생산성이 높고 코드가 간결해지는 건 좋은데, 상태를 어디서든 바꿀 수 있어서 "이 상태가 왜 이 값이지?"를 추적하기가 어려워집니다. 컴포넌트가 많아지고 상태가 여러 레이어로 흘러다니기 시작하면 이 문제가 바로 드러납니다.
2014년 Facebook이 React와 함께 발표한 아키텍처입니다. 등장 배경이 꽤 구체적인데, 당시 Facebook 앱에서 알림 숫자가 계속 잘못 표시되는 버그가 있었습니다. 상태가 여러 곳에서 변경될 수 있는 구조라 원인을 찾기가 너무 어려웠고, 이를 해결하기 위해 상태 변경 경로를 단 하나로 고정하는 아이디어가 나왔습니다.
Action → Dispatcher → Store → View → (다시 Action)상태를 바꾸려면 반드시 Action을 dispatch해야 합니다. Store는 이 Action을 받아 상태를 갱신하고, View는 갱신된 상태를 구독합니다. View가 직접 상태를 건드리는 경로는 없습니다.
dispatch({ type: 'ADD_COMMENT', payload: text });모든 상태 변경이 Action이라는 하나의 문을 통하기 때문에 어떤 순서로 무슨 일이 일어났는지 추적하기 쉽습니다. Redux DevTools에서 타임트래블 디버깅이 되는 것도 이 구조 덕분입니다. 단점은 절차가 강제된다는 것입니다. 단순한 상태 변경 하나에도 Action 정의, Reducer 작성, Store 연결이 필요해서, 작은 프로젝트에서는 오버엔지니어링이 됩니다.
React는 MVVM도 Flux도 아닙니다. setState를 호출하면 UI가 자동으로 갱신되는 게 데이터 바인딩처럼 느껴지지만, 실제로 React가 하는 일은 다릅니다.
React는 상태 관리 방식을 강제하지 않습니다. useState로 MVVM처럼 쓸 수도 있고, Redux나 Zustand와 함께 Flux처럼 쓸 수도 있습니다. 렌더링 엔진과 상태 관리를 분리해뒀기 때문에 둘 다 가능합니다.
Vue는 달랐습니다. Vue 2까지는 Vuex를 공식 상태 관리 라이브러리로 밀었는데, Vuex는 Flux 아키텍처를 그대로 따릅니다. State, Mutation, Action, Getter로 이루어진 단방향 흐름이 강제됩니다. Vue 3로 오면서 Pinia가 공식 대안이 됐고, Pinia는 Vuex의 Mutation을 없애고 Action으로 통합해 훨씬 가벼워졌지만 단방향 흐름이라는 Flux의 핵심은 유지됩니다.
Zustand는 React에서 쓰는 라이브러리인데, 두 패턴 모두 가능합니다. 상태를 어디서든 직접 set할 수 있어서 MVVM처럼 쓸 수 있고, Action 패턴을 명시적으로 정의해서 Flux처럼 쓸 수도 있습니다. Redux보다 훨씬 가볍고 보일러플레이트가 적어서 "Flux가 필요한데 Redux는 무겁다"는 상황에서 많이 선택됩니다.
요즘은 상태를 하나의 패턴으로 통일하기보다 성격에 따라 나눠서 관리하는 방향이 일반적입니다.
| 상태 종류 | 예시 | 관리 방식 |
|---|---|---|
| 서버 상태 | API 응답 데이터 | TanStack Query |
| UI 상태 | 모달 열림·닫힘, 토글 | useState |
| 글로벌 상태 | 인증 정보, 테마 | Zustand / Redux |
서버 상태를 글로벌 store에 넣으면 캐싱 전략이 복잡해지고, UI 상태를 전역으로 올리면 불필요한 리렌더가 생깁니다. "모든 상태를 한 곳에서 관리한다"는 생각은 이미 잘 안 통하는 접근입니다.
단순히 UI 상태에 빠르게 반응하고 컴포넌트별 상태 관리가 핵심이라면 MVVM 방식이 더 자연스럽습니다. 작은 규모, 빠른 프로토타이핑, 선언적 UI가 중요한 상황이라면 useState 기반으로 시작하는 게 맞습니다. 반면 상태 변경 흐름을 명확히 통제해야 하거나 여러 컴포넌트가 공유하는 복잡한 전역 상태가 있다면 Flux 패턴이 적합합니다. 요약하면 단순한 상태면 MVVM, 복잡한 전역 상태면 Flux입니다.
실제로 많이 겪는 흐름은 useState로 시작하다가, 상태 위치를 파악하기 어려워지거나 버그 재현이 힘들어지는 시점에 Zustand를 부분적으로 도입하는 것입니다. 처음부터 정답을 고를 필요는 없고, 문제가 생기는 부분부터 전환하면 됩니다.
"지금 상태 흐름이 통제 가능한가"라는 질문 하나가 패턴 전환 시점을 알려줍니다.
function addComment(text) {
model.add(text); // Model 업데이트
view.render(model.comments); // View에 데이터 전달 → View가 렌더링 처리
}// View — 입력을 전달하고 렌더링만 수행
button.onClick(() => presenter.handleAdd(input.value));
showComments(comments) { list.innerHTML = render(comments); }
// Presenter — 모든 UI 로직을 담당
handleAdd(text) {
this.model.add(text);
this.view.showComments(this.model.comments); // View에게 직접 지시
}const [comments, setComments] = useState([]);
const handleAdd = (text) => {
setComments(prev => [...prev, text]); // 상태만 바꾸면 View가 알아서 반응
};UI 아키텍처 패턴(MVC · MVP · MVVM · Flux)의 등장 계기와 장단점, 비교 분석을 담은 기록
funda 프로젝트와 개인 포트폴리오에 Three.js로 캐릭터를 올리며 겪은 문제들을 적은 기록
베이직부터 그룹 프로젝트까지, CS 지식을 체득하고 AI 엔지니어링과 설계의 본질을 깨달으며 성장한 7개월간의 기록
온보딩 시스템 개선부터 인프라 모니터링, 실시간 소켓 통신, 그리고 3D 캐릭터 최적화까지 7주간의 그룹 프로젝트 완주 기록
아이디어 선정부터 시니어 피드백을 거쳐 MVP 구현까지, 그룹 프로젝트의 기틀을 다지며 겪은 시행착오와 기술적 도전의 기록
벡터의 길이를 1로 만드는 정규화의 본질을 게임 이동 로직과 블렌더의 Apply Scale 사례를 통해 분석했습니다.
6주간의 그룹 스프린트를 돌아보며 경험한 설계의 시행착오, 협업의 기술, 그리고 시니어 피드백을 통한 성장을 정리한 기록
10주 동안 진행된 부스트캠프 멤버십 학습 스프린트를 돌아보며 기술 학습, 설계 고민, 번아웃, 그리고 AI 활용까지 정리한 기록
네이버 부스트캠프 웹·모바일 10기 챌린지 과정 - 데일리 미션, CS 공부, 피어 피드백, AI 활용 방안
함수형 프로그래밍과 객체 지향 프로그래밍의 장단점, 리액트에서 함수형을 선택한 이유에 대한 탐구에 대한 기록