ゼロからの開発日誌

日々の学びなどを中心に

その4 Pre-rendering and Data Fetching

用語

  • Pre-rendering

  • Data Fetching

    • 文字のまま。データをフェッチするという意味。
    • つまり、データを取り込む、どこかからデータをとってくるといった感じ。

4-1. Pre-rendering and Data Fetching

  • 最終的にブログを作成したいが、現時点ではブログのコンテンツがない状態
  • このレッスンでは、外部のブログデータをアプリに取り込む方法を学ぶ
  • このチュートリアルでは、ブログのコンテンツをファイルシステムに格納する
    • コンテンツが他の場所(データベースやヘッドレスCMSなど)に格納されている場合でも同様に実装できる
このレッスンで学ぶこと
  • このレッスンでは、以下について学ぶ
    • Next.jsのpre-rendering機能について
    • 二種類のpre-rendering方式: Static GenerationServer-side Rendering
    • データありとデータなしのStatic-Generation
    • getStaticPropsを使用した、インデックスページへの外部ブログデータのインポート方法
    • getStaticPropsに関するいくつかの有益な情報

4-2. Setup

  • 前のレッスンから続けて受講する場合は、このページを読み飛ばすことができます。

4-3. Pre-rendering

  • データの取り込みについて話す前に、Next.jsの最も重要な概念のひとつであるPre-renderingについて説明する

  • デフォルトでは、Next.jsはすべてのページを事前レンダリングする

  • 事前レンダリングとは

    • クライアントサイドのJavaScriptですべてのHTML生成を行うのではなく、Next.jsがあらかじめ各ページのHTMLを生成しておくことを意味する
    • パフォーマンスとSEOの向上につながります。
  • 生成された各HTMLは、そのページに必要な最小限のJavaScriptコードと関連付けられている

    • ブラウザがページを読み込むと、そのJavaScriptコードが実行され、ページが完全にインタラクティブになる
    • このプロセスは、Hydrationと呼ばれる
プリレンダリングが行われていることを確認する

注意:上記の手順をlocalhostで試すこともできますが、JavaScriptを無効にするとCSSが読み込まれません。

  • アプリがプレーンな React.js アプリ(Next.js を含まない)の場合、プリレンダリングがないため、JavaScript を無効にするとアプリが表示されなくなります。たとえば
    • ブラウザのJavaScriptを有効にして、このページをご覧ください。これは、Create React Appで構築したプレーンなReact.jsのアプリです。
    • ここで、JavaScriptを無効にして、もう一度同じページにアクセスしてみてください。
    • アプリは表示されなくなり、代わりに "You need to enable JavaScript to run this app." と表示されます。これは、アプリが静的なHTMLにプリレンダリングされていないためです。
まとめ:プリレンダリングとプリレンダリングなしの比較

簡単に図式化すると、次のようになります。

  • Next.jsありの場合

Initial Load: 事前読み込みされたHTMLが表示される
JavaScriptファイル読み込み
Hydration: React要素が初期化され、アプリがインタラクティブになる
例えば、インタラクティブな要素である<Link />があった場合、JSファイル読み込み後にアクティブになる

  • Next.jsを使用しない素のReact.jsアプリの場合

Initial Load: 画面には何も描画されない
JavaScriptファイル読み込み
Hydration: React要素が初期化され、アプリがインタラクティブになる(素のReactにはLinkコンポーネントはない)

4-4. Two Forms of Pre-rendering

  • 事前レンダリングには次の2種類があります。
    • ① Static Generation
      • プロダクション環境向けにアプリをビルドした際にHTML生成を行っておき、リクエストがきたらそのHTMLを再利用して返す方式
    • ② Server-side Rendering
      • 各リクエスト毎にHTML生成を行う方式

