Chapter 02

静的なページから始めよう

oubakiou
oubakiou
2021.10.30に更新

静的なページから始めよう

先ずはデプロイ(ビルド)時にページ内容が静的に固定されるシンプルなアプリケーションから始めましょう。

本当の意味で静的なページだけで良いのであればNext.jsのexportコマンドによって生成されたHTML等のファイルを配信するだけで良いのですが、我々の作るアプリケーションは最終的にはSNSになるので何らかの形で動的コンテンツを扱う必要があります。

そこで本書では段階的にアプリケーションの改築を進め、最終段階ではSSRを基本としつつもSWR(Stale-While-Revalidate)対応のCDNを組み合わせる事で高パフォーマンスの実現を目指します。SWR方式については下巻で詳しく触れますが「先ずはキャッシュをユーザーへ返し、その裏側で自動的にリモートの最新データをチェックしてユーザーへ改めて返す方式」という理解で今は大丈夫です。

コラム:SSR? SSG? コンテンツ生成方式はどれを選べばいい?

Next.jsはパフォーマンスに優れたSSGを第一選択として推奨しています。しかし対象となるページの特性によっては必ずしもSSGが採用できるわけではありません。

SSR(Server Side Rendering)

WordPressのようなCMSやRuby on Rails等で作られた一般的なWebサービスの多くもこれに該当し、Next.jsにおいてはgetServerSidePropsを利用したページが該当します。ブラウザからのリクエストを受けたサーバーがその都度リクエスト内容に応じたHTMLを動的に生成してブラウザへ返すという仕組みです。ただしSSRの場合でもユーザーの操作に応じて部分的にCSRを行ったり、CDN等を経由した配信ではSSGと動作モデルが似るなど、画一的に分類できるわけではありません。

Next.jsの魅力の一つは「SSRはERB(Ruby)だけどCSRはjQuery(JavaScript)」のようにそれぞれ異なる言語で二重管理されていたViewをどちらも同じ言語(JavaScript)の同じViewライブラリ(React)で統一できるという点で、この形態はUniversal/Isomorphic JavaScriptと呼ばれる事があります。なお本書では扱いませんが、逆にサーバーサイド言語へViewを一本化するLivewireHotWireのようなアプローチもあります。

CSR(Client Side Rendering)

コンテンツ更新頻度の極端に高いチャットアプリケーションや複雑でインタラクティブなUIを持ったアプリケーション色の強いサービスなどで採用される事が多い、よりネイティブアプリケーションに近い動作モデルの方式です。サーバーサイドから完成済のHTMLをブラウザが受け取りそのままレンダリングをするのではなく、JSON等の形式でデータを受け取ったブラウザがクライアントサイドでHTMLを組み立ててレンダリングします。

また純粋にCSRのみで構成されたアプリケーションはSPA(Single Page Application)と呼ばれる事もあります。SPAの場合JavaScriptが動作しないとページとして機能しないためJavaScriptが十分にサポートされていないクライアント、例えばOGPタグを取得しに来たBOT等が機能しない事があります。また完成品のHTMLを(場合によってはCDNのような共有キャッシュから)受け取るSSRに対してCore Web VitalsのLCP等で不利になりやすいという側面もあります。そういった要件のない管理画面など、認証を必要とするアプリケーションもまたSPA向きだと言えるでしょう。

SSG(Static Site Generation)

Movable TypeのようなCMSやJekyll等で採用されている方式でNext.jsにおいてはgetStaticPropsやgetStaticPathsを利用したページが該当します。サーバーサイドでHTMLを組み立てるという点ではSSRと同一ですが、リクエストがあったタイミングではなくビルドと呼ばれる作業で事前にコンテンツを静的なファイルとして生成してから配信するため、配信自体には複雑な仕組みを必要とせず高速で安全な配信を実現しやすいという特長があります。SSGによって生成される静的ファイルをキャッシュの一種だと見做すなら、SSGはCDNやVarnishのようなHTTPアクセラレータの親戚だとも言えるでしょう。

SSG単独では更新頻度の高いページや、性質上事前生成が難しいページ(例えばログインしたユーザーにのみ表示されるページ、検索条件と並び順の組合せで爆発的にページ数が増える検索結果ページ等)に対応できないため、CSR等と組み合わせて利用される事もあります。

ISG(Incremental Static Generation)

