Next.jsは「近道」か「迷路」か。迷子の初学者がWebの歴史を遡り、Next.jsの本質が見えてきた話
1. はじめに
この記事を書いた理由
僕は現在、Next.js を使ってポートフォリオアプリケーションを開発しています。しかし、ある時ふと気づきました。
「なぜ Next.js を使っているのか、自分の言葉で説明できない」
チュートリアルをこなし、公式ドキュメントを読み、アプリを作る。基本的なことはある程度できるようになりましたが、「なぜこの技術が必要なのか」 という本質を理解していませんでした。
表面的な使い方だけではなく、技術の本質を理解したい。そう思った僕は Web アプリケーションの歴史を調べ始めました。
Web アプリケーションの変遷との出会い
学習を進める中で、Web アプリケーションの変遷について学べる資料に出会いました。
こちらの資料を読んだ理由は React の基礎を学びたかったからですが、Web アプリ開発の移り変わりについて非常に丁寧に書いてくださっており、そちらにも興味が湧きました。
そして以下の流れが見えてきました。
- 2000 年代のMVC
- 2010 年代のSPA
- 現在のNext.js
この流れを知った時、僕は思いました。
実際にアプリを MVC モデルと最新フロントエンド技術である Next.js で作り比べ、検証することで
肌で技術の移り変わりを体感できるかもしれない。
また、ある仮説も浮かびました。
仮説: 技術は問題や課題解決の結果として生まれる
「流行っている技術、新しい技術は、何かの問題を解決した結果として誕生し、普及したのではないか?」
もしそうなら
- MVC には何か解決できない問題が生まれた
- SPA がそれを解決したが、また別の問題が生まれた
- Next.js はその問題を解決するために生まれた
この仮説を検証するために、僕は実際に手を動かすことにしました。
検証方法
同じブログアプリケーションを、MVC モデルと Next.js の両方で実装し、具体的な違いを比較します。
僕と同じ初学者、Next.js を何となく使っている、技術選択の理由を知りたい方の参考になれば幸いです。
2. 比較検証のアプリケーション紹介
MVC ブログアプリの概要
まず、2000 年代の web アプリケーションを体験するために、MVC モデルでブログアプリを作成しました。
このアプリケーションは、記事の投稿・閲覧・編集・削除ができるシンプルなブログです。あえて jQuery や EJS といった古い技術を使うことで、当時の開発スタイルを再現しています。