※留意事項

  • 開発環境(npm run dev実行時)においては、開発しやすくするために①, ②に関わらず、リクエスト毎にページは事前レンダリングされる。
  • 本番環境においては、①の方式を採用していれば、ビルド時に一度だけ事前レンダリングが行われる。

  • Next.jsでは、各ページに対して①, ②のどちらの方式を採用するか選択することができる

  • 一般的な構成ではほとんどのページが①Static Generation、その他のページを②Server-side Renderingで実現される
  • 一度ビルドすればCDNから提供され、各リクエストに対する応答がより高速であるということから可能な限り①を使うことが推奨される
  • ①Static Generationが適している具体なページの例としては下記がある

    • マーケティングページ
    • ブログ投稿ページ
    • Eコマース製品の一覧ページ
    • ヘルプページやドキュメント
  • 「ユーザのリクエストより前にこのページをレンダリングできるか?」という質問にYESなら①を採用すればよく、

  • 逆に、頻繁に更新されるデータを表示するページやリクエスト毎にコンテンツが変化するような場合は②を採用しましょう。
  • 次のレッスンでは、①Static Generationに焦点を当てて、データを伴う場合、データを伴わない場合について解説していきます。

4-5. Static Generation with and without Data

  • Static Generationはデータを伴う場合とデータを伴わない場合に分けて考えることができる
  • ここまでに作成したページは、外部データを取得しないもの

    • このようなページではプロダクション環境向けにアプリをビルドした時に自動的に静的にHTMLが生成されることになります。
  • 一方で、データ取得した後でなければ描画できないページも考えられる

  • 例えば、ビルド時に、ファイルシステムや外部API、データベースなどからデータを取得するような場合だ
  • Next.jsでは、データを伴うページの静的生成(Static Generation with data)にも対応できるようになっている
Static Generation with Data using getStaticProps
  • さて、データを伴うページの静的生成は、どのようにして動くのでしょうか?

    • Next.jsでは、ページコンポーネントと一緒にgetStaticPropsという非同期関数(async function)もエクスポートすることで実現されます。
  • 動作としては、

    • getStaticPropsがプロダクション環境向けのビルド時に実行され
    • 関数内で、外部データをfetchして、そのデータをpropsとしてページに渡せます
export default function Home(props) { ... }

export async function getStaticProps() {
  // ファイルシステム、API、DBなどからの外部データを取得する
  const data = ...

  // `props`をキーとする値が`Home`コンポーネントに渡される
  return {
    props: ...
  }
}

[プログラムの要点] - async functionとしてgetStaticPropsをexportして、その中で外部データを取得して、propsとしてリターンすると、Home componentに渡すことができる

  • 基本的に、getStaticPropsはNext.jsに次のように伝えます。

    • 「おい、このページにはいくつかのデータ依存性があるぞ。だから、ビルド時にこのページをプリレンダリングするときは、最初にそれらを解決しておけよ!」。
  • 注意: 開発モードでは、getStaticPropsは各リクエストで実行されます。

getStaticPropsを使ってみよう
  • 実際にやってみるとわかりやすいので、次のページからはgetStaticPropsを使ってブログを実装していきます。

4-6. Blog Data

シンプルなブログのアーキテクチャを作成する
マークダウンファイルの作成
  • ルートフォルダにnextjs-app/posts/という新しいディレクトリを作成する(これはpages/postsとは別物)
  • nextjs-app/posts/の中に、pre-rendering.mdssg-ssr.mdという2つのファイルを作成する

さて、以下のコードをposts/pre-rendering.mdにコピーしてください。

---
title: 'Two Forms of Pre-rendering'
date: '2020-01-01'
---

Next.js has two forms of pre-rendering: **Static Generation** and **Server-side Rendering**. The difference is in **when** it generates the HTML for a page.

- **Static Generation** is the pre-rendering method that generates the HTML at **build time**. The pre-rendered HTML is then _reused_ on each request.
- **Server-side Rendering** is the pre-rendering method that generates the HTML on **each request**.

Importantly, Next.js let's you **choose** which pre-rendering form to use for each page. You can create a "hybrid" Next.js app by using Static Generation for most pages and using Server-side Rendering for others.

そして、以下のコードをposts/ssg-ssr.mdにコピーしてください。

---
title: 'When to Use Static Generation v.s. Server-side Rendering'
date: '2020-01-02'
---

