Next.js

Next.jsのSSR入門:getServerSideProps・キャッシュ・配信の注意点

この記事でわかること

Pages RouterのgetServerSidePropsでSSRを実装する方法を解説。リクエスト情報、props、キャッシュ、秘密情報の扱い、静的出力と併用できない理由を確認できます。

SSRは、リクエストに応じてサーバーでHTMLを生成する方法です。Next.jsのPages Routerでは、ページからgetServerSidePropsをexportして、リクエスト時に取得した値を画面へ渡します。

この記事はTypeScriptとPages Routerを使用し、Next.jsを実行するサーバーがある構成を前提にします。output: 'export'で作る静的サイトには、getServerSidePropsを含められません。App Routerでは別のデータ取得方法を使うため、以下のコードをsrc/app/へそのまま移さないでください。

SSRのページを作成して確認する

リクエスト時の値を画面へ渡す

src/pages/server-time.tsxへ次のコードを保存します。

import type { GetServerSideProps } from 'next';

type Props = {
  serverRenderedTime: string;
};

export const getServerSideProps: GetServerSideProps<Props> = async ({ res }) => {
  res.setHeader('Cache-Control', 'private, no-store');
  return {
    props: { serverRenderedTime: new Date().toISOString() },
  };
};

export default function ServerTimePage({ serverRenderedTime }: Props) {
  return (
    <main>
      <h1>サーバーで生成した時刻</h1>
      <p><time dateTime={serverRenderedTime}>{serverRenderedTime}</time></p>
    </main>
  );
}

/server-timeへアクセスすると、リクエストを処理したサーバーの時刻が表示されます。ここではキャッシュしない設定にしているため、再取得すると値が変わります。ブラウザが独自に動かす時計ではなく、レスポンスを作った時点の値です。

getServerSidePropsはサーバー側で実行されますが、返したpropsはブラウザへ送られます。APIキーやセッショントークンなどを含めないでください。日時はISO文字列へ変換するなど、JSONとして扱えるデータを返します。

クエリの型を確認して検索する

URLの値を使う場合は、文字列と決め付けずに確認します。src/pages/search.tsxへ次のコードを保存すると、/search?q=TypeScriptのようなURLを処理できます。

import type { GetServerSideProps } from 'next';

type Article = { id: string; title: string };
type Props = { query: string; articles: Article[] };

const articles: Article[] = [
  { id: 'types', title: 'TypeScriptの型を学ぶ' },
  { id: 'pages', title: 'Next.jsでページを作る' },
];

export const getServerSideProps: GetServerSideProps<Props> = async ({ query, res }) => {
  const rawQuery = query.q;
  const search = typeof rawQuery === 'string' ? rawQuery.trim().slice(0, 100) : '';
  const matches = articles.filter((article) =>
    article.title.toLowerCase().includes(search.toLowerCase())
  );

  res.setHeader('Cache-Control', 'private, no-store');
  return { props: { query: search, articles: matches } };
};

export default function SearchPage({ query, articles }: Props) {
  return (
    <main>
      <h1>記事検索</h1>
      <form method="get">
        <label>
          キーワード
          <input name="q" defaultValue={query} maxLength={100} />
        </label>
        <button type="submit">検索</button>
      </form>
      <p>{articles.length}件見つかりました。</p>
      <ul>
        {articles.map((article) => <li key={article.id}>{article.title}</li>)}
      </ul>
    </main>
  );
}

同名のクエリが複数あると配列になる場合もあるため、typeofで確認しています。これはURL入力の扱いを学ぶ例で、小さな固定データの検索自体にSSRが必須という意味ではありません。

データベースを使う場合は、この検索部分をサーバー用の取得処理へ置き換えます。SQLへ文字列を直接連結せず、利用するライブラリのパラメーター化されたクエリを使います。認証が必要なページでは、データ取得の前にセッションとアクセス権限を確認します。

本番モードで動作を確認する

next.config.jsoutput: 'export'がないサーバー構成で、次を実行します。すでに開発サーバーが同じポートを使っている場合は停止してください。

npm run build
npm run start

npm run startは、通常next startを呼び出すスクリプトです。配信先は、Node.jsを実行するWebサーバーや、Next.jsのSSRに対応したホスティングを選びます。生成された一部のHTMLだけを静的サーバーへ転送しても、リクエスト時の処理は再現できません。

データ取得とキャッシュの注意点

getServerSidePropsから同じアプリのAPI RouteへHTTPでアクセスする必要はありません。共通の取得関数をサーバー側から直接呼ぶと、余分な通信を避けられます。外部APIを呼ぶ場合は応答ステータス、タイムアウト、入力値の検証を考慮します。

SSRの応答時間には外部APIやデータベースの待ち時間が加わります。頻繁に変わらない公開記事であれば、SSGの方が構成に合う場合があります。SSRにしただけで検索順位や表示速度が必ず改善するわけではありません。

公開情報は条件に応じてキャッシュできますが、利用者別の情報を共有キャッシュへ保存すると、別の利用者へ情報が渡る危険があります。この記事の例は挙動を分かりやすくするためprivate, no-storeにしています。実際の配信環境のCDN設定も合わせて確認してください。

レンダリング時に使えないもの

windowdocumentlocalStorageはサーバーに存在しません。これらを必要とする処理は、ブラウザのイベントやEffectなどに分けます。ページコンポーネントそのものもサーバーで描画される点に注意してください。

また、SSGとSSRを同じページで併用するために、getStaticPropsgetServerSidePropsを同時にexportすることはできません。ページに必要なデータと配信方法から選びます。

関連記事と公式資料

最初の環境構築はNext.jsをインストール、ブラウザの処理との分担はuseEffectの使用方法で説明しています。

利用者ごとの表示を安全に扱う

キャッシュの共有範囲を確認する

ログイン状態などに応じたページを、別の利用者へ共有するキャッシュへ保存しないようにします。SSRであることだけを根拠に安全だと判断せず、応答ヘッダーと配信基盤の設定を確認します。

propsへ渡す情報を絞る

サーバーで読んだ値でも、propsへ渡せばブラウザーへ送られます。APIキーや表示に不要な項目を除き、公開してよいデータだけを返してください。

Next.jsとは・ルーターの違い・学習順を確認する

y.
WRITTEN BY

y_ymo10

SEの部屋で、JavaScript・TypeScript・React.js・Next.jsの開発ノートを公開しています。

ほかの開発ノートを読む →