技術スタック
- バックエンド: Node.js (Express)
- テンプレートエンジン: EJS
- データベース: PostgreSQL
- フロントエンド: jQuery
実装した機能
- 記事の CRUD 操作(作成・閲覧・編集・削除)
アーキテクチャの特徴
MVC モデル最大の特徴は関心の分離(Separation of Concerns) です。
- Model: データとビジネスロジック (データの取得)
- View: 表示(UI)
- Controller: 制御(リクエストの処理と Model と View の橋渡し)
この3つの役割を分離することで、保守性・再利用性・拡張性・開発効率を高めることができます。
実際のコード
MVC で記事一覧を表示する処理は以下のように分離されています。
Model (Post.js) - データ取得に専念
static async findAll() {
try {
const result = await pool.query(
'SELECT * FROM posts ORDER BY created_at DESC'
);
return result.rows;
} catch (err) {
throw err;
}
}
Controller (postsController.js) - 制御に専念
exports.index = async (req, res) => {
const posts = await Post.findAll(); // Modelに「データくれ」と依頼
res.render('posts/index', {
// Viewに「これ表示して」と渡す
title: '記事一覧',
posts: posts,
});
};
View (index.ejs) - 表示に専念
<% posts.forEach((post) => { %>
<article class="post-card">
<h2><a href="/posts/<%= post.id %>"><%= post.title %></a></h2>
<p><%= post.content.substring(0, 100) %>...</p>
</article>
<% }); %>
それぞれが独立しているため、例えば
- データベースを変更したい → Model だけ修正
- 表示デザインを変更したい → View だけ修正
- ページネーションを追加したい → Controller だけ修正
このように影響範囲を限定できるのが MVC の強みです。
3. MVC モデルの課題
MVC は保守性・拡張性に優れたアーキテクチャですが、モダンな Web アプリケーションに求められる要件を満たすには限界がありました。
1. インタラクティブな操作が困難
MVC の基本は「リクエストを送り、新しい HTML を受け取る」という一往復の動作です。
そのため、ボタンを押下した瞬間に UI を変化させるような処理が苦手です。
例えば、削除ボタンを押下した際に「サーバーレスポンスを待たずに、まず画面から消す」といった楽観的 UI を実装しようとすると、MVC では複雑になります。
また EJS などのテンプレートエンジンは「サーバーが HTML を作るための道具」であり、ブラウザ上で状態を保持する機能がありません。
<form action="/posts/<%= post.id %>?_method=DELETE" method="POST">
<button type="submit">削除</button>
</form>
現状の MVC では悲観的 UI
- ボタンを押す
- サーバーに POST リクエスト
- サーバーが DB 変更後、HTML 全体を再生成
- ページ全体がリロード
- やっと記事が消える
jQuery による命令的な DOM 操作は、HTML の構造が変わるたびに JavaScript 側も修正が必要になり、バグの温床になります。
EJS などのテンプレートエンジンは状態を持てないため、クライアント側でのデータ管理が困難です。
2. フロントエンド/バックエンドの分離による課題
さらに、当時の開発ではバックエンドとフロントエンドが完全に分離されていました。
バックエンド(サーバー) EJS で HTML 構造を生成
<article class="post-card">
<h2><%= post.title %></h2>
</article>
フロントエンド(ブラウザ) jQuery で DOM 操作
$('.post-card').fadeOut();
この分離により、おそらく以下のような課題があったと考えられます。
- HTML のクラス名やレイアウト変更のたび、バックエンド/フロントエンド間でコミュニケーションが必要
- セレクタの不一致によるバグが発生しやすい
- UI とロジックが別ファイルで管理され、保守が困難
一方現代の React などの宣言的 UIでは
function PostCard({ post, onDelete }) {
return (
<article>
<h2>{post.title}</h2>
<button onClick={() => onDelete(post.id)}>削除</button>
</article>
);
}
UI とロジックが同じコンポーネント内に存在するため、変更の影響範囲が明確で保守性が向上します。
3. ページ全体のリロード
MVC では、画面遷移のたびにページ全体がリロードされます。
例えば、記事一覧から詳細画面へ移動する場合
<a href="/posts/<%= post.id %>"><%= post.title %></a>
この遷移で起こること
- サーバーにリクエスト
- ヘッダー・フッター・ナビゲーションを含む全ての HTMLを再生成
- ブラウザが全ての HTML を再描画
- CSS や JavaScript も再読み込み
問題点
- 画面のチラつき(一瞬白くなる)
- 変わらない部分(ヘッダー/フッター)も毎回再描画
- 不要な通信とレンダリングコストが発生
モダンなフレームワーク(React/Next.js)では
- 変更された部分だけ再レンダリング
- ヘッダー・フッターはそのまま維持
- スムーズな画面遷移(チラつきなし)
- 最小限の通信で高速表示
この違いが、ユーザー体験の向上に直結します。
4. SPA の登場とその課題
SPA が解決した問題
2010 年代、React・Vue・Angular などの登場により、SPA(Single Page Application) が主流になりました。
SPA は、MVC モデルの3つの課題をどのように解決したのでしょうか?
1. クライアント側での状態管理 → 楽観的 UI の実現
MVC では不可能だった楽観的 UI が、SPA では簡単に実現できるようになりました。
//Reactの例
const [posts, setPosts] = useState([]);
const handleDelete = (id) => {
setPosts(posts.filter((post) => post.id !== id)); //①即座にUI更新
api.delete(`/posts/${id}`); //②バックグラウンドでサーバー通信
};
MVC との違い
- MVC: サーバーの DB 更新を待ってから、ページ全体をリロードして表示
- SPA: ブラウザ上の状態を先に更新し、サーバー通信は後回し
これにより、ユーザーは待たされることがないので、ユーザー体験が向上します。
2. 宣言的 UI → UI とロジックの凝集
バックエンド(EJS)とフロントエンド(jQuery)の分離による問題も解決されました。
function PostCard({ post, onDelete }) {
return (
<div>
{post.title}
<button onClick={() => onDelete(post.id)}>削除</button>
</div>
);
}
宣言的 UI とは
- 命令的(jQuery): 「どのように」実現するかを手順で記述
- 宣言的(React): 「どうあるべきか」を宣言するだけ
MVC との違い
- MVC: EJS で HTML 生成 → jQuery で操作(2 箇所を修正)
- SPA: 1つのコンポーネントに完結(1 箇所のみ修正)
- UI とロジックが同じコンポーネント内に存在
- クラス名の変更がすぐに分かる
- バックエンド/フロントエンド間のコミュニケーションコスト削減
3. 部分的な再レンダリング → スムーズな画面遷移
ページ全体のリロードも不要になりました。
function App() {
return (
<>
<Header /> {/* 変更なし→再レンダリングされない */}
<PostList props={posts} /> {/* ここだけ更新 */}
<Footer /> {/* 変更なし→再レンダリングされない */}
</>
);
}
React の仮想 DOM による差分検出
React は内部で仮想 DOM として状態を保持しており、状態が変更された際に差分を計算します。変更があった部分だけを実際の DOM に反映するため、無駄な再レンダリングを避けることができます。
MVC との違い
-
MVC:
/posts→/posts/1へ遷移時、サーバーに全 HTML を再リクエスト - SPA: 初回アクセス時に JavaScript をダウンロード済み。画面遷移はブラウザ上で完結
クライアントサイドルーティング
SPA では、URL とコンポーネントがマッピングされています。(React Router など)。サーバーへのリクエストなしに、ブラウザ上でルーティングを処理できます。
- 変更部分だけ更新
- ヘッダー・フッターはそのまま
- チラつきのないスムーズな画面遷移
しかし、新たな問題も....
SPA は MVC の課題を解決しましたが、新たな問題も生まれました。
1. SEO の問題
SPA では、サーバーからほぼ空の HTMLが返ってきます。
<!DOCTYPE html>
<html>
<head>
<title>My Blog</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
JavaScript が実行されて初めて<div id="root">内にコンテンツが生成されます。
問題点
検索エンジンのクローラーがこの HTML を見ても
- 記事のタイトルが見えない
- 記事の本文が見えない
- 記事ごとのメタタグ(description)が見えない
MVC との違い
- MVC サーバーが完成した HTML を返す → クローラーが内容を認識
- SPA 空の HTML を返す → クローラーが内容を認識できない
結果
- 検索結果に表示されない
- 検索順位が下がる
- SNS でシェアされても適切なプレビューが表示されない
このため、SPA は
- 社内ツール向き。管理画面など(SEO 不要)
- 企業サイト、ブログ、EC サイトなどは不向き。(SEO 必須)
という使い分けが必要になりました。
2. 初回ロードの遅さ
SPA では、初回アクセス時にすべての JavaScriptをダウンロードする必要があります。
SPA の初回ロードの流れ:
ユーザーがアクセス
↓
① 空のHTMLが届く
↓
② bundle.jsをダウンロード
↓
③ JavaScriptを実行・解析
↓
④ APIでデータ取得
↓
やっと画面表示
MVC との比較
- MVC: 完成した HTML が届く → 即表示
- SPA: HTML → JS → 実行 → API → 表示
問題点:
bundle.js には、アプリ全体のコードが含まれています:
- トップページのコード
- 記事詳細ページのコード
- 記事編集ページのコード
- すべてのライブラリ(React、React Router 等)
しかし、トップページを見るだけなら、他のページのコードは不要です。
結果
- 初回ロードが遅い
- ユーザーが待たされる
- 離脱率の増加
3. JavaScript への依存
SPA は、JavaScript がなければ何も表示されません。
JavaScript が動かない状況
- JavaScript の読み込みエラー(ネットワーク問題)
- 古いブラウザ(非対応)
- ユーザーが意図的に無効化
MVC との違い
- MVC: サーバーが完成した HTML を返す → JavaScript 不要でも表示可能
- SPA: 空の HTML を返す → JavaScript 必須
アクセシビリティへの影響
- スクリーンリーダーでの利用が困難
- 低スペック端末での表示が遅い
- 通信環境が悪い場所でアクセスできない
Web の基本原則であるプログレッシブエンハンスメント(基本は HTML で提供し、JS で拡張)から外れており、これは許容できない問題でした。
5. Next.js での再実装
Next.js ブログアプリの概要
MVC ブログアプリと同じ機能を、Next.js で再実装しました。