We recommend using **Static Generation** (with and without data) whenever possible because your page can be built once and served by CDN, which makes it much faster than having a server render the page on every request.

You can use Static Generation for many types of pages, including:

- Marketing pages
- Blog posts
- E-commerce product listings
- Help and documentation

You should ask yourself: "Can I pre-render this page **ahead** of a user's request?" If the answer is yes, then you should choose Static Generation.

On the other hand, Static Generation is **not** a good idea if you cannot pre-render a page ahead of a user's request. Maybe your page shows frequently updated data, and the page content changes on every request.

In that case, you can use **Server-Side Rendering**. It will be slower, but the pre-rendered page will always be up-to-date. Or you can skip pre-rendering and use client-side JavaScript to populate data.
  • Note
    • 各マークダウン・ファイルにはタイトルと日付を含むメタデータ・セクションが上部にある
    • これはYAML Front Matterと呼ばれ、gray-matterというライブラリを使って解析(パース)することができる
gray-matterのインストール
  • まず、各マークダウンファイルのメタデータをパースするためのgray-matterをインストールする
npm install gray-matter
ファイルシステムを読み込むユーティリティ関数の作成
  • ファイルシステムからデータをパースするための、次の処理を行えるユーティリティ関数を作成する
    • 各マークダウン・ファイルを解析し、タイトル、日付、ファイル名(投稿URLのidとして使用)を取得する
    • インデックスページにデータを日付順にリストアップする
    • ルートディレクトリにlib/というトップレベルのディレクトリを作成し、lib/posts.jsというファイルを作成し、以下のコードをコピー&ペーストする
      • ここで作成するlib/命名は任意。慣例としてlibutilsにすることが多い(ちなみに、pages/のようにNext.jsで決まっていて変更不可のものもあるので注意)
import fs from 'fs';  // Note: ファイルシステムからファイルを読み込むためのモジュール
import path from 'path';  // Note: ファイルのパスを操作するためのモジュール
import matter from 'gray-matter';  // Note: マークダウンファイルのメタデータをパースするためのライブラリ

const postsDirectory = path.join(process.cwd(), 'posts');

export function getSortedPostsData() {
  // ディレクトリ"/posts"配下のファイル名取得する
  const fileNames = fs.readdirSync(postsDirectory);
  const allPostsData = fileNames.map((fileName) => {
    // ファイル名をidとして扱うため、不要となる拡張子".md"をファイル名から削除する(正規表現を使用している)
    const id = fileName.replace(/\.md$/, '');

    // マークダウンファイルを文字列として読み込む
    const fullPath = path.join(postsDirectory, fileName);
    const fileContents = fs.readFileSync(fullPath, 'utf8');

    // 投稿のメタデータを解析するために"gray-matter"を使う
    const matterResult = matter(fileContents);

    // 取得したidとデータを紐付ける
    return {
      id,
      ...matterResult.data,
    };
  });
  // 投稿日でソートする
  return allPostsData.sort(({ date: a }, { date: b }) => {
    if (a < b) {
      return 1;
    } else if (a > b) {
      return -1;
    } else {
      return 0;
    }
  });
}
ブログデータの取得
  • ブログデータが解析されたので、それをインデックスページ (pages/index.js) に追加する必要がある
  • これを行うには、Next.jsのデータ取得メソッドであるgetStaticProps()を使用する
  • 次のセクションでは、getStaticProps()の実装方法について学びます。
4-6 まとめ
  • DBの位置付けとしてこのチュートリアルではマークダウンファイルを用意した
  • これらのファイルは、メタデータ(日付、タイトル)を含んでいる
  • マークダウン(のメタデータ部分)を解析するためにgray-matternpm installした
  • 解析処理を行うための関数getSortedPostsDataをエクスポートした
  • ここまでのステップでマークダウンファイルからpages/index.jsに掲載するidとデータを紐付けた内部データを扱えるようになった

  • 次のステップでは、Next.jsのデータ取得関数getStaticProps()を利用する方法を学習する

4-7. Implement getStaticProps

