CloudFront + S3 で React(SSG)を動かしてみよう!
はじめに
PHPer(ペチパーというらしい:まともにできるプログラミング言語が「PHP」くらいしかないプログラマのこと)の私が最近 React に触れる機会があったので、簡単に整理してみました。
また、条件によっては CloudFront + S3 でも動作するらしい?という情報を仕入れたので試してみました。
対象読者
- React 初心者の方
- CloudFront + S3 の構成や動作が分かる方
おことわり
- 私の作業端末が Windows11 のため、実行コマンドなどは Windows ベースで説明を行います。
Mac などをご利用の場合は、適宜読み替えてください。 - CloudFront(OAC) + S3 の環境を推奨します。
React 基礎知識
React を使ったアプリケーションには、大きく3つの実行方式があります。
それぞれの特徴は以下の通り。
1. SPA( Single Page Application )
ブラウザで初回に HTML・JS・CSS をすべて読み込み、その後はクライアント側で動的に画面を切り替えるアプリケーション形式です。
初期 HTML は最小限の構成で、JavaScript によって DOM が動的に構築されます。CloudFront + S3 でホスティングすることで、クライアント側での API 連携により動的コンテンツを実現できます。
ただし、初期 HTML に完成したコンテンツが含まれていないため、SEO クローラーが JS 実行を待つ必要があります。
CloudFront + S3 から配信された index.html が、リンクされた JavaScript ファイル( main.js など)をブラウザに読み込ませ、その JavaScript がブラウザ上で実行されて DOM を構築し、React アプリケーションが動作することで実現可能
2. SSR( Server-Side Rendering )
サーバー側で React コンポーネントを HTML に変換し、ブラウザに返す方式です。初期表示は HTML を返すため初速が速いですが、サーバーの処理能力が必要です。
サーバーが必要なため CloudFront + S3 のみでは不可
3. SSG( Static Site Generation )
ビルド時に全ページの HTML を事前生成し、生成された静的ファイルを配信する方式です。更新には再ビルドが必要ですが、サーバー処理が不要です。
全て html で生成されるため、CloudFront + S3 のみで実現可能
比較表
| 項目 | SPA | SSR | SSG |
|---|---|---|---|
| 初期読み込み | JavaScript 読み込み・実行時間が必要 | HTML をサーバーから返すため速い | 生成済み HTML をそのまま返すため速い |
| ページ遷移 | クライアント側で処理されるため速い | サーバーとの通信が必要 | ページ再取得が必要 |
| SEO | JavaScript 実行後のコンテンツ反映に対応が必要 | HTML に完成したコンテンツが含まれる | HTML に完成したコンテンツが含まれる |
| 動的コンテンツ対応 | クライアント側で API 連携 | サーバー側で各リクエストに対応 | 限定的(ビルド時点でのみ) |
| 更新タイミング | リアルタイム | リアルタイム | ビルド時のみ |
| サーバー負荷 | 低い | 高い | なし |
| ホスティング | S3 可能 | サーバー必要 | S3 可能 |
| スケーリング対応 | AWS が自動対応 | スケーリング設定必要 | AWS が自動対応 |
CloudFront + S3 でホスティングする場合、どれを選択するべきか
CloudFront + S3 はサーバー実行環境を提供しないため、SSR は要件に合いません。
(SSR は Lambda、EC2 などの実行環境が必要)
そこで SPA か SSG の選択となります。本記事では SSG を選択しました。
理由:
- ビルド時に完全な HTML が生成されるため、JavaScript 実行前にすべてのコンテンツが利用可能
- SEO クローラーが確実にコンテンツを読める
- API キーなどの機密情報がクライアント側に露出しない
- CloudFront キャッシュとの組み合わせで最高のパフォーマンスを実現
余談:SSG について深掘り
SSG のメリット、デメリットと SSG を採用する(しない)基準をまとめました。
システム要件により使い分けするとよいです。
SSG のメリット
| メリット | 内容 |
|---|---|
| ホスティング単純 | 静的ファイルのみ、S3 で可能 |
| キャッシュ効率 | CDN キャッシュに適している |
| クローラー対応 | メタタグが確実に読まれる |
| 初期表示速度 | HTML が完成した状態で配信 |
| セキュリティ | API キーがクライアント側に渡されない |
| エラー検出 | ビルド時に全ページ検証可能 |
| スケーリング | 自動対応で設定作業不要 |
SSG のデメリット
| デメリット | 内容 |
|---|---|
| ビルドサイクル | 更新に再ビルドが必要 |
| 動的コンテンツ制限 | ユーザー毎の異なるコンテンツ不可 |
| ビルド時間 | ページ数増加で時間増加 |
| 開発複雑性 | ビルドプロセスが必要 |
| 更新漏れリスク | デプロイ忘れの可能性 |
| ホスティング制限 | ISR など機能制限がある |
| ハイブリッド対応 | 複雑な機能では SPA との組み合わせ必要 |
採用判断
SSG 採用が適切な用途:
- ブログ、ニュースサイト
- ポートフォリオサイト
- ドキュメントサイト
- SEO が重要なマーケティングサイト
SSG が適さない用途:
- リアルタイムデータ更新が必要
- ユーザーログイン後の個別画面
- 高頻度なコンテンツ更新
Windows 11 で SSG サンプルプログラムを構築しデプロイする
環境構築
ステップ1:Node.js のインストール
1-1. Node.js をダウンロード
- ブラウザで https://nodejs.org/ にアクセス
- LTS(Long Term Support)版をダウンロード
- Windows インストーラー(.msi)を実行
1-2. インストール確認
PowerShell またはコマンドプロンプトを開き、以下を実行:
node --version
npm --version
バージョン番号が表示されれば正常です。
参考:私の動作確認環境
> node --version
v20.16.0
> npm --version
10.8.1
ステップ2:Next.js プロジェクト作成
2-1. 作業フォルダを作成
PowerShell を開き、以下のコマンドを実行:
# ホームディレクトリに移動
cd ~
# 作業用フォルダを作成
mkdir ssg-sample
cd ssg-sample
2-2. Next.js プロジェクトを初期化
npx create-next-app@latest . --typescript --tailwind
質問が表示されたら、以下のように答えます:
✔ Would you like to use TypeScript? … Yes
✔ Would you like to use ESLint? … Yes
✔ Would you like to use Tailwind CSS? … Yes
✔ Would you like to use `src/` directory? … No
✔ Would you like to use App Router? … Yes
✔ Would you like to customize the import alias? … No
2-3. 必要なパッケージをインストール
npm install
ステップ3:next.config.js を設定
3-1. next.config.js を編集
プロジェクトルートの next.config.js を開き、以下のように編集します:
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export',
trailingSlash: true,
}
module.exports = nextConfig
設定の説明:
-
output: 'export':静的ファイルをエクスポート -
trailingSlash: true:URL の末尾に/を追加(/blog/という形式で生成)
参考資料:
- Static Exports:https://nextjs.org/docs/app/building-your-application/deploying/static-exports
- trailingSlash:https://nextjs.org/docs/app/api-reference/next-config-js/trailing-slash
サンプル SSG プログラムの作成
ステップ4:ページコンポーネントの作成
4-1. ブログ記事一覧ページを作成
app/blog/page.tsx を作成:
// app/blog/page.tsx
import Link from 'next/link';
export default function BlogList() {
return (
<div className="max-w-2xl mx-auto p-8">
<h1 className="text-4xl font-bold mb-8">ブログ一覧</h1>
<div className="space-y-4">
<article className="border-b pb-4">
<Link href="/blog/post-1/" className="text-xl font-semibold hover:text-blue-600">
最初のブログ記事
</Link>
<p className="text-gray-600 mt-2">
これは最初のブログ記事です。
</p>
<p className="text-sm text-gray-500 mt-2">2024年1月1日</p>
</article>
<article className="border-b pb-4">
<Link href="/blog/post-2/" className="text-xl font-semibold hover:text-blue-600">
2番目のブログ記事
</Link>
<p className="text-gray-600 mt-2">
これは2番目のブログ記事です。
</p>
<p className="text-sm text-gray-500 mt-2">2024年1月2日</p>
</article>
<article className="border-b pb-4">
<Link href="/blog/post-3/" className="text-xl font-semibold hover:text-blue-600">
3番目のブログ記事
</Link>
<p className="text-gray-600 mt-2">
これは3番目のブログ記事です。
</p>
<p className="text-sm text-gray-500 mt-2">2024年1月3日</p>
</article>
</div>
</div>
);
}
4-2. ホームページを更新
app/page.tsx を編集:
// app/page.tsx
import Link from 'next/link';
export default function Home() {
return (
<div className="min-h-screen bg-gradient-to-b from-blue-50 to-white">
<div className="max-w-2xl mx-auto p-8">
<h1 className="text-4xl font-bold mb-4">
SSG サンプルサイト
</h1>
<p className="text-xl text-gray-600 mb-8">
Next.js を使用した静的サイト生成(SSG)のデモです。
</p>
<div className="space-y-4">
<section className="bg-white p-6 rounded-lg shadow">
<h2 className="text-2xl font-semibold mb-2">このサイトについて</h2>
<p className="text-gray-700">
このサイトは Next.js の SSG 機能を使用して構築されています。
すべてのページはビルド時に HTML として生成されます。
</p>
</section>
<section className="bg-white p-6 rounded-lg shadow">
<h2 className="text-2xl font-semibold mb-2">ナビゲーション</h2>
<ul className="space-y-2">
<li>
<Link href="/blog/" className="text-blue-600 hover:underline">
ブログ一覧
</Link>
</li>
<li>
<Link href="/about/" className="text-blue-600 hover:underline">
このサイトについて詳細
</Link>
</li>
</ul>
</section>
</div>
</div>
</div>
);
}
4-3. About ページを作成
app/about/page.tsx を作成:
// app/about/page.tsx
import Link from 'next/link';
export default function About() {
return (
<div className="max-w-2xl mx-auto p-8">
<h1 className="text-4xl font-bold mb-8">このサイトについて</h1>
<div className="prose max-w-none">
<section className="mb-8">
<h2 className="text-2xl font-semibold mb-4">SSG とは</h2>
<p className="text-gray-700 mb-4">
Static Site Generation(SSG)は、ビルド時にすべてのページを HTML として生成する方式です。
</p>
<p className="text-gray-700 mb-4">
生成された HTML はサーバーなしで配信可能で、セキュリティも高く、速度も速いという特徴があります。
</p>
</section>
<section>
<h2 className="text-2xl font-semibold mb-4">技術スタック</h2>
<ul className="list-disc list-inside space-y-2 text-gray-700">
<li>Next.js(SSG フレームワーク)</li>
<li>TypeScript(型安全性)</li>
<li>Tailwind CSS(スタイリング)</li>
</ul>
</section>
</div>
<div className="mt-8">
<Link href="/" className="text-blue-600 hover:underline">
← ホームに戻る
</Link>
</div>
</div>
);
}
4-4. レイアウトを更新
app/layout.tsx を編集:
// app/layout.tsx
import type { Metadata } from 'next';
import Link from 'next/link';
import './globals.css';
export const metadata: Metadata = {
title: 'SSG サンプルサイト',
description: 'Next.js を使用した静的サイト生成のデモ',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ja">
<body className="bg-gray-50">
<nav className="bg-white shadow">
<div className="max-w-2xl mx-auto p-4">
<Link href="/" className="text-xl font-bold">
SSG Sample
</Link>
</div>
</nav>
<main>{children}</main>
</body>
</html>
);
}
ステップ5:動的ルートでブログ記事を生成
5-1. ブログ記事用のデータを作成
lib/posts.ts を作成:
// lib/posts.ts
export interface Post {
id: string;
title: string;
date: string;
content: string;
}
export const posts: Post[] = [
{
id: 'post-1',
title: '最初のブログ記事',
date: '2025年12月1日',
content: `
# 最初のブログ記事
これは SSG で生成される最初のブログ記事です。
## セクション1
ビルド時にこのコンテンツは HTML として固定化されます。
## セクション2
ユーザーがページにアクセスすると、既に生成されている HTML がそのまま返されます。
`,
},
{
id: 'post-2',
title: '2番目のブログ記事',
date: '2025年12月2日',
content: `
# 2番目のブログ記事
SSG の特徴についてさらに詳しく説明します。
## ビルド時に生成される
ブログ記事のようなコンテンツが少なく更新が定期的な場合、SSG は最適です。
## 動的ルートの使用
Next.js では動的ルート([id])を使用することで、複数の記事ページを効率的に生成できます。
`,
},
{
id: 'post-3',
title: '3番目のブログ記事',
date: '2025年12月3日',
content: `
# 3番目のブログ記事
実際の運用でよく使用されるパターンについて。
## 記事の更新方法
記事を新しく追加または更新する場合:
1. Markdown または JSON で記事を追加
2. npm run build を実行
3. 生成されたファイルをホスティングサービスにアップロード
`,
},
];
export function getPost(id: string): Post | undefined {
return posts.find((post) => post.id === id);
}
export function getAllPostIds(): string[] {
return posts.map((post) => post.id);
}
5-2. 動的ブログ記事ページを作成
app/blog/[id]/page.tsx を作成:
// app/blog/[id]/page.tsx
import { getPost, getAllPostIds } from '@/lib/posts';
import { notFound } from 'next/navigation';
import Link from 'next/link';
export async function generateStaticParams() {
const ids = getAllPostIds();
return ids.map((id) => ({ id }));
}
interface Props {
params: Promise<{ id: string }>;
}
export default async function BlogPost({ params }: Props) {
const { id } = await params;
const post = getPost(id);
if (!post) {
notFound();
}
return (
<div className="max-w-2xl mx-auto p-8">
<div className="mb-8">
<Link href="/blog/" className="text-blue-600 hover:underline">
← ブログ一覧に戻る
</Link>
</div>
<article>
<header className="mb-8">
<h1 className="text-4xl font-bold mb-2">{post.title}</h1>
<time className="text-gray-500">{post.date}</time>
</header>
<div className="prose max-w-none">
<div className="whitespace-pre-wrap text-gray-700">
{post.content}
</div>
</div>
<hr className="my-8" />
<footer>
<Link href="/blog/" className="text-blue-600 hover:underline">
← ブログ一覧に戻る
</Link>
</footer>
</article>
</div>
);
}
ビルドと動作確認
ステップ6:開発環境での動作確認
6-1. 開発サーバーを起動
PowerShell で以下を実行:
npm run dev
6-2. ブラウザで確認
- ブラウザで http://localhost:3000 を開く
- 以下のページが表示されることを確認:
- http://localhost:3000/
- http://localhost:3000/blog/
- http://localhost:3000/blog/post-1/
- http://localhost:3000/blog/post-2/
- http://localhost:3000/blog/post-3/
- http://localhost:3000/about/
ステップ7:Static Export でビルド
7-1. ビルドを実行
開発サーバーを停止(Ctrl+C)してから実行:
npm run build
ビルド完了時に以下のようなログが表示されます:
▲ Next.js 15.0.0
○ Compiled successfully
✓ Exported to ./out
7-2. 生成されたファイルの確認
# Windows エクスプローラーで確認
explorer .\out
out/ フォルダ内に以下が生成されます:
out/
├── index.html ← ホームページ
├── about/
│ └── index.html ← About ページ
├── blog/
│ ├── index.html ← ブログ一覧
│ ├── post-1/
│ │ └── index.html ← ブログ記事1
│ ├── post-2/
│ │ └── index.html ← ブログ記事2
│ └── post-3/
│ └── index.html ← ブログ記事3
└── _next/
└── static/ ← CSS、JavaScript
生成されたファイルの確認
ステップ8:ファイル構造の確認
# out フォルダ内の構造を表示
dir out /s /b
以下のような構造が確認できます:
out\index.html
out\about\index.html
out\blog\index.html
out\blog\post-1\index.html
out\blog\post-2\index.html
out\blog\post-3\index.html
out\_next\static\...
S3 へのアップロード準備
ステップ9:S3 + CloudFront(OAC)の設定
9-1. S3 バケット設定
- パブリックアクセス:ブロック(デフォルトのまま)
- 静的ウェブサイトホスティング:無効(デフォルトのまま)
- バケットの静的ウェブサイトホスティング:
- インデックスドキュメント:
index.html - エラードキュメント:
index.html
- インデックスドキュメント:
9-2. CloudFront 設定
- オリジン設定で S3 バケットを選択
- 「Origin access」で「Origin access control settings (recommended)」を選択
- 「Create new OAC」をクリックして OAC を作成
- CloudFront が生成するバケットポリシーを S3 に適用
9-3. CloudFront Functions でサブディレクトリ対応
/blog/ → /blog/index.html の変換が必要なため、CloudFront Functions を設定します。
9-4. S3 にアップロード
生成された out/ ディレクトリ内の全ファイルを S3 にアップロードします。
AWS 管理コンソールで作業しても構いません。
参考:CLI コマンド
# AWS CLI でアップロード
aws s3 sync out/ s3://my-bucket/ --delete
動作確認
ステップ10:CloudFront ディストリビューションドメイン名での動作確認
10-1. CloudFront ディストリビューションドメイン名を確認
- AWS 管理コンソールで CloudFront を開く
- ディストリビューション一覧から作成したディストリビューションを選択
- ドメイン名をメモ(例:d123abc456.cloudfront.net)
10-2. CloudFront 経由でのアクセス確認
ブラウザで以下の URL にアクセスし、各ページが正常に表示されることを確認します。
https://d123abc456.cloudfront.net/
https://d123abc456.cloudfront.net/about/
https://d123abc456.cloudfront.net/blog/
https://d123abc456.cloudfront.net/blog/post-1/
https://d123abc456.cloudfront.net/blog/post-2/
https://d123abc456.cloudfront.net/blog/post-3/
10-3. キャッシュ動作の確認
上記 URL に複数回アクセスしてから、ブラウザの開発者ツール(F12)を開き、レスポンスヘッダー内の X-Cache が Hit from cloudfront になっていることを確認してください。これはキャッシュが効いていることを示しています。
10-4. リンク遷移の動作確認
各ページから別のページへのリンク遷移が正常に機能することを確認します。
- ホームページ(/)→ ブログ一覧(/blog/)
- ブログ一覧(/blog/)→ 個別記事(/blog/post-1/)
- 個別記事(/blog/post-1/)→ ブログ一覧に戻る
- ナビゲーション全体が正常に動作すること
10-5. トラブルシューティング
-
403 エラーが表示される場合:
- S3 バケットアクセス権限を確認
- CloudFront の OAC(Origin Access Control)設定を確認
- S3 バケットポリシーが正しく設定されているか確認
-
ページが表示されない場合:
- CloudFront ディストリビューションのデプロイ状態を確認(Status: Deployed が表示されるまで待機)
- CloudFront キャッシュをクリア(コンソール > Invalidations > Create invalidation で /* を指定)
まとめ
React = プログラム = サーバー必要と思っていたんですが、SSG であれば、S3 + CloudFront で動作するんですね。勉強になりました!
通常は Vercel や AWS Amplify 利用がマストだと思いますが、プログラム要件によってはありかなと思います。
この記事が、誰かの役に立てば幸いです。
Discussion