技術スタック
- フレームワーク: Next.js 15 (App Router)
- 言語: TypeScript
- データベース: Supabase (PostgreSQL)
- スタイリング: Tailwind CSS
実装した機能
- 記事の CRUD 操作(作成・閲覧・編集・削除)
アーキテクチャの特徴
Next.js のアーキテクチャは、MVC とは根本的に異なります。
1. ファイルベースルーティング
MVC では、ルーティングを明示的に定義する必要がありました。
//routes/posts.js
router.get('/', postController.index);
router.get('/:id', postController.show);
router.post('/', postController.create);
Next.js ではファイル構造がそのままルーティングになります。
app/
posts/
page.tsx →/posts
new/
page.tsx →/posts/new
[id]/
page.tsx →/posts/:id
edit/
page.tsx →/posts/:id/edit
- ルーティング定義が不要
- ファイル構造 = URL 構造で直感的
- しかし全体像が見えにくい
2. Server Component と Client Component の使い分け
Next.js では、コンポーネントをサーバーとクライアントで使い分けます。
Server Component
- サーバーで実行される
- データーベースに直接アクセス可能
- JavaScript がブラウザに送られない
Client Component
- ブラウザで実行される
-
useState、useEffectなどのフックが使える
3. 関心の分離の再定義
MVC の時代
技術で分離
- Model (データ)
- View (表示)
- Controller (制御)
Next.js の時代
機能で分離
- posts/page.tsx (記事一覧に関するすべて)
- posts/[id]/page.tsx (記事詳細に関するすべて)
1 つのファイルで
- データ取得 (Model 的)
- UI 表示 (View 的)
- ロジック (Controller 的)
が完結します。
SPA の問題を Next.js がどう解決したか
SPA の3つの課題を、Next.js はどのように解決したのでしょうか?
1. SEO の問題 → Server Side Rendering(SSR)
SPA の問題
<!DOCTYPE html>
<html>
<head>
<title>My Blog</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
検索エンジンのクローラーには、ほぼ空の HTML しか見えません。
Next.js の解決策
Server Component は、サーバー側で完成した HTML を生成します。
//app/posts/page.tsx(Server Component)
export default async function PostsPage() {
const supabase = await createClient();
const { data: posts } = await supabase.from('posts').select('title, content');
return (
{posts?.map(post => (
<article>
<h2>{post.title}</h2>
<p>{post.content}</p>
</article>
))}
);
}
クローラーが受け取る HTML
<!DOCTYPE html>
<html>
<head>
<title>My Blog</title>
</head>
<body>
<article>
<h2>記事のタイトル</h2>
<p>記事の本文</p>
</article>
</body>
</html>
- 検索エンジンが内容を認識できる
- SNS でのプレビューも正常に表示される
ここで重要なのはMVC も同じことをしていた。
もちろんなことですが、MVC モデルもサーバーで HTML を生成していました。
// Controller
exports.index = async (req, res) => {
const posts = await Post.findAll();
res.render('posts/index', { posts }); // EJSでHTML生成
};
違いは何か?
MVC 時代
- サーバーで HTML を生成するのが当たり前だった
- 「SSR」という言葉すら存在しなかった
- すべてサーバーサイドレンダリング
SPA 時代
- すべてクライアントサイドレンダリング(CSR)に移行
- SEO 問題が顕在化
Next.js 時代
- サーバーとクライアントを使い分ける
- 「SSR」という言葉で、改めてサーバーサイドレンダリングの重要性を再認識
つまり、Next.js は MVC の良さ(SSR)を取り戻しつつ、SPA の良さ(インタラクション)も実現した、ハイブリッドなアプローチです。
2. 初回ロードの遅さ → Server Component とコード分割
SPA の問題
初回アクセス時に、アプリ全体の JavaScript(bundle.js)をダウンロードする必要がありました。
ハイドレーション(HTML に JS を紐付ける作業)に時間がかかります。
ユーザーがアクセス
↓
① 空のHTMLが届く
↓
② bundle.jsをダウンロード
↓
③ JavaScriptを実行・解析
↓
④ APIでデータ取得
↓
やっと画面表示
問題点
bundle.js には、アプリ全体のコードが含まれています
- トップページのコード
- 記事詳細ページのコード
- 記事編集ページのコード
- 全てのライブラリ(React、React Router など)
でも、トップページを見るだけなら、他のページのコードは不要です。
※Code Splitting(コード分割)などの最適化手法はありますが、手動での設定が必要でした。
Next.js の解決策
Server Component はクライアントに送られない
//app/posts/page.tsx(Server Component)
export default async function PostsPage() {
const supabase = await createClient();
const { data: posts } = await supabase.from('posts').select('title, content');
return (
{posts?.map(post => (
<article>
<h2>{post.title}</h2>
<p>{post.content}</p>
</article>
))}
);
}
このコードは
- サーバーで実行される
- サーバーで HTML を生成
- 完成した HTML だけをブラウザに送る
- このコンポーネントの JavaScript はブラウザに送られない
つまり、JavaScript のサイズが劇的に削減されます。
Client Component は必要な部分だけ
//components/DeleteButton.tsx(Client Component)
'use client';
export function DeleteButton({ postId }: { postId: number }) {
const handleDelete = async () => {
const supabase = createClient();
const { error } = await supabase.from('posts').delete().eq('id', postId);
};
return <button onClick={handleDelete}>削除</button>;
}
このコンポーネントの JavaScript だけがブラウザに送られます。
結果
MVC
完成したHTML → 即表示
しかしインタラクションが弱い
SPA
bundle.js → 遅い
でもインタラクティブ
Next.js
Server Component → 速い
+ 必要最小限のJavaScript → インタラクティブ
ハイドレーションを最小限に抑えられる
- 初回ロードが速い(MVC と同等)
- インタラクティブ(SPA と同等)
- 両者の良いとこ取り
3. JavaScript への依存 → ハイブリッドアプローチ
SPA の問題
SPA は、JavaScript がなければ何も表示されません。
プログレッシブエンハンスメントとは
Web の基本原則
①HTMLで基本機能を提供
②CSSで見た目を改善
③JavaScriptで体験を向上
JavaScript は「必須」ではなく「拡張」であるべき。
Next.js のServer Actionsという機能を使うと、今回実装した記事作成フォームはJavaScript なしでも動作します。
export default function NewPostPage() {
async function createPost(formData: FormData) {
'use server';
await supabase.from('posts').insert({ title, content });
redirect('/posts');
}
return (
<form action={createPost}>
<div>
<label htmlFor="title">タイトル</label>
<input type="text" id="title" name="title" required />
</div>
<div>
<label htmlFor="content">本文</label>
<textarea name="content" id="content" required rows={10} />
</div>
</form>
);
}
JavaScript 無効でも
- HTML の標準的な form の仕組みを使っているのでフォーム送信が動作
- 記事が作成される
- リダイレクトされる
JavaScript 有効なら
- ローディング状態の表示
- ページリロードなし
- スムーズな UX
MVC との比較
MVC も、JavaScript なしで動作しました。
<form action="/posts" method="POST" class="post-form">
<div class="form-group">
<label for="title">タイトル</label>
<input
type="text"
id="title"
name="title"
value="<%= post.title %>"
placeholder="記事のタイトルを入力"
/>
</div>
</form>
違いは
- MVC: JavaScript なしで動く。でもページ全体リロード
- Next.js: JavaScript なしでも動く。JavaScript があればよりリッチな体験に
Next.js は、MVC の堅牢性と SPA の UX を両立しました。
実際のコード例
MVC と同様に、記事一覧を表示する処理を見ていきます。
記事一覧ページ Next.js の場合:1 つのファイルで完結
app/posts/page.tsx
import Link from 'next/link';
import { createSupabaseServerClient } from '../lib/supabase/server';
export default async function PostPage() {
//①データ取得(Model的)
const supabase = await createSupabaseServerClient();
const { data: posts } = await supabase
.from('posts')
.select('id , title, content, created_at');
//②UI表示(View的)
return (
<main className="min-h-screen p-8">
<div className="max-w-4xl mx-auto">
<div className="flex justify-between items-center mb-8">
<h1 className="text-4xl font-bold">記事一覧</h1>
<Link
href="/posts/new"
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
新規作成
</Link>
</div>
</div>
{posts && posts.length === 0 ? (
<p className="text-gray-500">記事がありません</p>
) : (
<div className="space-y-4">
{posts?.map((post) => (
<article
key={post.id}
className="p-6 border rounded-lg hover:shadow-lg transition"
>
<Link href={`/posts/${post.id}`}>
<h2 className="text-2xl font-bold mb-2 hover:text-blue-600">
{post.title}
</h2>
</Link>
<p className="text-gray-600 mb-4">
{post.content.length > 50
? `${post.content.substring(0, 50)}...`
: post.content}
</p>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-500">
{new Date(post.created_at!).toLocaleDateString('ja-JP')}
</span>
<Link
href={`/posts/${post.id}`}
className="text-blue-600 hover:underline"
>
詳細を見る
</Link>
</div>
</article>
))}
</div>
)}
</main>
);
}
MVC との違い
① Model (Post.js)
↓
② Controller (postsController.js)
↓
③View (index.ejs)
3つのファイルを行き来
Next.js
① posts/page.tsx
1つのファイルで完結
- データ取得
- UI表示
全く違うことが分かります。
この違いについては、次のセクションで Next.js の設計思想と一緒に紐解いていきます。
6. MVC モデルと Next.js の決定的な違い
MVC と Next.js の構造的な違いを図でまとめました。