SSGにおいて静的ページをまとめてではなく徐々に生成する方式でNext.jsにおいてはgetStaticPathsでfallbackオプションをblockingtrueにする事で利用できます。通常10万件の商品を持ったECサイトで素直にSSGする場合、10万件の商品ページを一度に生成しようとして莫大なビルド時間がかかってしまいますが、そのページが必要になったタイミングで徐々に生成する事でこれを回避する仕組みです。

ISR(Incremental Static Regeneration)

SSGにおいて一度生成された静的ページを徐々に最新の内容へ更新する方式でNext.jsにおいてはgetStaticPropsの返り値でrevalidateに秒数を指定する事でSWR(stale-while-revalidate)方式での再生成をする事ができます。

事前に準備しておくもの

本書では開発環境としてmacOSVisual Studio CodeChromeを想定し、また下記が事前にインストールされているものとしますが、手に馴染んだ代替ソフトウェアがあるのであれば必ずしも本書の構成に従う必要はありません。また記載のバージョンは本書で動作確認を行ったバージョンです。

Node.js v16.13.0

本書ではJavaScript実行環境としてNode.jsが既にインストールされているものとします。

npm / npx v8.1.2

JavaScript向けパッケージマネージャのnpmおよびnpxが既にインストールされているものとします。通常はどちらもNode.jsに同梱されているためNode.jsをインストールした時点で特に意識していなくても利用可能になっています。

direnv

ディレクトリ単位での環境変数管理ツールとしてdirenvが既にインストールされているものとします。

vscode-eslint

コードを解析して不適切な箇所があれば教えてくれるlinter(ESLint)をVSCodeから利用するための拡張機能です。

prettier-vscode

コードを綺麗に整形してくれるformatter(Prettier)をVSCodeから利用するための拡張機能です。

Debugger for Chrome

VSCodeからChromeへ接続しデバッグするための拡張機能です。

graphql/vscode-graphql

GraphQLのシンタックスハイライトや入力補完などを提供する拡張機能です。

prisma/vscode

Prismaで扱うPrisma Schema Language(PSL)のシンタックスハイライトなどを提供する拡張機能です。

React Developer Tools

Reactコンポーネントのインスペクションやプロファイルが出来るChrome向け拡張機能です。

Docker Desktop

本書では何らかのDockerコンテナの実行環境が既にインストールされているものとします。

MySQL Workbench

本書ではMySQLの動作確認のためにMySQL Workbenchを使う事がありますが、CUIでの操作や他のソフトウェアで代用しても構いません。

コラム:コンテナ技術、例えばDockerを利用するべきか?

Docker(Docker Engine)はコンテナという単位で仮想環境を構築したり実行したりできる製品です。コンテナ仮想化はマシン単位で仮想化する旧来方式と比べて軽量で起動も速く、Dockerfileによるイメージの作成や共有が容易などの特徴から利用が広まりました。本書で扱うような開発環境の構築においてもDockerを利用すると、マシン本体の環境を汚さずに隔離されたコンテナの中で作業が完結できたり、異なるマシン間や異なるOS間での環境再現が容易といったメリットがあります。

その一方でmacOSにおいてDockerを利用する場合はファイルアクセスの性能があまり良くなく、頻繁なファイルアクセスが必要な用途では開発体験が悪化する場合があります。このため本書では基本的にはDockerを利用していませんが、開発環境用のMySQLでは利用しています。また下巻CI(Continuous Integration)の章でも触れる予定です。

create-next-appを実行してアプリケーションの土台を生成しよう

create-next-appはNext.jsアプリケーションの土台を生成するオフィシャルなCLIツールです。それではさっそく好きな作業ディレクトリで下記を実行してみましょう。

npx -y create-next-app --use-npm --example with-firebase-hosting helloworld-app

コマンドの実行が正常に終わるとhelloworld-appという名前のディレクトリが生成されます。

npxコマンドはnpmパッケージ等の形でインターネット上に公開されているJavaScript製CLIツールを実行するための便利なコマンドです。マシンにインストールされていないリモートパッケージを実行しようとした場合、npxがパッケージの一時的なインストールの確認を求めてきますが-yオプションを指定する事でこれをスキップしています。

またcreate-next-appに--exampleを指定する事で公式exampleからアプリケーションの土台となる雛形を選択できるので、今回はその中からwith-firebase-hostingを利用してhelloworld-appという名前で土台を生成しています。

TypeScriptで書く準備をしよう

with-firebase-hostingはJavaScriptで書かれているため、先ずはTypeScriptで開発ができるよう設定しましょう。helloworld-appディレクトリへ移動して下記を実行します。

