概要
npm run dev を実行してからブラウザに画面が表示されるまでの流れってどうなっているんだろう?
と思ったので、Next.js(App Router)の仕組みに沿って紹介します。
サンプルコードには記事一覧アプリを使います。バックエンドAPIから記事を取得して一覧表示するシンプルな構成で、実際のプロジェクトでもよく見るパターンだと思います。
src/
app/
layout.tsx # 全ページ共通の外枠
page.tsx # トップページ(/)
components/
ArticleList.tsx # メインUI・状態管理
ArticleCard.tsx # 記事1件の表示
lib/
api-client.ts # バックエンドへの通信
types/
article.ts # 型定義
Step 1: コマンドの解釈
npm run dev
↓
"dev": "next dev" (package.json の scripts に定義)
↓
Next.js の開発サーバーが起動
→ http://localhost:3000 でブラウザからアクセス可能になる
package.json はプロジェクトの設定ファイルです。scripts の中に dev というコマンドが定義されていて、npm run dev を叩くとそれが実行されます。
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
}
}
Step 2: Next.js がファイルを探す
Next.js は src/app/ フォルダを自動的に見に行きます。これは Next.js の App Router という仕組みで、ファイルの場所がそのままURLになります。
src/app/layout.tsx → 全ページ共通の「外枠」
src/app/page.tsx → http://localhost:3000/ にアクセスしたときのページ
どこで定義されているのか
src/app を見に行くのは Next.js 自体にハードコードされた規約です。設定ファイルではなく、Next.js のソースコード内部に「この順番でフォルダを探す」というロジックが組み込まれています。
Next.js が探す場所と優先順位:
ルート直下の app/ または pages/ があれば、それを使う
↓ なければ
src/app/ または src/pages/ を使う
ポイントは、ルート直下に app/(または pages/)が存在すると、src/ 配下は無視されるという点です。「両方あったら片方にフォールバックする」というより、「ルート直下を見つけたら、そちらだけを使う」という排他的な挙動になっています。
なお app/(App Router)と pages/(Pages Router・旧方式)は併用も可能ですが、本記事では App Router に絞って解説します。
なぜ src/app/ になるのか
npx create-next-app でプロジェクト作成時に対話形式で聞かれます。
Would you like your code inside a `src/` directory? › No / Yes
ここで Yes を選ぶと src/app/ になり、No を選ぶとルート直下の app/ になります。その選択結果がフォルダ構造として残るだけで、設定ファイルへの書き込みは一切ありません。
next.config.ts を見ても src/app に関する記述はなく、Next.js が実行時にフォルダの存在を検知して自動判定しています。
「CoC(設定より規約, Convention over Configuration)」という考え方で、明示的に書かなくても決まった場所に置けば動く、という設計思想に近いらしいです。
Step 3: ファイルが組み合わさって画面になる
App Router では、画面は 「サーバーで一度組み立てて、ブラウザで仕上げる」 という2段階で表示されます。
【サーバー側】
① layout.tsx → page.tsx の順にHTMLを組み立てる
② この時点の ArticleList はまだデータを持っていないので、
「読み込み中...」状態のHTMLが生成され、ブラウザに送られる
↓
【ブラウザ側】
③ 送られてきたHTMLにReactが対応づけを行う(ハイドレーション)
④ ArticleList の useEffect が走り、fetchArticles() でAPIからデータ取得
⑤ 取得したデータで再描画され、ArticleCard が記事を1件ずつ並べる
「ブラウザを開いた瞬間に全部が描画される」のではなく、まず枠とローディング状態が表示され、そのあとデータが差し込まれるという流れになっています。
各ファイルの役割
src/app/layout.tsx — HTML の骨格
// 全ページ共通の「外枠」
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ja">
<body>
{children} {/* ここに page.tsx の内容が入る */}
</body>
</html>
);
}
- どのページにも共通で適用される HTML の枠組みを定義
- フォントや
globals.cssなどの共通スタイルもここで読み込む
src/app/page.tsx — トップページ(/)
import ArticleList from '@/components/ArticleList';
export default function Home() {
return <ArticleList />;
}
-
http://localhost:3000/にアクセスしたときに表示されるページ - 実際の画面処理はコンポーネント(ここでは
ArticleList)に委譲する
src/components/ArticleList.tsx — メインの画面ロジック
アプリの中心。APIから記事を取得し、状態に応じて表示を切り替えます。
'use client';
import { useState, useEffect } from 'react';
import { fetchArticles } from '@/lib/api-client';
import { Article } from '@/types/article';
import ArticleCard from '@/components/ArticleCard';
export default function ArticleList() {
const [articles, setArticles] = useState<Article[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
setLoading(true);
fetchArticles()
.then(setArticles)
.catch(() => setError('取得に失敗しました'))
.finally(() => setLoading(false));
}, []);
if (loading) return <p>読み込み中...</p>;
if (error) return <p>{error}</p>;
return (
<ul>
{articles.map(article => (
<ArticleCard key={article.id} article={article} />
))}
</ul>
);
}
useState とは
コンポーネントに「状態(変数)」を持たせるReactの組み込みフックです。通常の変数と違い、値が変わると画面が自動で再描画されます。
useEffect とは
「画面が表示されたあとに実行したい処理」を書くReactの組み込みフックです。APIの呼び出しなど、描画とは切り離して行いたい副作用的な処理に使います。
3つの useState は、APIの呼び出し状況に合わせて表示を切り替えるために使います。
| 状態 | 値 | 表示 |
|---|---|---|
loading |
true |
「読み込み中...」を表示 |
error |
文字列 | エラーメッセージを表示 |
articles |
記事の配列 |
ArticleCard を並べる |
src/components/ArticleCard.tsx — 記事1件の表示
ArticleList から呼ばれ、記事を1件ずつ描画します。
type Props = { article: Article };
export default function ArticleCard({ article }: Props) {
return (
<div>
<h2>{article.title}</h2>
<p>{article.body}</p>
</div>
);
}
src/lib/api-client.ts — バックエンドへの通信
const getBaseUrl = () =>
process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
export async function fetchArticles(): Promise<Article[]> {
const res = await fetch(`${getBaseUrl()}/articles`);
return res.json();
}
- バックエンドのAPIエンドポイントは環境変数
NEXT_PUBLIC_API_URLで切り替えられる - 未設定の場合はローカルの
localhost:3001を使う
src/types/article.ts — 型定義
export type Article = {
id: number;
title: string;
body: string;
};
バックエンドから返ってくるデータの「形」を TypeScript の型として定義します。コードの補完やエラー検出に使われます。
tsconfig.json — TypeScript の設定
{
"compilerOptions": {
"paths": { "@/*": ["./src/*"] }
}
}
import ArticleList from '@/components/ArticleList' のように @/ で src/ フォルダを短く参照できるようになります。
まとめ: 全体の関係図
npm run dev
│
▼
Next.js 開発サーバー起動
│
▼ ブラウザで localhost:3000 にアクセス
│
layout.tsx ── globals.css(スタイル読み込み)
│
└── page.tsx
│
└── ArticleList.tsx ← メインUI・状態管理
│
├── api-client.ts → バックエンドAPI呼び出し
│ └── types/article.ts(型定義)
│
└── ArticleCard.tsx ← 記事1件の表示
補足: 'use client' とは?
今回のサンプルでは ArticleList.tsx の先頭に 'use client' が付いています。
Next.js はデフォルトでサーバー側でHTMLを生成しますが、useState や onClick などブラウザ特有の機能を使うときは 'use client' を先頭に書いて「このファイルはブラウザで動かす」と宣言する必要があります。
'use client';
import { useState } from 'react';
// ...