役割(技術)ごとに分断されていた部品が、コンポーネントという一つの単位に集約されています。
同じ機能を持つブログアプリを自ら作り、比較検証して感じた違いは沢山あります。その中でも、特に僕が「常識が変わった」と確信した 2 つの違いを深掘りします。
6-1. 関心の分離の再定義 Colocation の思想
MVC と Next.js の本質的な違いは、**「何を基準に分離するか」**です。
MVC の思想 技術で分離
MVC は、アプリケーションを技術的な役割で分離します。
記事一覧機能を実装すると
// models/Post.js ← データ取得
static async findAll() {
...
}
//controllers/postsController.js ← 制御
exports.index = async (req, res) => {
const posts = await Post.findAll();
res.render('posts/index', {
title: '記事一覧',
posts: posts,
});
};
//views/posts/index.ejs ← 表示
<% posts.forEach((post) => { %>
<article class="post-card">
<h2><a href="/posts/<%= post.id %>"><%= post.title %></a></h2>
<p><%= post.content.substring(0, 100) %>...</p>
</article>
<% }); %>
「記事一覧」という1つの機能が、3 つのファイルに分散します。
Next.js の思想 機能で凝集(Colocation)
Next.js は、機能的なまとまりで整理します。
こちらも記事一覧機能を実装すると
// app/posts/page.tsx
export default async function PostsPage() {
// データ取得 (Model的)
const posts = await supabase.from('posts').select('id, title, content');
// 表示 (View的)
return (
<div>
{posts?.map((post) => (
<p key={post.id}>{post.title}</p>
))}
</div>
);
}
「記事一覧」という1つの機能が、1 つのファイルに凝集されています。
Colocation(コロケーション) = 関連するものは近くに置く
app/
posts/
page.tsx ← UI + データ取得 + ロジック
layout.tsx ← レイアウト
loading.tsx ← ローディング
components/ ← 専用コンポーネント
PostCard.tsx
actions.ts ← Server Actions
「/posts」に関係するファイルが、すべて posts/ディレクトリにあります。
この変化は革命的だと僕は思います。
MVC ではファイルを分離することによって「整理」したが、開発者にとっては「分断」であり
複数のファイルを跨いで修正が必要なので大変だったと思います。
しかし Next.js では関係しているファイルがディレクトリ(セグメント)に揃っているので分かりやすい。これは僕が実際に作って感じたことです。
この変化を可能にしたもの
MVC では不可能だったこの凝集。それを可能にしたのは React による **「UI のコンポーネント化」と「言語の統一」**でした。
MVC 時代は言語による物理的制約がありました。サーバーサイドのロジック(JavaScript/Ruby/PHP 等)と、表示テンプレート(EJS/ERB/Blade 等)は異なる言語であり、言語が違う以上、それらを物理的に分離し、データを「注入」する形をとらざるを得ませんでした。
MVC 時代
// Model: JavaScript
class Post { ... }
// View: EJS (別の言語)
<% posts.forEach(...) %>
JavaScript と EJS は別の言語。
だから、物理的に分離しなければなりません。
React/Next.js 時代
// 全部JavaScript/JSX
export default async function PostsPage() {
const posts = await supabase.from('posts').select('id, title, content');
return (
<div>
{posts?.map((post) => (
<p key={post.id}>{post.title}</p>
))}
</div>
);
}
全部 JavaScript。
だから、凝集できる。
UI の構造も振る舞いもすべて JavaScript(JSX)という単一の言語で記述します。
この「言語の壁」が消滅したことにより、データ取得から UI 構築までを1つのコンポーネント内に記述する Colocation が現実的な選択肢となりました。
MVC 時代、長く続いた「技術で分離」という常識。
それを、React は「コンポーネントで」覆し、
Next.js は「Colocation」で完成させました。
React という技術革新が、設計思想の革新を生んだのです。
6-2. サーバーとクライアントの「責務の最適化」
MVC と Next.js のもう一つの大きな違いは、「どこで何を実行させるか」というリソースの配分です。
MVC 時代
MVC では、サーバーで HTML を生成していました。
もしブラウザ側の動き(DOM 操作)が必要なら、
重い JavaScript ライブラリをページ全体に読み込ませるしかありませんでした。
サーバー: すべてのHTML生成
+
ブラウザ: すべてのインタラクション (jQuery等)
→ JavaScript が重い
→ FCP(First Contentful Paint)が悪化
SPA 時代
SPA では、すべてをクライアント側で実行します。
サーバー: 空のHTML
+
ブラウザ: すべて(データ取得、HTML生成、インタラクション)
→ JavaScript がさらに重い
→ 初回ロードが遅い
Next.js 時代: ハイブリッドな実行環境
Next.js は、コンポーネント単位で実行場所を切り替えます。
Server Component
// サーバーで実行
export default async function PostsPage() {
const posts = await supabase.from('posts').select('id, title, content');
return {posts?.map(...)};
}
- データを取得し、HTML を生成
- JavaScript はブラウザに送らない
Client Component:
// ブラウザで実行
'use client';
export function DeleteButton({ postId }) {
const handleDelete = async () => { ... };
return 削除;
}
- 必要な箇所だけハイドレーション
- インタラクティブな UI を提供
責務の最適化
Server Component
- データ取得
- 初期HTML生成
→ 速い表示 (FCP最適化)
Client Component
- インタラクティブな操作
- 状態管理
→ 必要最小限のJavaScript
この「使い分け」により
- 高速な初期表示
- SPA の滑らかな操作性
を両立させています。
エンジニアの自由度
これは、サーバーサイドの堅牢さと、クライアントサイドの柔軟性を、エンジニアが自由に設計できるようになったことを意味します。
MVC: サーバーのみ
SPA: クライアントのみ
Next.js: サーバーとクライアントを組み合わせる
そして最適なバランスへ。
そして、バックエンドとフロントエンドという明確な境界線も、曖昧になりました。
Sever Component も Client Component も、同じ React コンポーネント。
物理的な境界線は消えましたが、「どこまでをサーバーでやり、どこまでをクライアントに任せるか」という境界線をコード1行単位で意識する必要があります。
MVC には「Model」「Controller」「View」に分けるという明確なルールがありました。
しかし Next.js には明確なルールがないように思えます。
つまりエンジニアが判断する必要があります。
これは、現代のフロントエンド開発における重要な議論の的と思います。
7. まとめ
1.Next.js が変えた「境界線」の引き方
以前の僕にとって、Server Component と Client Component の使い分けは、**「エラーが出ないようにする」ためだけでした。しかし、Web の歴史と向き合い、自ら手を動かし作り比べた今それは、「ネットワークの境界線をどこに引くか」**という極めて高度な設計思想であると気づきました。
かつての常識はサーバーとクライアントは別物であり、その間には「通信」という壁がありました。
しかし Next.js はその常識を塗り替え、1つのコンポーネントの中でサーバー(データ取得)とクライアント(操作)を自在に融合させました。
これはまさに、Web 開発のパラダイムシフトだと言えます。
2.「関心の分離」の再定義
MVC モデルは3つの役割を「技術(ファイル)」によって物理的に分離します。対して Next.js は、UI とロジックを「機能(コンポーネント)」として1箇所に集約します。実際に両方で実装してみた結果、現代の複雑な UI 開発において、**「意味のある単位でのカプセル化」**がいかに強力であるかを肌で感じることができました。
3.Next.js は「近道」か「迷路」か
今回の検証を経て、確信したことがあります。それは、「Next.js は初学者にとって強力な武器だが、同時に中身が見えにくいブラックボックスになりやすい」 ということです。
Next.js はサーバーとクライアントの複雑な関係を魔法のように隠してくれます。しかし、その魔法に頼りすぎると、ネットワークの向こう側で何が起きているのかという「本質」を見失うリスクがあります。
もちろん、**「伝統的な手法から始めろ」**と強制するつもりはありません。新しい技術への好奇心こそが、学習の最大のエンジンだからです。
-
MVC から学び、不自由さを知ってから Next.js の恩恵を受けるのか
-
Next.js から学び、作りながら「なぜ?」を遡って本質を追いかけるのか
どちらが正解かは分かりません。しかしそれが、「近道」になるのか「迷路」になるのかは、学習者が本質を理解しようとする姿勢次第なのだと僕は思います。
僕と同じように、「なんとなく」から一歩踏み出し、技術の内部構造を理解したいと願う方の参考になれば幸いです。
参考にした記事
この記事を書く上で、以下の記事を参考にさせていただきました。
Discussion
本文のように各ページでデータ操作機能を実装する場合、操作するデータと表示するページが一対一の場合は問題出ないんですが、複数のページで同じデータを扱いたいときに困りそうな気がします。
凝集にこだわると各ページで都度都度データ操作処理を実装し、同じ処理が異なるページ間で重複して存在するような状態になりそうですが、これは問題ないでしょうか?
コメントいただきありがとうございます。
おっしゃる通り、ロジックをページに閉じ込めすぎると、他のページでも同じデータを使いたい時にコードが重複してしまうのは問題です。
ぼくはその解決策として、主にカスタムフックを活用するようにしています。
以前、認証機能を実装した際、複数の画面で同じようなローディングやエラー処理を書く必要が出てきて悩んだことがありました。その時、OSSのコードを読んでいて「ロジックだけをフックに切り出す」という手法を知り、ヒントを得て 解決しました。
これなら、各ページに凝集させる良さを活かしつつ、中身の処理は一箇所にまとめて再利用できるので、今のところ自分の中ではしっくりきています。
もちろん、アプリ全体で使いたいデータは状態管理ライブラリなどを検討すればいいと思います。
これは言われなければ気づかなかった盲点です。
貴重なご指摘、本当にありがとうございました。
いただいたコメントを記事に追記しました。
ありがとうございます。