cd helloworld-app
npm install --save-dev typescript @types/react @types/node eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-jsx-a11y prettier eslint-config-prettier

実行が終わるとこのプロジェクトの開発環境向けにTypeScript本体、React及びNode.jsの型定義、ESLintとPrettierがインストールされます。具体的にはnode_modulesディレクトリにパッケージの実体が保存されpackage.jsonのdevDependenciesへその構成情報が保存されます。

またESLintの設定ファイルとしてプロジェクトのルートディレクトリに下記を作成しましょう。

.eslintrc.json
{
  "root": true,
  "parser": "@typescript-eslint/parser",
  "plugins": [
    "@typescript-eslint"
  ],
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:react/recommended",
    "plugin:react-hooks/recommended",
    "plugin:jsx-a11y/recommended",
    "prettier"
  ],
  "rules": {
    "react/prop-types": "off"
  }
}

(本書ではtypescript-eslintのREADMEに記載されている最小設定を下敷きにしていますが、あなたの好みやチームの規約に応じて修正を行っても問題ありません。)

続けてPrettierの設定ファイルとして下記を作成しましょう。

.prettierrc.json
{
  "singleQuote": true,
  "semi": false
}

(本書ではNext.jsの.prettierrc.jsonを下敷きにしていますが、あなたの好みやチームの規約に応じて修正を行っても問題ありません。)

ファイル保存時に実行するデフォルトフォーマッターがPrettierになるよう、また

  • このプロジェクトのnode_modulesにインストールしたTypeScriptを利用する
  • 相対パスではなく絶対パスでauto import(import文の自動追加)する

ようVSCodeの設定として下記を作成しましょう。

.vscode/settings.json
{
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.formatOnSave": true,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
  "typescript.tsdk": "node_modules/typescript/lib",
  "typescript.preferences.importModuleSpecifier": "non-relative"
}

一緒にVSCodeから使うデバッガ用の設定も作成します。typeがnodeになっている部分はサーバーサイド用、typeがchromeになっている部分はDebugger for Chrome拡張を利用したクライアントサイド用の設定です。

.vscode/launch.json
// @see https://github.com/vercel/next.js/issues/16442
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "attach",
      "name": "Next: Node",
      "skipFiles": ["<node_internals>/**"],
      "cwd": "${workspaceFolder}/src",
      "port": 9229
    },
    {
      "type": "chrome",
      "request": "launch",
      "name": "Next: Chrome",
      "url": "http://localhost:3000",
      "webRoot": "${workspaceFolder}/src",
      "sourceMaps": true,
      "sourceMapPathOverrides": {
        "webpack://_N_E/*": "${webRoot}/*"
      }
    }
  ]
}

一通り設定ファイルの作成が終わったら、pagesディレクトリにjsxという拡張子で存在しているコンポーネントの一つをtsxという拡張子に変更した上でNext.jsの開発環境を起動します。

mv src/pages/index.jsx src/pages/index.tsx
npm run dev

npm runはそのプロジェクトのpackage.jsonのscripts内で定義された処理を実行するコマンドですが、このexampleではnpm run devで開発環境が起動するよう最初から定義されています。

package.json
  "scripts": {
-    "dev": "next src/",
+    "dev": "NODE_OPTIONS='--inspect' next src/",

デバッガを使うため次回からinspectオプションが有効になるよう修正しておきましょう。

We detected TypeScript in your project and created a tsconfig.json file for you.

というメッセージが表示されれば、tsx(TypeScript)ファイルの存在を検知したNext.jsによってtsconfig.json(TypeScriptの設定ファイル)が自動生成されているので中身を確認してみましょう。

src/tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
-    "strict": false,
+    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
+    "baseUrl": ".",
+    "paths": {
+      "@/*": ["components/*"]
+    }    
  },
  "include": [
    "next-env.d.ts",
    "**/*.ts",
    "**/*.tsx"
  ],
  "exclude": [
    "node_modules"
  ]
}

Next.jsが開発環境での実行時に自動生成するtsconfig.jsonはstrictオプションが無効化されているため忘れずにstrictオプションを有効化しておきます。またコンポーネントのimport文が書きやすくなるようパスエイリアスの設定も追加しておきましょう。

それではブラウザからNext.jsの開発環境へアクセスして動作しているか確認してみましょう。ブラウザのアドレス欄にlocalhost:3000と入力します。

HomeAboutという2つのページを持ったシンプルなアプリケーションが表示されたでしょうか。

React Developer Toolsを使ってみよう