Next.jsのpre-rendering(事前レンダリング)には二通りあり、これらの違いは"HTMLがいつ生成されるか"にある

  • ① Static Generation (静的生成)

    • HTMLをビルド時に生成する方式で、リクエスト毎に再利用される
  • ② Server-side Rendering (サーバーサイドレンダリング)

    • HTMLを各リクエスト毎に毎回生成する方式

※ Next.jsにおいては、多くのページを①で実現し、その他残りのページを②で実現することで描画の効率を高めている

静的生成(getStaticProps())を利用する
  • さて、getSortedPostsDataのimportを追加して、pages/index.jsgetStaticPropsの中で呼び出す必要があります。
  • pages/index.jsをエディタで開き、エクスポートされたHomeコンポーネントの上に以下のコードを追加します。
import { getSortedPostsData } from '../lib/posts';

export async function getStaticProps() {
  const allPostsData = getSortedPostsData();
  return {
    props: {
      allPostsData,
    },
  };
}
  • getStaticPropspropsオブジェクトの中にallPostsDataを返すことで、ブログの記事がpropとしてHomeコンポーネントに渡されることになります。
  • これで、以下のようにブログ記事にアクセスすることができます。
export default function Home ({ allPostsData }) { ... }
  • ブログ記事を表示するために、Homeコンポーネントを更新し、自己紹介のあるセクションの下に、データを含む別の
    タグを追加しましょう。
  • props()から({ allPostsData })に変更することを忘れないでください。
export default function Home({ allPostsData }) {
  return (
    <Layout home>
      {/* Keep the existing code here */}

      {/* Add this <section> tag below the existing <section> tag */}
      <section className={`${utilStyles.headingMd} ${utilStyles.padding1px}`}>
        <h2 className={utilStyles.headingLg}>Blog</h2>
        <ul className={utilStyles.list}>
          {allPostsData.map(({ id, date, title }) => (
            <li className={utilStyles.listItem} key={id}>
              {title}
              <br />
              {id}
              <br />
              {date}
            </li>
          ))}
        </ul>
      </section>
    </Layout>
  );
}
  • これで、http://localhost:3000にアクセスすれば、ブログデータが表示されるはずです。

  • おめでとうございます。外部データを(ファイルシステムから)取得し、このデータでインデックスページをプリレンダリングすることに成功しました。

getStaticProps()

Markdownファイルの解析

ページコンポーネントにpropsとして投稿データの配列を渡す

インデックスページにブログ投稿のリストを表示する

  • 次のページでは、getStaticPropsの使い方のコツについて説明します。

4-8. getStaticProps Details

getStaticPropsの詳細
  • getStaticPropsについて知っておくべき重要な情報の紹介
外部APIやデータベースを取得する
  • lib/posts.jsでは、ファイルシステムからデータを取得するgetSortedPostsDataを実装しています。
  • しかし、あなたは外部APIエンドポイントのような他のソースからデータをフェッチすることができ、それはうまく動作します。
export async function getSortedPostsData() {
  // Instead of the file system,
  // fetch post data from an external API endpoint
  const res = await fetch('..');
  return res.json();
}

注意:Next.jsはクライアントとサーバーの両方でfetch()をポリフィルしています。インポートする必要はありません。

  • また、直接データベースに問い合わせることもできます。
import someDatabaseSDK from 'someDatabaseSDK'

const databaseClient = someDatabaseSDK.createClient(...)

export async function getSortedPostsData() {
  // Instead of the file system,
  // fetch post data from a database
  return databaseClient.query('SELECT posts...')
}
  • これは、getStaticPropsがサーバーサイドでのみ実行されるためです。
  • クライアントサイドでは決して実行されません。ブラウザ用のJSバンドルに含まれることもありません。
  • つまり、データベースへの直接問い合わせのようなコードを、ブラウザに送信することなく書くことができるのです。
