フロントエンド UI アーキテクチャパターン — MVC、MVP、MVVM、Flux
UI アーキテクチャパターン(MVC · MVP · MVVM · Flux)の登場経緯と長所短所、比較分析を記録
UI アーキテクチャパターン(MVC · MVP · MVVM · Flux)の登場経緯と長所短所、比較分析を記録
入力したパスワードは秘密コメントの閲覧、修正、削除に使われます。
フロントエンドプロジェクトが大きくなると、ある瞬間から「どのように実装するか」よりも**「どこで状態を管理し、どのように流れさせるか」**がより大きな問題になります。機能は動作していても、状態変更の流れが不明瞭になると、デバッグ・拡張・協力が同時に難しくなり始めます。
MVC、MVP、MVVM、Fluxはその問題を解決するために出現したパターンで、各パターンは以前のパターンの問題を解決しようとして新しい問題を生み出し、その繰り返しが今の流れに繋がっています。
| パターン | 解決した問題 | 発生した問題 |
|---|---|---|
| MVC | 役割分離 | コントローラー肥大化 |
| MVP | ビュー単純化、テスト容易 | プレゼンター肥大化 |
| MVVM | 生産性、宣言的UI | 状態追跡困難 |
| Flux | 予測可能な状態の流れ | コード複雑度増加 |
1970年代のSmalltalkで初めて登場したパターンです。(データ・ビジネスロジック)、(画面レンダリング)、(ユーザー入力処理)の3つの役割に分けます。
コントローラーがモデルをアップデートし、ビューにデータを渡すと、ビューがそれを受け取って自分で処理します。リクエストを受けて処理し、HTMLを返すサーバーサイドの構造とよく合うため、Rails、Django、Spring MVCはすべてこの方式を使っています。
問題は、コントローラーが非常に多くのことを知っている必要があるという点です。ユーザー入力、モデルの状態、ビューに何を渡すべきかすべてがコントローラー内に集中します。画面が増えるほどコントローラーが肥大し、ビューとモデルの間の依存関係も複雑になります。クライアントサイドに移行する際に、この問題がさらに顕著になりました。
MVCのコントローラー問題を解決するために出たパターンです。Model(データ・ロジック)、View(レンダリングのみ)、Presenter(すべてのUIロジック担当)の構造です。
MVCと決定的に異なる点は、Viewがレンダリングのみを行うということです。MVCではビューがデータを受け取って自分で処理していましたが、MVPではビューはユーザー入力をプレゼンターに伝達し、プレゼンターの指示に従うだけで描画します。ロジックは全くありません。
ビューが完全に受動的なので、プレゼンターはUIなしでも単独でテストできます。「入力が入ったときにモデルが正しく更新されるか」をViewなしで検証できるという意味です。明確なテストが必要なときや、ビューを徹底的に単純に保つ必要があるときに今でも有用なパターンです。
Android初期開発で多く使われたのはこのテストの容易さのためでした。AndroidはActivity/FragmentがViewとControllerの役割を同時に果たしやすい構造だったため肥大しがちでしたが、MVPでプレゼンターを分離すればActivityはレンダリングのみを担当し、ロジックはJUnitで迅速にテストできました。
一方、iOSはUIKit時代にMVCを公式パターンとして推し進めており、実際にはMassive View Controller問題が深刻だったためMVVMとRxSwiftの組み合わせが多く使われました。今はSwiftUIに移行するにつれ、MVVMが事実上の標準となっています。
ただし、すべての流れがプレゼンターに集中するため、結局はプレゼンターが肥大する同じ問題が繰り返されます。
MicrosoftがWPF(ウィンドウズ・プレゼンテーション・ファンデーション、ウィンドウズデスクトップアプリ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をディスパッチしなければなりません。StoreはこのActionを受け取り状態を更新し、Viewは更新された状態を購読します。Viewが直接状態を触る経路はありません。
dispatch({ type: 'ADD_COMMENT', payload: text });すべての状態変更がActionという一つの門を通るため、どの順序で何が起こったか追跡しやすいです。Redux DevToolsでタイムトラベルデバッグができるのもこの構造のおかげです。欠点は手続きを強制されることです。単純な状態変更一つにもAction定義、Reducer作成、Store接続が必要で、小さなプロジェクトではオーバーエンジニアリングになります。
ReactはMVVMでもフラックスでもありません。setStateを呼ぶとUIが自動で更新されるのがデータバインディングのように感じられますが、実際にReactが行っていることは異なります。
Reactは状態管理方式を強制しません。useStateでMVVMのように使うこともでき、ReduxやZustandと共にフラックスのように使うこともできます。レンダリングエンジンと状態管理を分離しているため両方が可能です。
Vueは違いました。Vue 2まではVuexを公式の状態管理ライブラリとして推していましたが、Vuexはフラックスアーキテクチャをそのまま踏襲しています。State、Mutation、Action、Getterで構成された単方向の流れが強制されます。Vue 3に移行しPiniaが公式代替となり、PiniaはVuexのMutationを除いてActionに統合し、はるかに軽くなりましたが単方向の流れというフラックスの核心は維持されています。
ZustandはReactで使うライブラリですが、両方のパターンが可能です。状態をどこからでも直接setすることができ、MVVMのように使うことができ、Actionパターンを明示的に定義してフラックスのように使うこともできます。Reduxよりもはるかに軽くボイラープレートが少ないため「フラックスが必要だがReduxは重い」という状況でよく選ばれます。
最近では、一つのパターンで統一するよりも性格に応じて分けて管理するのが一般的です。
| 状態の種類 | 例 | 管理方法 |
|---|---|---|
| サーバー状態 | APIレスポンスデータ | TanStack Query |
| UI状態 | モーダルの開閉、トグル | useState |
| グローバル状態 | 認証情報、テーマ | Zustand / Redux |
サーバー状態をグローバルストアに入れるとキャッシングストラテジーが複雑になり、UI状態をグローバルにすると不要なリレンダリングが発生します。「すべての状態を一箇所で管理する」という考え方は、既にうまくいかないアプローチです。
単純にUI状態に素早く反応し、コンポーネントごとの状態管理が重要であれば、MVVM方式が自然です。小規模、迅速なプロトタイピング、宣言的UIが重要な状況ならuseStateに基づいて始めるのが正しいです。一方、状態変更の流れを明確に制御する必要があるか、複数のコンポーネントが共有する複雑なグローバル状態がある場合は、Fluxパターンが適しています。要するに単純な状態ならMVVM、複雑なグローバル状態ならFluxです。
実際には、useStateで始めて、状態の位置を把握しづらくなったり、バグの再現が難しくなる時点でZustandを部分的に導入することが多いです。最初から正解を選ぶ必要はなく、問題が発生する部分から移行すれば良いです。
「現在の状態の流れは制御可能か」という一つの質問がパターン転換のタイミングを教えてくれます。
function addComment(text) {
model.add(text); // Modelアップデート
view.render(model.comments); // Viewにデータを渡す → Viewがレンダリング処理
}// ビュー — 入力を伝達しレンダリングのみ実行
button.onClick(() => presenter.handleAdd(input.value));
showComments(comments) { list.innerHTML = render(comments); }
// プレゼンター — すべての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にする「正規化」の本質を、ゲームの移動ロジックやBlenderの「Apply Scale」の事例から紐解きます。
6週間のグループスプリントを通じて学んだ設計の試行錯誤、協業のノウハウ、そしてシニアからのフィードバックによる成長の記録
Boostcampメンバーシップで経験した10週間の学習スプリントを振り返り、技術的な学び、設計の悩み、燃え尽き、そしてAI活用についてまとめた記録。
Boostcamp Challenge 期間の振り返り。毎日のミッション、CS学習、ピアフィードバック、チーム活動、そして AI と共に成長する学習方法についてまとめました。
関数型プログラミングとオブジェクト指向プログラミングの長所と短所、およびリアクトで関数型を選択した理由についての探求の記録