React Developer Toolsの動作も確認しておきましょう。F12キー等でデベロッパーツールを表示するとComponentsProfilerという2つのタブが追加されているはずです。

Componentsタブではブラウザ標準のデベロッパーツールでDOM要素をインスペクションするのと同様の操作で、Reactコンポーネントやそのprops(コンポーネントの引数)などをインスペクションする事ができます。

またReact Developer Toolsの設定でHighlight updates when components render.にチェックを入れるとコンポーネントの再レンダリング発生時にハイライト表示をしてくれます。なお青色でのハイライトは最も低頻度、赤色でのハイライトは最も高頻度な再レンダリングの発生を意味しています。

Next.jsを触ってみよう

先ほどファイルの拡張子だけ変更しておいたsrc/pages/index.tsxをエディタで改めて開いてみましょう。

src/pages/index.tsx
import App from '../components/App'

export default function Home() {
  return (
    <App>
      <p>Index Page</p>
    </App>
  )
}

Reactを触ったことが無ければ戸惑うかもしれませんが<App>...</App>という見慣れないタグはimport文が指し示すようにsrc/components/App.jsx(またはtsx)に実体が存在するAppという名前の独自コンポーネントです。またこのsrc/pages/index.tsxはHomeという名前の関数として定義されていますが、このように関数で表現されたReactコンポーネントは関数コンポーネント(function component)、クラスで表現されたReactコンポーネントはクラスコンポーネントと呼ばれます。本書では原則として前者の関数コンポーネントのみを扱います。

Next.jsではpagesディレクトリに配置したReactコンポーネント(本書ではページコンポーネントと呼称します)とURLとを自動で対応させるファイルベースのルーターが提供されているため、/というパスに対してブラウザ等からのアクセスがあるとこのsrc/pages/index.tsxに定義されたページコンポーネントが表示される事になります。

なおVSCode上でESLintが正常に動作していれば下記のような警告がいくつか表示されているかもしれません。

例えばこれはHome関数(exportされている関数)の返り値の型が明示的に書かれていない事に対する警告です。以下のように変更し、ついでにpタグで囲まれたIndex Pageという文章を好きなものへ変更してみましょう。

src/pages/index.tsx
+import React from 'react'
import App from '../components/App'

-export default function Home() {
+export default function Home(): JSX.Element {
  return (
    <App>
 -      <p>Index Page</p>
+      <p>出来る!100%TypeScriptとサーバーレス環境でSNSを作るチュートリアル(上) - GCP編</p>
    </App>
  )
}

VSCode上の警告は消えたでしょうか?続けてブラウザでの表示も確認してみましょう。

コンポーネントの変更を保存するとFast Refreshと呼ばれる機能によってブラウザ上にも自動で変更が反映されます。

VSCodeからデバッガを使ってみよう

この章の締めくくりにデバッガのブレークポイントを使ってみましょう。先ずは以下のようにコードを変更します。

src/pages/index.tsx
import React from 'react'
import App from '../components/App'

export default function Home(): JSX.Element {
+  const test = 1
+  
  return (
    <App>
      <p>
        出来る!100%TypeScriptとサーバーレス環境でSNSを作るチュートリアル(上) -
        GCP編
      </p>
    </App>
  )
}

続けて開発サーバーを起動します。

npm run dev

前回の起動時とは違いinspectオプションが有効になっているため下記のようなメッセージが表示されるはずです。

> with-firebase-hosting@5.0.0 dev
> NODE_OPTIONS='--inspect' next src/

Debugger listening on ws://127.0.0.1:9229/*****
For help, see: https://nodejs.org/en/docs/inspector

それではVSCode上でreturn行の端をクリックして赤丸(ブレークポイント)をつけましょう。

続けてVSCodeの左端ペインから「実行とデバッグ」へ進み、launch.jsonで設定した構成の中から今回は「Next: Chrome」を選択し緑色の三角形(デバッグを開始)ボタンをクリックしましょう。

デバッグを開始するとChromeが起動しますが今回ブレークポイントを設置したのはページコンポーネントが返る前の位置なので白画面が表示され、VSCodeにはブレークポイント時点での変数の一覧などが表示されているはずです。

またブレークポイントで止めた箇所からステップイン(実行を1行進める)等の操作も可能です。

今回はGUI操作でデバッグを実行しましたがF5キーを押す事でも手軽にデバッグの実行はできます。ステップインやステップオーバーといった操作にも同様のショートカットキーは設定されているので、慣れや好みで使い分けると良いでしょう。