開発環境と本番環境
  • 開発環境(npm run devまたはyarn dev)では、getStaticPropsはすべてのリクエストで実行されます。
  • 実運用環境では、getStaticPropsはビルド時に実行されます。
  • しかし、この動作はgetStaticPathsが返すフォールバックキーを使って拡張することができます。
  • これはビルド時に実行されるものなので、 クエリパラメータや HTTP ヘッダのようなリクエスト時にしか使用できないデータは使用できません。
ページ内でのみ許可される
  • getStaticPropsは、ページからしかエクスポートできません。ページ以外のファイルから書き出すことはできません。

  • この制限の理由の1つは、Reactはページがレンダリングされる前に必要なデータをすべて持っている必要があるためです。

リクエスト時にデータを取得する必要がある場合はどうすればよいですか?
  • 静的生成はビルド時に一度だけ行われるため、頻繁に更新されるデータや、ユーザーのリクエストごとに変更されるデータには適していません。

  • このような、データが変化する可能性がある場合は、サーバーサイド・レンダリングを使用することができます。

  • サーバーサイド・レンダリングについては、次のセクションで詳しく説明します。
4-8 まとめ
  • getStaticPropsがサーバサイドで実行されるため、ファイルシステムやデータベースからの情報の読み込みを実現できる
  • 開発環境では、getStaticPropsはリクエスト毎に実行されるが、本番環境ではビルド時のみ実行される(ただし、fallbackキーを使って拡張可能)
  • getStaticPropsはページファイルからのみエクスポートできる
  • 頻繁に更新が必要なページにはこの手法は向いていないため、以降に紹介されるServer-sdie Renderingを利用する
    • 言い換えると、ビルド時ではなく、リクエスト時にデータの取得の必要があるならばServer-sdie Renderingを利用すべき

4-9. Fetching Data at Request Time

ビルド時ではなく、リクエスト時にデータを取得する必要がある場合は、サーバーサイドレンダリングを試すことができます。

サーバーサイド・レンダリングを使用するには、ページからgetStaticPropsの代わりにgetServerSidePropsをエクスポートする必要があります。

getServerSidePropsの使用法

getServerSidePropsのスターターコードです。このブログの例では必要ないので、実装しないことにします。

export async function getServerSideProps(context) {
  return {
    props: {
      // props for your component
    },
  };
}

getServerSidePropsはリクエスト時に呼び出されるため、そのパラメータ(context)にはリクエスト固有のパラメータが含まれます。

getServerSideProps は、リクエスト時にデータを取得しなければならないページをプリレンダリングする必要がある場合にのみ、使用する必要があります。TTFB (Time to First Byte) は getStaticProps よりも遅くなります。なぜなら、サーバーはリクエストごとに結果を計算しなければならず、その結果は特別な設定なしにCDNによってキャッシュされることができないからです。

クライアントサイドレンダリング

データをプリレンダリングする必要がない場合は、次の戦略(Client-side Renderingと呼ばれる)を使用することもできます。

外部データを必要としないページの部分を静的に生成(プリレンダリング)します。 ページが読み込まれたら、JavaScriptを使用してクライアントから外部データを取得し、残りの部分にデータを入力します。

この方法は、たとえば、ユーザーのダッシュボード・ページで有効です。ダッシュボードはプライベートなユーザー固有のページなので、SEOは関係なく、ページがプリレンダリングされる必要もありません。データは頻繁に更新されるため、リクエスト時にデータを取得する必要があります。

SWR

Next.jsの開発チームは、SWRと呼ばれるデータフェッチ用のReactフックを作成しました。クライアントサイドでデータを取得する場合は、これを強くお勧めします。SWRは、キャッシュ、再バリデーション、フォーカストラッキング、インターバルでのリフェッチなどを処理します。ここでは詳細を説明しませんが、使用例を示します。

import useSWR from 'swr';

function Profile() {
  const { data, error } = useSWR('/api/user', fetch);

  if (error) return <div>failed to load</div>;
  if (!data) return <div>loading...</div>;
  return <div>hello {data.name}!</div>;
}

詳しくは、SWRのドキュメントをご覧ください。

以上です。 次のレッスンでは、ダイナミックルートを使って各ブログ投稿のページを作成します。