🦔

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 をダウンロード

  1. ブラウザで https://nodejs.org/ にアクセス
  2. LTS(Long Term Support)版をダウンロード
  3. 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/ という形式で生成)

参考資料


サンプル 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">202411</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">202412</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">202413</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. ブラウザで確認

  1. ブラウザで http://localhost:3000 を開く
  2. 以下のページが表示されることを確認:
- 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 バケット設定

  1. パブリックアクセス:ブロック(デフォルトのまま)
  2. 静的ウェブサイトホスティング:無効(デフォルトのまま)
  3. バケットの静的ウェブサイトホスティング:
    • インデックスドキュメント:index.html
    • エラードキュメント:index.html

9-2. CloudFront 設定

  1. オリジン設定で S3 バケットを選択
  2. 「Origin access」で「Origin access control settings (recommended)」を選択
  3. 「Create new OAC」をクリックして OAC を作成
  4. 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 ディストリビューションドメイン名を確認

  1. AWS 管理コンソールで CloudFront を開く
  2. ディストリビューション一覧から作成したディストリビューションを選択
  3. ドメイン名をメモ(例: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