2020年師走における Next.js をベースとしたフロントエンドの環境構築

34 min read読了の目安(約31200字

さて、今年ももう終盤。12 月ですね。
今年のはじめに考えていたことが出来たのかといえば、出来ていないというのが正直なところです。とはいえ、別に後悔はなく、やりたいことが増えて優先順位を大きく変えただけの話ですかね。まあ、残された時間は有限ですので、その辺のコントロールもしなきゃなと思いつつダラダラと過ごしている日々です。

そして 12 月といえば、そうですアドベントカレンダーですね。
実は私、今年の 3 月頃に以下の記事を書いたんですよ。

https://qiita.com/syuji-higa/items/931e44046c17f53b432b

それをアップデートしたのがこの記事であり、Next.js Advent Calendar 2020 3 日目の記事となります。

一部修正というよりは、ほとんど書き直しています。たった 9 ヶ月ほど前の記事なのですが、既に古くなっていると感じたがゆえの書き直しです。構成自体はそこまでは大きくは変わらないのですが、Next.js そのものや個々のモジュールの使い方などなど細々と変わってきています。

全体的な印象ですが、以前よりも簡単に構築できるようなりました。
以前の記事の公開後に気になっていた箇所もあったのですが、実際に記事を書き直しながら環境構築を進めてみて、前よりも使いやすくなったと感じる部分もあります。書き直してよかったと思える内容にはなりました。

Next.js とえいば、3 月頃は SSG(静的サイト生成)が実装されたことで盛り上がっていましたね。たまたま記事を書いていた私として丁度よいタイミングだったと記憶しています。それからも次々とアップデートを重ねて、Next.js Conf も開催されてより一層の盛り上がりをみせているようにも感じます。

特定のフレームワークなどに固執しすぎることは良いことだとは思いませんが、Next.js 自体は好きであり良いものであるとは思っていますので現状は喜ばしいです。また、だからこそ熱量をもって記事を書くことが出来たのかなと。まあ、最新の機能はサラッと見た程度なので試せていないのが多いですが。

それでは、相変わらず長い記事ではありますが、楽しみながら読んで試してもらえればと思います。

技術選定

基本的には最新かつ人気のある構成を目指して選定していますが、まあ、好みの部分も大きいです。
できるだけ必要なものだけを追加したり、別のものを使用できるように記載はしているつもりですが、TypeScript などの基礎となるものを省くとけっこう分かりづらくなるかもです。

バージョンはこの記事を書くために構築した環境のものを、そのまま記載しています。

構築手順

セクションごとに細かく分けて記述していますので、ひとつひとつは難しいものではないかと思います。私の記載ミスとか、環境依存またはモジュールのアップデートなどによるエラーなどが発生する場合はコメントいただけると助かります。

Linux コマンドでファイルの作成などをしていきますので、windows の方などは適宜読み替えてください。

構築済みのプロジェクトのリポジトリを以下に用意していますので、参考にするなり、そのまま使うなりしてもらえればと思います。

https://github.com/syuji-higa/template-nextjs-2020-december

1. プロジェクトを作成

それではプロジェクトを作成していきますが、表題にあるとおり Next.js を使います。Next.js とは JavaScript のライブラリである React のフレームワークです。数年前までは薄いフレームワークといった印象でしたが、現在では Nuxt.js に負けないくらい様々な機能が初期状態から追加されています。

Next.js のプロジェクトをセットアップします。

$ yarn create next-app .

今回は現在のディレクトリで作成しますので、あらかじめ空のプロジェクトディレクトリを用意している想定です。

2. ベースディレクトリを変更

Next.js で v9.1 から srcpages などを入れることができます。のちのち複雑になる可能性を考えて src ディレクトリへ移動しておくことにします。

2-1. ディレクトリを移動

pagesstylessrc のディレクトリ内に移動します。

$ mkdir src && \
  mv pages/ src/pages & \
  mv styles/ src/styles

3. TypeScript に対応

TypeScript は簡単にいうと JavaScript に静的型付けを加えたスーパーセットです。高い保守性と堅牢性を得ることでプロジェクトを健全に保ちやすくなります。

3-1. インストール

TypeScript 関連のモジュールをインストールします。

$ yarn add -D typescript @types/react @types/react-dom @types/node

3-2. 設定ファイルなどを追加

開発サーバを起動することで tsconfig.json, next-env.d.ts を自動生成します。

$ yarn dev

生成されたら開発サーバを終了します。

3-3. TS ファイルに変換

src ディレクトリ配下の JS ファイルを TS ファイルに変換します。

$ find src/pages -name "_app.js" -or -name "index.js" | sed 'p;s/.js$/.tsx/' | xargs -n2 mv & \
  find src/pages/api -name "*.js" | sed 'p;s/.js$/.ts/' | xargs -n2 mv

3-4. App コンポーネントを変更

App コンポーネントを TypeScript に対応します。

src/pages/_app.jsx
// React と AppProps を読み込む
import React from 'react'
import { AppProps } from 'next/app'

// 引数に型を追加する
function MyApp({ Component, pageProps }: AppProps): JSX.Element {
  // 関数の内容はそのまま
}

3-5. ページコンポーネントを変更

ページコンポーネントを TypeScript に対応します。

src/pages/index.jsx
// React と NextPage を読み込む
import React from 'react'
import { NextPage } from 'next'

// 型を追加
const Home: NextPage = () => {
  // 関数の内容はそのまま
}

// export を分離
export default Home

3-6. API を変更

API を TypeScript に対応します。

src/pages/api/hello.ts
// レスポンスの型を追加
type Response = {
  statusCode: number
  json({ name: string }): void
}

// 型を指定&使用していない引数にアンダースコア接頭詞を追加
export default (_req: void, res: Response): void => {
  // 関数の内容はそのまま
}

API を使わないのであれば、ファイルの変更をせずに src/pages/api/ フォルダごと削除しても良いかと思います。

4. Document コンポーネントを追加

Document コンポーネント を使うと、初期状態だと自動で追加される <html><body> に変更を加えることができます。

まずは、Document コンポーネントを作成します。

$ touch src/pages/_document.jsx

作成した Document コンポーネントに以下を記述します。

src/pages/_document.jsx
import React from 'react'
import Document, { Html, Head, Main, NextScript } from 'next/document'

interface MyDocumentInterface {
  url: string
  title: string
  description: string
}

class MyDocument extends Document implements MyDocumentInterface {
  url = 'https://example.com'
  title = 'Demo Next.js'
  description = 'Demo of Next.js'

  render(): JSX.Element {
    return (
      <Html lang="ja-JP">
        <Head>
          {/* `<Head>` の内容は必要に応じて変更 */}
          <meta name="description" content={this.description} />
          <meta name="theme-color" content="#333" />
          <meta property="og:type" content="website" />
          <meta property="og:title" content={this.title} />
          <meta property="og:url" content={this.url} />
          <meta property="og:description" content={this.description} />
          <meta property="og:site_name" content={this.title} />
          <meta property="og:image" content={`${this.url}/ogp.png`} />
          <meta name="twitter:card" content="summary_large_image" />
          <meta name="format-detection" content="telephone=no" />
          <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    )
  }
}

export default MyDocument

<Head> の内容は個人的によく記述するものを記載していますので、プロジェクトに合わせて変更してください。ファビコンなど Next.js が自動で挿入しているものもありますので、必要なタグがあれば確認した上で追加するとよいです。

5. ベース URL を設定

srcpages ディレクトリなどを移動しましたので、src をベース URL に設定します。

よくみるような、~@ をエイリアスとすることもできます。

ただ、設定ファイルが複雑になりそうでしたので、今回はベース URL の設定だけにしておきました。インポート時の記述も短く書くことができますし。

5-1. TypeScript の設定を変更

TypeScript の設定にモジュールインポートのベース URL を追記します。

tsconfig.json
{
  "compilerOptions": {
    // ベース URL を追加
    "baseUrl": "src"
  }
}

5-2. 各コンポーネントを変更

各コンポーネントのモジュールインポートの指定を、ベース URL 指定に変更します。

$ sed -i '' -e 's/..\/styles/styles/' src/pages/_app.tsx & \
  sed -i '' -e 's/..\/styles/styles/' src/pages/index.tsx

6. PWA に対応

PWA は Progressive Web Apps の略称で、クロスプラットフォームのウェブアプリケーションのことです。PWA の利点を引用すると以下のように記載されています。

PWA は発見でき、インストールでき、リンクでき、ネットワークに依存せず、プログレッシブで、再エンゲージでき、レスポンシブで、安全です。

前の記事では next-offline を使いましたが、執筆時点での公式の FAQ から参照されている 参考の実装では、next-pwa が使われていました
なかなか設定するのが面倒と感じていましたが、だいぶ簡単になった気がします。

6-1. インストール

PWA のモジュールをインストールします。

$ yarn add next-pwa

6-1. 設定ファイルの追加

まずは、Next.js の設定ファイルがまだ無いかと思いますので作成します。

$ touch next.config.js

作成した設定ファイルに以下を記述します。

next.config.js
const withPWA = require('next-pwa')

module.exports = withPWA({
  pwa: {
    dest: 'public'
  }
})

6-2. ウェブアプリマニフェストを追加

  1. Web App Manifest Generator でウェブアプリマニフェスト関連のファイルを作成します
  2. 作成した manifest.jsonimages フォルダを public 直下に設置します

6-3. Document コンポーネントを変更

Document コンポーネントにマニュフェストへのリンクを追記します。

src/pages/_document.tsx
<Head>
  <link rel="manifest" href="/manifest.json" />
</Head>

6-4. 無視ファイルを追加

gitignore にリポジトリで管理しないファイルを追記します。

.gitignore
# PWA 関連ファイルの自動生成ファイルを追加
**/public/precache.*.*.js
**/public/sw.js
**/public/workbox-*.js
**/public/worker-*.js
**/public/precache.*.*.js.map
**/public/sw.js.map
**/public/workbox-*.js.map
**/public/worker-*.js.map

7. 状態管理ライブラリを追加

前回の記事では状態管理に Redux Toolkit を追加しましたが、これについては様々な新しい選択肢がでてきている状態かと思います。

今回はこの項目を省こうかとも思ったのですが、ある程度の規模になると何かしら状態管理ライブラリを入れることにはなるかと思います。ですので、一応なにかしらを追加してみます。

Redux Toolkit 以外の選択肢ですと、例えば以下などですかね。

状態管理ライブラリを追加する必要があるのであれば、それぞれの状況に合わせ、ライブラリの特徴を確認したうえで慎重に選択してください。

今回は個人的に使いたいと思っている Recoil を追加してみます。

Recoil は Facebook が作成したもので、まだ現時点ではバージョンが 0.1.2 と正式リリースされていません。詳細は特にお話しませんが、紹介している記事などが結構ありますので調べてみてください。

7-1. Recoil のインストール

Recoil をインストールします。

$ yarn add recoil

使い方はドキュメントを確認してください。

8. スタイルの設定

Next.js はデフォルトで CSS Modules に対応しています。

CSS-in-JS を使うこともでき、選択肢としては以下などがあります。

今回はなにを選ぶか悩みましたが決めることができなかったので、デフォルトの CSS Modules のままにしておきます。

CSS Modules と CSS-in-JS どちらを選ぶかについて

CSS-in-JS は同じ JS ファイル内に記述できることもあり楽には感じますが、それによるいくつかの問題があります。解決することができるものもありますが、それを解決していくことの辛みが伴いますし、そのような問題は新しく機能を拡張をする際にもついて回ってくることが多い気がします。

それであれば、CSS Modules を使うほうが良いのではないか?という考え方もあります。しかし、CSS Modules にも全く問題は無いわけではないですので、それぞれのメリットやデメリットを比較してどちらを選ぶのかが重要かと思います。

詳細は省きますが、個人的には以下の記事が分かりやすかったです。

🔗 styled-components(CSS in JS)をやめた理由と、不完全なCSS Modulesを愛する方法

つぎに、CSS プリプロセッサについてです。
有名どころですと SASSLESSStylus があります。

上記の CSS プリプロセッサの設定方法はドキュメントに記載されています。

もしくは、PostCSS などで標準仕様にそった新しい機能のみを追加する方法もあります。

PostCSS の設定方法もドキュメントに記載されています。

今回は使用している割合が比較的に高そうな SASS を追加してみます。

CSS プリプロセッサを使用するかどうかについて

今どきのブラウザを対象とするのであれば、特に追加しないでも良いんじゃないかなとも思えてきています。ベンダープレフィックスが必要なプロパティもほぼ無いですし、カスタムプロパティ(カスケード変数)も使えますので。
ただ、mixins などの便利な機能もありますので、CSS プリプロセッサを使うのも視野に入れたうえで検討するとよいです。

8-1. SASS のインストール

SASS をインストールします。

$ yarn add -D sass

8-2. SASS ファイルに変換

src/styles ディレクトリ内の CSS ファイルを SASS ファイルに変換します。

$ find src/styles -name "*.css" | sed 'p;s/.css$/.scss/' | xargs -n2 mv

8-3. SASS ファイルを読み込むように変更

CSS ファイルを SASS ファイルに変換しましたので、正しく読み込めるようにコンポーネントを変更します。

$ sed -i '' -e 's/\.css/\.scss/' src/pages/_app.tsx & \
  sed -i '' -e 's/\.css/\.scss/' src/pages/index.tsx

9. デフォルト CSS の追加

ブラウザ間の誤差を吸収する為や、スタイルを追加しやすくする為に、デフォルト CSS はあります。
デフォルトスタイルについては、必要な箇所でだけ自前で記載してもよいかもですが、デフォルト CSS があったほうが楽かなとは思います。

ここ数年おおきな変化は無い気がしますが、選択肢としては以下などですかね。

比較的新しい(とはいえ十分に枯れている)し思想も好きですので、今回は sanitize.css を使用します。

どのデフォルト CSS を使用するのかについて

単純なスタイルの当てやすさだけであれば reset.css が楽なのかもしれません。ただ、そうなると全てのブラウザのデフォルトスタイルを上書きすることになるので、無駄が発生します。

normalize.css であれば必要な箇所にだけスタイルを当てることができます。

sanitize.css は normalize.css 上位互換という感じで、使いやすくなるように一部のスタイルを調整してくれています。ただし、その分の記述量が増えています。

それぞれ一長一短がありますので、プロジェクトに合わせて選択するとよいです。

9-1. インストール

sanitize.css をインストールします。

$ yarn add -D sanitize.css

9-2. App コンポーネントを変更

デフォルト CSS を全体に適応する為に、App コンポーネントで sanitize.css を読み込みます。

src/pages/_app.tsx
// sanitize.css を読み込む
import 'sanitize.css'

10. 静的解析と整形のツールを追加

コードの静的解析と整形の為に、以下のツールを追加します。

それぞれの担っている内容が重複しているところもありますが、コンフリクトしないように設定して導入するとよいです。

10-1. EditorConfig の追加

EditorConfig は言語を問わず、テキストファイルのレウアウトルールを設定することができるツールです。

設定ファイルを作成します。

$ touch .editorconfig

つぎに、作成した設定ファイルに以下を記述します。

.editorconfig
# editorconfig.org
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
trim_trailing_whitespace = false

10-2. Prettier の追加

Prettier は自動整形ツールです。

10-2-1. インストール

Prettier のインストールをします。

$ yarn add -D prettier

10-2-2. 設定ファイルの追加

Prettier の設定ファイルの作成をします。

$ touch .prettierrc.js

つぎに、作成した設定ファイルに以下を記述します。

.prettierrc.js
module.exports = {
  semi: false,
  arrowParens: 'always',
  singleQuote: true,
}

10-3. ESLint の追加

ESLint は JavaScript または TypeScript の静的コード分析ツールです。

10-3-1. インストール

ESLint の関連モジュールをインストールします。

$ yarn add -D eslint eslint-plugin-react \
              eslint-config-prettier eslint-plugin-prettier \
	      @typescript-eslint/parser @typescript-eslint/eslint-plugin

10-3-2. 設定ファイルの追加

ESLint の設定ファイルを作成します。

$ touch .eslintrc.js

つぎに、作成した設定ファイルに以下を記述します。

.eslintrc.js
module.exports = {
  ignorePatterns: ['!.eslintrc.js', '!.prettierrc.js'],
  extends: [
    'eslint:recommended',
    'plugin:react/recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:@typescript-eslint/eslint-recommended',
    'plugin:prettier/recommended',
    'prettier/@typescript-eslint'
  ],
  plugins: ['@typescript-eslint', 'react'],
  parser: '@typescript-eslint/parser',
  env: {
    browser: true,
    node: true,
    es6: true,
    jest: true
  },
  parserOptions: {
    sourceType: 'module',
    ecmaFeatures: {
      jsx: true
    }
  },
  settings: {
    react: {
      version: 'detect'
    }
  },
  rules: {
    // 必要に応じてルールを追加
    'react/prop-types': 'off',
    'react/react-in-jsx-scope': 'off',
    '@typescript-eslint/no-explicit-any': 'off'
  }
}

10-3-3. Next.js の設定ファイルを修正

Next.js の設定ファイルの先頭に eslint-disable を設定する。

next.config.js
/* eslint-disable
   @typescript-eslint/no-var-requires
*/

ESLint の設定を TypeScript にする為、js ファイルに適応すると問題がおきるので設定しています。ESLint の適応外にすることはできるのですが、自動整形を優先してこの形にしました。

10-3-4. 無視ファイルを追加

gitignore にリポジトリで管理しないファイルを追記します。

.gitignore
# ESLint のキャッシュファイルを追加
.eslintcache

10-3-5. NPM スクリプトの追加

ESLint を実行する NPM スクリプトを追記します。

package.json
{
  "scripts": {
    "lint": "eslint --ext .js,.jsx,.ts,.tsx --ignore-path .gitignore ."
  }
}

問題なく Lint が通るか確認します。

$ yarn lint

もし通らない場合は fix オプションをつけて yarn lint --fix と実行することで、整形可能なものに関しては自動整形することができます。

10-4. VSCode の設定

VSCode の設定ファイルを追加します。

$ mkdir .vscode && \
  touch .vscode/settings.json

以下を設定することファイルの保存時に自動整形することができます。

.vscode/settings.json
{
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
  "eslint.validate": [
    "javascript",
    "javascriptreact",
    "typescript",
    "typescriptreact"
  ]
}

VSCode の ESLint プラグインがインストールされていない場合は、そちらもインストールしてください。

VSCode の設定ファイル(.vscode ディレクトリ以下)を Git で管理しないのであれば gitignore に追加してリポジトリで管理しないようにもできます。

開発者の環境に依存するので管理しないほうが正当かなという気はするのですが、他のエディタをつかっている人に影響があるわけではないので、個人的には邪道かなとおもいつつ管理することにしています。

11. テストの追加

テストと言っても色々とありますが、今回は以下のものを追加します。

テストの種類について

テストの種類は以前の記事の一部で簡単にまとめていますので、確認してみてください。

Static テストについては既に追加済みですので、全ての層におけるテストはひととおり準備できていることになります。

それぞれのテストに何を使用するかの選択肢は他にもあります。
ここでは紹介はしませんが、特徴を調べてプロジェクトに合わせて選ぶと良いかと思います。

11-1. Jest を追加

11-1-1. インストール

# Jest 関連モジュールをインストール
$ yarn add -D jest identity-obj-proxy

# Jest の TypeScript に関するモジュールをインストール
$ yarn add -D ts-jest @types/jest

11-1-2. 設定ファイルの追加

Jest の設定ファイルを作成します。

$ touch jest.config.js

作成した設定ファイルに以下を記述します。

jest.config.js
module.exports = {
  preset: 'ts-jest',
  roots: ['<rootDir>/src'],
  moduleNameMapper: {
    // CSS モックをモックする設定
    '\\.(css|scss)$': 'identity-obj-proxy',
    // pages と components ディレクトリのエイリアスを設定(必要であれば他のディレクトリも追加)
    '^(pages|components)/(.+)': '<rootDir>/src/$1/$2',
  },
  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
  globals: {
    'ts-jest': {
      tsconfig: {
        jsx: 'react',
      },
    },
  },
}

moduleNameMapper の設定を以前の記事で間違えていたようで、こちらの記事にて指摘が入っていましたので修正しました。

11-1-3. NPM スクリプトの追加

テストを実行する為の NPM スクリプトを追記します。

package.json
{
  "scripts": {
    "test": "jest src/__tests__/.*/*.test.tsx?$",
  }
}

11-1-4. テストの追加

必要であればテストを追加します。
現時点でテストを書く必要がなくとも、テストが問題なく動作している状態にしておくのは良いことだと思います。

まずはテストファイルを置くフォルダと、サンプルのテストファイルを作成します。

$ mkdir src/__tests__ && \
  touch src/__tests__/Sample.test.tsx

今回はとりあえず現状で用意できる適当なテストを書いてみます。

src/__tests__/Smaple.test.tsx
/// <reference types="jest" />

import React from 'react'
import Home from 'pages/index'

it('Home ページコンポーネントが存在している', () => {
  expect(Home).toBeTruthy()
})

問題なくテストが動作しているか確認します。

$ yarn test

エラーにならずに書いたテストをパスしていれば問題ないです。

11-2. React Testing Library を追加

React Testing Library をインストールします。

$ yarn add -D @testing-library/react

あとは、Jest で使うテストファイル内で import して使用できます。
適当なテストを追記してみます。

src/__tests__/Smaple.test.tsx
// React と React Testing Library を読み込みます
import React from 'react'
import { cleanup, render, screen } from '@testing-library/react'

// 各テスト実行後にレンダーしたコンポーネントをアンマウントする
afterEach(cleanup)

it('「Next.js!」のリンクが Next.js の公式サイトのトップページである', () => {
  render(<Home />)
  expect(screen.getByText('Next.js!').getAttribute('href')).toBe(
    'https://nextjs.org'
  )
})

基本の使い方はサンプルをみるとよいかと思います。

ただ、Jest や DOM Testing Library というベースがあるので、あまり詳しくは記載されていません。分かりやすい記事を探してみるのも良いかもしれません。

11-3. Cypress を追加

11-3-1. インストール

$ yarn add -D cypress

11-3-2. TypeScript に対応

$ find cypress -name "*.js" | sed 'p;s/.js$/.ts/' | xargs -n2 mv

プラグインファイルでワーニングがでるので修正します。

cypress/plugins/index.ts
// 使っていない引数を削除して ES Modules の記述に変更
export default (): void => {
  // `on` is used to hook into various events Cypress emits
  // `config` is the resolved Cypress config
}

コマンドファイルでエラーがでるので修正します。

cypress/support/commands.ts
// `isolatedModules: true` なので解決する為に追加
export {}

11-3-3. NPM スクリプトの追加

Cypress を起動する為の NPM スクリプトを追記します。

package.json
{
  "scripts": {
    "cypress:open": "cypress open",
    "cypress:run": "cypress run"
  }
}

11-3-4. 初期ファイルを追加

Cypress を起動して初期ファイルをプロジェクト内に展開します。

$ yarn cypress:open

以下のようにウィンドウが開かれます。

このウィンドウは、そのまま閉じてよいです。

あと、サンプルファイルは不要であれば削除します。

$ rm -rf cypress/integration/examples

11-3-5. 無視ファイルを追加

gitignore にリポジトリで管理しないファイルを追記します。

.gitignore
# cypress
cypress/videos
cypress/screenshots

11-3-6. 設定ファイルを追加

Cypress の設定ファイルを作成します。

$ touch cypress.json

作成した設定ファイルに以下を記述します。

cypress.json
{
  "baseUrl": "http://localhost:3000"
}

11-3-6. テストファイルの追加

必要であればテストファイルを作成します。

$ touch cypress/integration/sample_spec.ts

適当なテストを記述してみます。

cypress/integration/sample_spec.ts
/// <reference types="cypress" />

describe('タイトルのテスト', () => {
  it('タイトルが「Create Next App」である', () => {
    cy.visit('/')
    cy.title().should('include', 'Create Next App')
  })
})

// TypeScript でエラーがでるので記述
export {}

Next.js は isolatedModules が自動的に true になるので、export されていないとエラーになります。今回は export {} を使いすることで無理やり回避しています。

正しい方法がある気がしますが、いくつか試してうまくいかなかったですので、正しい方法があれば教えてもらえると嬉しいです。

まず、開発サーバを起動します。

$ yarn dev

次にテストを実行して、問題なくテストが動作しているか確認します。

$ yarn cypress:run

この場合は別のターミナルを使ってテストを実行します。

12. コンポーネントカタログの追加

コンポーネントを元にそれを組みわせてアプリケーションを作り上げていく形では、コンポーネントの一覧や状態を確認できるコンポーネントカタログはとても便利です。

コンポーネントカタログといえば、Storybook ですかね。
他の選択もあるかとは思いますが、今のところはこれ以外のまともな選択肢がわからないですので、今回は Storybook を追加していきます。

前回の記事では Storybook のアドオンを追加しましたが、今回はアドオンの追加をしません。
理由としては以前と比べてデフォルトの機能が拡充されている為です。個人的に Storybook を使うなら必須と考える機能は揃っているかと思います。

一度、アドオンのページを確認してみて、気になる機能があれば追加してもよいかと思います。ただし、同等の機能が既にデフォルトに含まれているものがありますので気をつけてください。

12-1. Storybook のインストール

Storybook をセットアップします。

$ npx sb init

セットアップが終わると以下が対応されています。

  • 関連ファイルのインスール
  • 設定ファイルの追加
  • サンプルファイルの追加
  • NPM スクリプトの追記

12-2. ESLint の設定を変更

ESLint の設定ファイルに Storybook の設定ファイルを無視しないように追記します。

.eslintrc.js
module.exports = {
  ignorePatterns: [
    // Storybook の設定フォルダを追加する
    '!.storybook'
  ],
}

一度、自動整形を実行します。

$ yarn lint --fix

いくつかのサンプルファイルでエラーがでているので修正します。

src/stories/Header.tsx
export interface HeaderProps {
  // `{}` の型を変更する
  user?: Record<string, unknown>
}
src/stories/Page.tsx
export interface PageProps {
  // `{}` の型を変更する
  user?: Record<string, unknown>
}

export const Page: React.FC<PageProps> = () => (
  <li>
    Use a higher-level connected component. Storybook helps you compose
    {/* `"` を実体参照に変更する */}
    such data from the &quot;args&quot; of child component stories
  </li>
)

サンプルファイルですので削除しても良いかと思います。
ただ、一旦エラーを解消するほうが早かったのでここでは修正する形にしました。

12-3. sass-loader をインストール

SASS を使っていれば sass-loader をインスールします。

$ yarn add -D sass-loader

sass-loader は Next.js で使用しているかと思うのですが、読み込めないようなのでインスールしています。もしかしたら不要かもしれないです。

12-4. Storybook の設定を変更

設定ファイルにエイリアスと SASS の設定を追記します。

.storybook/main.js
// ESLint のエラーを回避する
/* eslint-disable
    @typescript-eslint/no-var-requires
*/

const { resolve } = require('path')

module.exports = {
  webpackFinal: async (config) => {
    // SASS ファイルを読み込みるように設定する
    config.module.rules.push({
      test: /\.scss$/,
      use: ['style-loader', 'css-loader', 'sass-loader'],
      include: resolve(__dirname, '../'),
    })
    // エイリアスを設定する
    config.resolve.alias = {
      ...config.resolve.alias,
      components: resolve(__dirname, '../src/components'),
      styles: resolve(__dirname, '../src/styles'),
    }
    return config
  },
}

SASS を追加していなければ適宜不要な記述を省いてください。

12-5. 無視ファイルを追加

gitignore にリポジトリで管理しないファイルを追記します。

.gitignore
# Storybook のビルドディレクトリを追加
storybook-static

12-6. デフォルト CSS を追加

Storybook で表示されるコンポーネント自体に、デフォルト CSS を効かせる為に以下を追記します。

storybook/preview.js
// デフォルト CSS を読み込む
import 'sanitize.css'

13. コンポーネントカタログのスナップショットの追加

13-1. StoryShots の追加

スナップショットのテストをすることで、機能追加などの際に意図しない変更が起きていないかを確認することができます。
共通コンポーネントの変更は影響範囲が大きいですので、明確に差分を確認させることでリクスを減らすことができるかと思います。

13-1-1. インストール

$ yarn add -D @storybook/addon-storyshots

13-1-2. 設定の追加

Storybook の設定ファイルを作成します。

$ mkdir storyshots && \
  touch storyshots/storybook.test.ts

設定ファイルに以下を記述します。

src/storybook.test.ts
import initStoryshots from '@storybook/addon-storyshots'

initStoryshots()

13-1-3. TS ファイルに変換

JS ファイルのままだと tsconfig.json の設定を使えずに、ファイルの読み込みエラーなどがおきるので preview.js を TS ファイルに に変換します。

$ mv .storybook/preview.js .storybook/preview.ts

preview.js を preview.ts にして使う記載は見つけられませんでしたので、けっこう無理やりな対応ではあるかと思います。
正しい方法があれば教えてもらえると嬉しいです。

13-1-4. ファイルの読み込みに対応

サンプルのストーリーファイルで DMX と SVG のファイルを読み込んでいる為、スナップショット時にエラーがでてしまいます。

SVG の変換をするモジュールを追加します。

$ yarn add -D jest-svg-transformer

テストの設定ファイルに MDX の変換処理SVG の変換処理を追加します。

jest.config.js
module.exports = {
  // DMX と SVG の変換処理を追加
  transform: {
    '^.+\\.svg$': 'jest-svg-transformer',
    '^.+\\.jsx?$': 'ts-jest',
    '^.+\\.mdx$': '@storybook/addon-docs/jest-transform-mdx',
  },
}

MDX も SVG も使わなくて後で消すのであれば対応しないでもよいです。

13-1-5. NPM スクリプトの追加

スナップショットテストを実行する NPM スクリプトを追記します。

package.json
{
  "scripts": {
    "storyshots": "jest src/storybook.test.ts",
  }
}

こちら解決しましたので 16-1-4 にて対応を記載しています。

MDX フォーマットのファイルがサンプルに含まれているのですが、そのファイルがをスナップショットテスト時にワーニングを起こしてしまいます。

色々と試したので解決できていないのですが、大きな問題ではないのでとりあえずそのままにしています。でもスナップショット毎に表示されてとてもうるさいです。よい解決方法が教えてもらえると嬉しいです。

13-2. Puppeteer storyshots の追加

Puppeteer を使いスクレイピングすることで、ストーリーごとの画像キャプチャを撮り見た目の差分を検知することができます。

13-2-1. インストール

$ yarn add -D @storybook/addon-storyshots-puppeteer puppeteer

13-2-2. 設定の追加

Storybook の設定ファイルを作成します。

# 基本(PC用)の設定ファイルを作成
$ touch storyshots/puppeteer-storyshots.test.ts

# タブレット用の設定ファイルを作成
$ touch storyshots/puppeteer-storyshots-ipad.test.ts

# スマホ用の設定ファイルを作成
$ touch storyshots/puppeteer-storyshots-iphone8.test.ts

基本(PC用)の設定ファイルに以下を記述します。

storyshots/puppeteer-storyshots.test.ts
import initStoryshots from '@storybook/addon-storyshots'
import { imageSnapshot } from '@storybook/addon-storyshots-puppeteer'

initStoryshots({
  test: imageSnapshot(),
})

タブレット用の設定ファイルに以下を記述します。

storyshots/puppeteer-storyshots-ipad.test.ts
import initStoryshots from '@storybook/addon-storyshots'
import { imageSnapshot } from '@storybook/addon-storyshots-puppeteer'
import { devices } from 'puppeteer'

const customizePage = (page) => page.emulate(devices['iPad'])

initStoryshots({
  suite: 'Image storyshots: iPad',
  test: imageSnapshot({ customizePage }),
})

スマホ用の設定ファイルに以下を記述します。

storyshots/puppeteer-storyshots-iphone8.test.ts
import initStoryshots from '@storybook/addon-storyshots'
import { imageSnapshot } from '@storybook/addon-storyshots-puppeteer'
import { devices } from 'puppeteer'

const customizePage = (page) => page.emulate(devices['iPhone 8'])

initStoryshots({
  suite: 'Image storyshots: iPhone 8',
  test: imageSnapshot({ customizePage }),
})

ひとつファイルで複数デバイスのスナップショットテストができるとよいのですが、initStoryshots を複数回よびだすことが出来ないため、それぞれのファイルに分けてあります。

多くなればなるほど実行時間もかかりますので、すごく数が増えるものではないと思います。ということで、これでよいかとは考えています。

13-2-3. 無視ファイルを追加

gitignore にリポジトリで管理しないファイルを追記します。

.gitignore
# Storyshots の差分ディレクトリを追加
src/storyshots/__snapshots__/__diff_output__

13-2-4. NPM スクリプトの追加

画像キャプチャのスナップショットテストを実行する NPM スクリプトを追記します。

package.json
{
  "scripts": {
    "puppeteer-storyshots": "jest storyshots/puppeteer-storyshots*.test.ts",
  }
}

それぞれのローカルで差分検出をする場合は、コードに差分がなくとも環境ごとの誤差が生まれてしまう場合があります。とはいえ、僅かな誤差であり視覚的に確認することはできます。

この誤差が大きな問題になる場合や、実行速度的な問題がある場合は、何かしらの対応や別の選択肢を考える必要があるかと思います。

Stroyshots 全体のフォルダ構成ですが、src 内にテストファイルを入れないと色々と問題があったのでそのようにしています。スナップショットの出力ファイルについては src 外に配置しようかと思ったのですが、一部うまくいかない箇所があったので初期設定のまま同じディレクトリ配下に出力しています。

14. フックスクリプトの追加

リポジトリへのコミットやプッシュの際に、事前に Lint やテストを自動実行できるようにします。
これによりプロジェクトを健全に保つことができます。

14-1. lint-staged の追加

lint-stagedGit のステージに上っているファイルだけを Lint の対象にすることができるツールです。

14-1-1. インストール

lint-staged をインストールします。

$ npx mrm lint-staged

14-1-2. NPM スクリプトに追加

lint-staged を実行する NPM スクリプトを追記します。

package.json
{
  "scripts": {
    "lint-staged": "lint-staged"
  }
}

14-2. husky の追加

husky はコミットやプッシュの前にテストなどを実行して、失敗したら止めることができる Git hooks を簡単に設定することがツールです。

14-2-1. インストール

husky のインストールをします。

$ yarn add -D  husky@next

14-2-2. Git hooks の有効化

以下のコマンドで Git hooks を有効化します。

$ yarn husky install

14-2-3. フックスクリプトを追加

Git コマンド実行時に以下の処理を実行するようにします。

  • コミット前にステージにあるファイルを対象に ESLint の実行
  • プッシュ前にすべてのテストの実行
$ yarn husky add pre-commit "yarn lint-staged" & \
  yarn husky add pre-push "yarn test && yarn storyshots && yarn puppeteer-storyshots"

プリプッシュで Puppeteer storyshots も実行するようにしていますが、これは画像キャプチャのスナップショットですので重い処理です。また、Storybook が起動していないとスクレイピングできないのでエラーになります。そのことを踏まえるとこのタイミングでテストする必要はないかもしれません。

CI での処理なども含めて、どの検証をどのタイミングで実行するかはプロジェクトごとに検討すると良いかと思います。

15. 環境変数の追加

開発環境や本番環境ごとなどに、違った変数を用意することができます。それを環境変数といいます。Next.js では環境変数をデフォルトで設定できるようになっています。

今回はとりあえず適当に環境変数を追加します。

記載しているように環境変数はデフォルトで設定できるようになっています。ですので特に用意する環境変数がなければ、このセクションは何もせずに終わってもよいです。

15-1. 環境変数ファイルを追加

開発環境と本番環境の環境変数ファイルを作成する。

$ touch .env.development & \
  touch .env.production

作成した開発環境の環境変数ファイルに、開発サーバの URL を環境変数として用意する。

.env.development
NEXT_PUBLIC_SITE_URL=http://localhost:3000

作成した本番環境の環境変数ファイルに、本番サーバの URL を環境変数として用意する。

.env.production
NEXT_PUBLIC_SITE_URL=https://example.com

15-2. 環境変数を使用する

Document コンポーネントのサイト URL に環境変数を設定します。

src/pages/_document.tsx
class MyDocument extends Document implements MyDocumentInterface {
  // 環境変数を追加
  url = process.env.NEXT_PUBLIC_SITE_URL
}

これで今回お伝えする環境構築は終わりになります。

長かったですね。とはいえ、まだ追加したほうがよいものもある気もします。が、あまり詰め込み過ぎてもというところはありますのでコレくらいにしておきます。

所感

どうでしょうか?前回の記事をみている方からすると前より簡単になった気がしませんか?単純に初期設定で対応してきたのもありますが、ここ最近の流行りですかね?設定をできるだけ書かなくてよいようにしようみたいな流れは感じますね。Parcel あたりからの流れでしょうか?

個人的には明示的であることが好きではあるのですが、Webpack あたりの設定の複雑さとか破壊的アップデートとかをみていると、その辛みも感じるところではあります。そういった複雑な設定による辛みやパフォーマンスの低下を回避する為に Rome などのモノリシックなフロントエンドが産まれるんだなとかも思ったりしたり。まあ、良くも悪くも時代は繰り返しているんだなと思いつつ、数年ほどの短いエンジニア人生を振り返っていました。

ちなみに今回の記事は前回よりも見やすいように、あまり複雑にならないように余計な設定は抑えようとか、公式のドキュメントに合わせて記述しようとか、セクションはちゃんと種類別に分けようとか、個々の説明を少しでも記載しようとか、そのあたりは意識して書いてみました。そのぶん長くなっちゃいましたが。。。

年内にこの記事を書き直すなんて思ってもいなかったですが、まあ、変わっている部分も多いのでしかたないですかね。とはいえ常に最新状態に合わせて更新するとか、また大幅に書き直すなんてことはしないかとは思います。本当にただただ記事の想定外の速さでの老朽化と、私の気が向いたタイミングがあっただけという感じですかね。どうせまた、半年もすればこの記事も古くなることでしょうし。

個人的にも新たな収穫もあったのでそれはそれで良かったです。明日以降はしばらく Next.js というかフロントエンド以外のことを試して過ごす日々になりそうなモチベーションです。飽き性なんでね。とはいえ仕事はあるけれども。

それではまたいつか、次の記事にて。

この記事に贈られたバッジ