LoginSignup
419
382

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-03-12

アップデート版の記事を以下に用意しています。
2020年師走における Next.js をベースとしたフロントエンドの環境構築

さて、今年に入って既に2ヶ月が経ちました。ということは3月に突入しているってことで、それは僕が東京で働き初めて2年が過ぎ去り、SPA なフロントエンドの環境をプロジェクトとして初めて構築して1年あまりということです。そして、冬も過ぎ去り春が来ようかというようなこの時期に、小さくはあるけれど新たな挑戦として Next.js を使うことになりました。

こんな記事を読んでいる方なら分かるとは思いますが、Next.js とは JavaScript のライブラリである React のフレームワーク です。Next.js といえば、同くフレームワークである Gatsby になんとなく押され気味なイメージを感じていましたが、v9 以降のアップデートがよい感じで、さらにごく最近の v9.3 のアップデートでは少しばかり興奮してしまいました。今回はアップデートの内容には触れませんが、とてもおもしろい機能が追加されているので確認してみることをおすすめします。

その Next.js で環境構築した際の内容と手順をご紹介していきます。

技術選定

好みがないとは言い切れないですが、基本的には 最新かつ人気のある構成 を目指して選定しました。一応は盛りすぎないようにも気をつけていますが、不要な場合はある程度は省きやすいようにしていますので、うまく読み替えてもらえればと思います。ただし、TypeScrpt を前提 で書いているので TypeScript を省くのは難しいかもです。

これらで構築されたプロジェクトのリポジトリは以下になります。

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

※上記以外のモジュールなども含まれており、以降で説明する構築手順とは違った内容の箇所もありますが、選定したものは全て含まれています。あとから別途リポジトリを用意するかも。

(追記:2020/03/13 22:35)
この記事の構築手順だけにしたリポジトリに差し替えました。
また、その際に通しで確認してうまくいかない箇所を見つけたので細々と記事を修正しています。

構築手順

少しばかり長くなっていますが、セクションごとにみると大したことはしていないので、少しずつ構築していくとよいかと思います。
(その過程で問題があるようでしたら、教えてもらえると大変うれしいです)

※Linux コマンドでファイルの作成などをしていますので、windows の方などは読み替えてもらえればと思います。ちなみにこの記事の大半は windows で書いています。

1. Next.js を追加

ベースとなるフレームワークの選定ですが、TypeScriptSSR がしたかった。くらいで、深い理由はなくて比較的簡単にできそうな Next.js をなんとなく選んだくらいの感覚です。ちなみに僕は React をまともに書いたことはないです。

# Next.js をセットアップ
# (現在のディレクトリで作成しますが必要に応じて変更)
$ npm init next-app .

# pages を src ディレクトリへ移動
$ mkdir src ; mv pages/ src/pages

# Next.js の設定ファイルを作成
$ touch next.config.js

Next.js で v9.1 から srcpages を入れること可能になったので、src に入れる形で進めます。

1-1. Next.js の設定

next.config.js
const { resolve } = require('path')

const nextConfig = {
  webpack: (config) => {
    // src ディレクトリをエイリアスのルートに設定
    config.resolve.alias['~'] = resolve(__dirname, 'src')
    return config
  }
}

module.exports = nextConfig

2. TypeScript に対応

TypeScript は簡単にいうと JavaScript に 静的型付け を加えたスーパーセットです。JavaScript は動的型付けである為、初心者にとってはわかりやすくはあるかもしれません。しかし、プロジェクトの規模が大きくなるほど静的型付けの 安全性 というメリットが強みを増してきます。「プロジェクトよ健全であれ」と誰が言ったかは分かりませんが、少しでも大きなプロジェクトであれば導入しておくのが良いでしょう。

# TypeScript 関連のモジュールをインストール
$ npm i -D typescript @types/react @types/react-dom @types/node

# TypeScript の設定ファイルを作成
$ touch tsconfig.json

# Next.js を開発モードで起動することで必要なファイルを自動生成
$ npm run dev
# 生成されたら終了する

# src ディレクトリ配下の js と jsx のファイルを ts と tsx に変換
$ find src -name "*.js" | sed 'p;s/.js$/.tsx/' | xargs -n2 mv

2-1. TypeScript の設定に追加

tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "~/*": [
        "./src/*"
      ]
    }
  }
}

src ディレクトリをエイリアスのルートに設定しています。

2-2. ページを TypeScript に対応

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

// 型を追加
const Home: NextPage = () => (
  // JSX はそのまま
}

2-3. Next.js の設定に追加

next.config.js
// ファイルの先頭に eslint-disable を追加
/* eslint-disable
    @typescript-eslint/no-var-requires,
    @typescript-eslint/explicit-function-return-type
*/

これはあとから追加する ESLint の話なのですが、eslint-disable は ESLint の設定を TypeScript にする為、js ファイルに適応すると問題がおきるので設定しています。ESLint の適応外にすることはできるのですが、自動整形を優先してこの形にしました。

本来であれば以下のどちらかにできればよいのですが、方法がわからなかったです。

  • next.config.ts にする
  • js ファイルには TypeScript 関連の Lint を適応しない

わかる方いたら教えてくださいませ。

3. Next.js の App と Document のコンポーネントをオーバーライド

# _app.tsx と _document.tsx のファイルを作成
$ touch src/pages/_app.tsx && touch src/pages/_document.tsx

# sanitize.css をインストールする
$ npm i -D sanitize.css

APP コンポーネントは、全てのページでページを初期化しています。
Document コンポーネントは、<html><body> を補強しています。
_app.tsx_document.tsx はそれらをオーバーライドしてカスタムすることができます。

3-1. APP コンポーネントをオーバーライド

src/pages/_app.tsx
import * as React from 'react'
import App, { AppProps } from 'next/app'
// 全体に適応する外部 CSS を読み込む
import 'sanitize.css'

class MyApp extends App {
  render(): JSX.Element {
    const { Component, pageProps }: AppProps = this.props

    return (
      <React.Fragment>
        <Component {...pageProps} />
      </React.Fragment>
    )
  }
}

export default MyApp

3-2. Document コンポーネントをオーバーライド

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

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

class CustomDocument extends Document implements CustomDocumentInterface {
  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="format-detection" content="telephone=no" />
          <meta name="twitter:card" content="summary_large_image" />
          <meta name="twitter:title" content={this.title} />
          <meta name="twitter:description" content={this.description} />
          <meta name="twitter:image" content={`${this.url}/ogp.png`}></meta>
          <link rel="icon" href="/favicon.ico" />
          <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    )
  }
}

export default CustomDocument

4. PWA に対応

PWAProgressive Web Apps の略称で、クロスプラットフォームのウェブアプリケーション のことです。様々なメリットがあり、かつ現在では比較的簡単に導入できるようになってきている為、既にウェブアプリのデファクトスタンダードになっているのではないでしょうか。

Next.js の PWA については next-offline を使います。公式ページをみると、めっちゃ簡単っぽく書いてるけど、ウェブアプリマニフェストは自前で用意する必要があったりして、 NuxtJS で PWA でしたことある人からすると地味に面倒に感じるかもです。それでも Service Worker の実装などは考えなくてよいので楽になっているのでしょう。

# PWA のモジュールをインストール
$ npm i -D next-offline

# now v2 でデプロイするとき以外は実行
$ touch server.js

Next.js のサーバを動かすときに、一般的な環境や now v1 ではとある問題があるため設定が必要です。詳細は後述します。

※ now は Next.js を提供している ZEIT が提供する PaaS です。

4-1. Next.js の設定に追加

next.config.js
// withOffline を読み込む
const withOffline = require('next-offline')

// nextConfig を withOffline に渡す
module.exports = withOffline(nextConfig)

4-2. ウェブアプリマニフェストを作成

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

4-3. Document コンポーネントに追加

src/pages/_document.tsx
class CustomDocument extends Document implements CustomDocumentInterface {
  render(): JSX.Element {
    return (
      <Html lang="ja-JP">
        <Head>
          {/* manifest.json を読み込む */}
          <link rel="manifest" href="/manifest.json" />
        </Head>
      </Html>
    )
  }
}

4-4. カスタムサーバの設定

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

const { createServer } = require('http')
const { join } = require('path')
const { parse } = require('url')
const next = require('next')

const app = next({ dev: process.env.NODE_ENV !== 'production' })
const handle = app.getRequestHandler()

app.prepare().then(() => {
  createServer((req, res) => {
    const parsedUrl = parse(req.url, true)
    const { pathname } = parsedUrl

    // handle GET request to /service-worker.js
    if (pathname === '/service-worker.js') {
      const filePath = join(__dirname, '.next', pathname)

      app.serveStatic(req, res, filePath)
    } else {
      handle(req, res, parsedUrl)
    }
  }).listen(3000, () => {
    console.log(`> Ready on http://localhost:${3000}`)
  })
})

これは Next.js の Node によるサーバの設定です。通常は変更する必要はないのですが、ウェブアプリマニフェストが出力されるディレクトリの問題で読み込むできない為、設定を変更する必要があります。

ただ、カスタムサーバを使うとサーバレス機能や自動静的最適化が無効化されるとの記載があります。詳細を理解してはいないのですが、できるだけ使わない方がよいとのことなのでどうにかならないものかなとは思いますが、next-offline のドキュメントにあるので仕方ないかなという感じです。PWA にすることとどちらがメリットがあるのかは気になるところです。

この設定は now v2 では不要になります。

4-5. npm-scripts を変更

package.json
{
  "scripts": {
    "dev": "node server.js",
    "build": "next build",
    "start": "NODE_ENV=production node server.js"
  }
}

カスタムサーバを使用した場合は設定し直す必要があります。

5. styled components を追加

Next.js には初期設定で styled-jsx が含まれています。そちらもで良いかとは思うのですが、styled-components の方が人気があるように感じたので、今回は styled-components を選択しています。

# styled-components の関連モジュールをインストール
$ npm i -D styled-components styled-media-query

# .babelrc.js を作成(エラーを回避する為に追加。詳細は後述)
$ touch .babelrc.js

styled-media-query は styled-components で media queries を比較的簡単に記述できるようにするモジュールです。

5-1. APP コンポーネントのオーバーライドを変更

src/pages/_app.tsx
// ThemeProvider を読み込む
import { ThemeProvider } from 'styled-components'

// テーマを必要に応じて設定
const theme = {}

class MyApp extends App {
  render(): JSX.Element {
    return (
      // `<React.Fragment>` を `<ThemeProvider>` に変更してテーマを渡す
      <ThemeProvider theme={theme}>
        <Component {...pageProps} />
      </ThemeProvider>
    )
  }
}

テーマを使うと <ThemeProvider> 配下の全てのコンポーネントにスタイルを渡すことができます。深くネストされているコンポーネントでも問題ありません。

5-2. Document コンポーネントのオーバーライドに追加

src/pages/_document.tsx
// ServerStyleSheet を読み込む
import { ServerStyleSheet } from 'styled-components'

class CustomDocument extends Document implements CustomDocumentInterface {
  // getInitialProps メソッドの追加
  static async getInitialProps(ctx): Promise<any> {
    const sheet = new ServerStyleSheet()
    const originalRenderPage = ctx.renderPage

    try {
      ctx.renderPage = (): any =>
        originalRenderPage({
          enhanceApp: (App) => (props): void =>
            sheet.collectStyles(<App {...props} />)
        })

      const initialProps = await Document.getInitialProps(ctx)
      return {
        ...initialProps,
        styles: (
          <>
            {initialProps.styles}
            {sheet.getStyleElement()}
          </>
        )
      }
    } finally {
      sheet.seal()
    }
  }
}

SSR 時に styled-components で追加したスタイルが適応されない問題があるので、こちらの対応が必要になります。

5-3. Babel を設定

.babelrc.js
module.exports = {
  presets: ['next/babel'],
  plugins: [
    ['styled-components', { ssr: true, displayName: true, preprocess: false }]
  ]
}

Next.js は v9 のバージョンアップにより、内部的に Babel が動作しているものの、Babel に関連するモジュールのインストールや設定ファイルが不要になりました。なので、.babelrc.js も基本は不要ではあるのですが、問題があるために一部の設定をオーバーライドする必要があります。

問題というのは、ローカルの開発サーバー側の SSR 時クライアント側のCSR 時に styled-components が付与するクラス名に差が生まれエラーが出てしまうというものです。

6. Redux Toolkit

Redux Toolkit は React における 状態管理 のライブラリである Redux を、効率的に扱うことのできるようにするライブラリです。プロジェクト全体に関わる状態をストアで管理することで、より効率的な設計ができると思います。

# Redux Toolkit 関連のモジュールをインストール
$ npm i -D @reduxjs/toolkit react-redux redux-logger

# Redux Toolkit に関するファイルを作成
$ touch src/store.ts && mkdir src/modules ; touch src/modules/rootState.ts

# ストアのサンプルを作成するのであれば実行
$ touch src/modules/sampleModule.ts

6-1. ストアを設定

src/store.ts
import {
  configureStore,
  getDefaultMiddleware,
  EnhancedStore
} from '@reduxjs/toolkit'
import { rootReducer } from './modules/rootState'
import logger from 'redux-logger'

export const setupStore = (): EnhancedStore => {
  const middlewares = [...getDefaultMiddleware()]

  // only development
  if (process.env.NODE_ENV === 'development') {
    middlewares.push(logger)
  }

  return configureStore({
    reducer: rootReducer,
    middleware: middlewares,
    devTools: true
  })
}

configureStoredevToolstrue にすることで Redux DevTools Extension を起動することができます。デバッグの役に立ちます。

6-2. APP コンポーネントに設定を追加

src/pages/_app.tsx
// Provider を読み込む
import { Provider } from 'react-redux'
// setupStore を読み込む
import { setupStore } from '~/store'

// ストアを作成
const store = setupStore()

class MyApp extends App {
  render(): JSX.Element {
    return (
      // Provider で囲んで store を渡す
      <Provider store={store}>
        <ThemeProvider theme={theme}>
          <Component {...pageProps} />
        </ThemeProvider>
      </Provider>
    )
  }
} 

6-3. ストアのルートを追加

src/modules/rootState.ts
import { combineReducers } from '@reduxjs/toolkit'
// サンプルを追加するのであれば読み込む
import sampleModule, { SampleState } from './sampleModule'

export interface RootState {
  // サンプルを追加するのであれば記述
  sample: SampleState
}

export const rootReducer = combineReducers({
  // サンプルを追加するのであれば記述
  sample: sampleModule.reducer
})

6-4. ストアのモジュールを設定

サンプルのモジュールファイルを追加したのであれば対応してください。

src/modules/sampleModule.ts
import { createSlice } from '@reduxjs/toolkit'

export type SampleState = {
  value: string
}

export const sampleInitialState: SampleState = {
  value: ''
}

const sampleModule = createSlice({
  name: 'sample',
  initialState: sampleInitialState,
  reducers: {
    update: (state): SampleState => {
      return { value: state.value }
    }
  }
})

export default sampleModule

7. EditorConfig と ESLint と Prettier を追加

これらは コードの記述ルールや整形 などをするものです。それぞれの担っている内容が重複しているところもあるのですが、ルールのコンフリクトを避けて全て導入しておくとよいです。もたらされたのは人類におけるタイプ数や無駄なコンフリクト、それに宗教戦争の大幅な削減です。これは、本当に素晴らしいことだと感じています。

# ESLint と prettier 関連モジュールをインストール
$ npm i -D eslint eslint-plugin-react prettier eslint-config-prettier eslint-plugin-prettier

# ESLint の TypeScript に関するモジュールをインストール
$ npm i -D @typescript-eslint/parser @typescript-eslint/eslint-plugin

# ESLint と prettier の設定ファイルを作成
$ touch .editorconfig && touch .eslintrc.js && touch .prettierrc.js

# VSCode を使うのであれば設定ファイルを作成
$ mkdir .vscode ; touch .vscode/settings.json

TypeScript の Linter として TSLint というものがあるのですが、現在では非推奨になっており ESLintへの移行 を促しています。これは両者が同じ目的の為に存在しており、ESLint が TSLint の機能を統合する方向で動いているからです。

7-1. EditorConfig の設定

.editorconfig
# 必要に応じて Prettier と競合が起きないように設定

# 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

7-2. ESLint の設定

.eslintrc.js
module.exports = {
  // 整形を効かせたいファイルなので除外を解除
  ignorePatterns: ['!.eslintrc.js', '!.babelrc.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'
  }
}

7-3. Prettier を設定

.prettierrc.js
module.exports = {
  // 必要に応じて EditorConfig や ESLint と競合が起きないように設定
  semi: false,
  arrowParens: 'always',
  singleQuote: true
}

7-4. npm-scripts を追加

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

eslint コマンドは --fix オプションによる 自動整形 もできます。npm-scripts を通してオプションを渡すため npm run lint -- --fix とコマンド打つことで実行できます。

7-5. VSCode の設定

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

VSCode は使っていない人はさようなら。使っている人はこちらの設定で ファイルの保存時に自動整形 することができます。見た目なんてなにも考えずにひたすら書いて、そして保存。もはやなくてはならない空気のような存在です。

ちなみに、VSCode の設定ファイルを Git で管理するかどうかという話があるかと思います。個人的には VSCode の普及率と、あって困るものではないという点で含めちゃってよいと思いますが、含めたくないのであれば .gitignore に追加してくださいませ。

8. Jest と React Testing Library を追加

Jest は JavaScript のテストフレームワークです。また、React Testing Library はテストを ユーザが操作する手順と同じように書く ことができます。このライブラリは React でも推奨されています。テストを書くことにより得られる恩恵は様々ありますが、僕自身がほとんどテストを書いたことが無いので、あまり話せることはないです。すいません。今後は書いていきます。

# Jest 関連モジュールをインストール
$ npm i -D jest jest-dom jest-css-modules

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

# React Testing Library をインストール
$ npm i -D @testing-library/react

# Jest の設定ファイルとテストファイルのディレクトリを作成
$ touch jest.config.js && mkdir src/__tests__

# もしサンプルのテストファイルを追加するのであれば実行
$ touch src/__tests__/Sample.test.tsx

8-1. Jest を設定

jest.config.js
module.exports = {
  preset: 'ts-jest',
  roots: ['<rootDir>/src'],
  moduleNameMapper: {
    // src ディレクトリをエイリアスのルートに設定
    '^~/(.+)': '<rootDir>/src/$1',
    // test 時に CSS ファイルを読み込まないようにする設定
    '\\.css$': '<rootDir>/node_modules/jest-css-modules'
  },
  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
  globals: {
    // test 時に TypeScript の設定を一部変更して実行する設定
    'ts-jest': {
      tsConfig: {
        jsx: 'react'
      }
    }
  }
}

jest-css-modules はテスト時に import している CSS ファイルまで読み込んでしまいエラーになるのを防ぐために設定してます。

この時点ではまだ問題はないのですが、このあとに設定する StoryBook の SnapShots をテスト時に実行していて、その設定ファイル内で CSS ファイルを読み込んでいるのでエラーになります。

ts-jesttsConfig.jsx については reactpreserve があり、TypeScript だけでコンパイルするときは react を使い、Babel などの変換を通す場合は preserve を使います。

さきほど軽く説明したとおり、Next.js は Babel を内包しています。しかし、ts-jest によるテストは TypeScript のみを使います。つまり、ビルド時とテスト時では tsConfig.jsx の設定を変更する必要があるので、テスト時に設定を変更しているのです。

8-2. テストの設定

サンプルのテストファイルを追加したのであれば対応してください。

src/__tests__/Sample.tset.tsx
import * as React from 'react'
import { render, cleanup } from '@testing-library/react'
import Index from '~/pages/index'

afterEach(cleanup)

describe('Index page', (): void => {
  // "Next.js!" というテキストのリンク先が Next.js のサイトであることをテスト
  it('link to Next.js site', (): void => {
    const { getByText } = render(<Index />)
    expect(getByText('Next.js!').getAttribute('href')).toBe('https://nextjs.org')
  })
  // TODO を書ける
  it.todo('Index TODO')
})

とりあえずページコンポーネントに対してテストを書いています。テスト内容も初期状態でできることがないので適当なものになっていますが、実際にはユーザの操作などを考えるとよいので、何かしらのイベントが入ることが多いかと思います。

また、TODO も書けるので TDD(テスト駆動開発)をするのに便利ですね。

8-3. npm-scripts を追加

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

test コマンドは --watch オプションによる 監視 もできます。npm-scripts を通してオプションを渡すため npm test -- --watch とコマンドを打つことで実行できます。

あと、styled-jsx が記述されている為、Warning がでるのですが、テスト自体は通るのと、今回は styled-components を使う方向で進めているのでスルーしています。

9. Storybook を追加

# 1. Storybook の基本機能を追加

# storybook をインストール
$ npx -p @storybook/cli sb init --type react

# stories のファルダを src 配下に移動
$ mv stories/ src/stories

# Stroybook で使う webpack に関するモジュールをインストール
$ npm i -D babel-preset-react-app

# Storybook で使う webpack の設定ファイルを作成
$ touch .storybook/webpack.config.js

# src/stories ディレクトリ配下の js と jsx のファイルを ts と tsx に変換
find src/stories -name "*.js" | sed 'p;s/.js$/.tsx/' | $ xargs -n2 mv


# 2. Addon を追加

# メジャーな Addon をインストール(必要に応じて選択)
$ npm i -D @storybook/addon-knobs
$ npm i -D @storybook/addon-storysource
$ npm i -D @storybook/addon-viewport
$ npm i -D @storybook/addon-backgrounds
$ npm i -D @storybook/addon-console

# Storybook のレンダリングに関する設定ファイルを作成
$ touch .storybook/preview.ts


# 3. StoryShots に関する Addon を追加

# StoryShots と Puppeteer storyshots をインストール
$ npm i -D @storybook/addon-storyshots @storybook/addon-storyshots-puppeteer puppeteer react-test-renderer

# StoryShots の設定ファイルを作成
$ touch src/__tests__/storyshots.test.ts

# Puppeteer storyshots の設定ファイルを作成
# PC、タブレット、スマホの3サイズを用意(必要に応じて作成)
$ touch src/__tests__/puppeteer-storyshots.runner.ts && touch src/__tests__/puppeteer-storyshots-ipad.runner.ts && touch src/__tests__/puppeteer-storyshots-iphone8.runner.ts

9-1. Storybook の基本機能を追加

9-1-1. Storybook の設定を変更

.storybook/main.js
module.exports = {
  // ディレクトリと拡張子の変更
  stories: ['../src/stories/**/*.stories.tsx']
}

9-1-2. ESLint の設定に追加

.eslintrc.js
module.exports = {
  ignorePatterns: [
    // Storybook の設定に関するファイルを ESLint の対象に含める
    '!.storybook/**/*.(js|ts)'
  ]
}

9-1-3. Stroybook で使う webpack を設定

.storybook/webpack.config.js
/* eslint-disable
    @typescript-eslint/no-var-requires,
    @typescript-eslint/explicit-function-return-type
*/

const { resolve } = require('path')

module.exports = ({ config }) => {
  config.module.rules.push({
    test: /\.(ts|tsx)$/,
    loader: require.resolve('babel-loader'),
    options: {
      presets: [require.resolve('babel-preset-react-app')]
    }
  })

  config.resolve.extensions.push('.ts', '.tsx')

  config.resolve.alias['~'] = resolve(__dirname, '../src')

  return config
}

Storybook のビルドやサーバは Next.js とは違う形で動かします。そのため、別途 Webpack の設定が必要になります。

9-1-4. gitignore に Storybook のビルドディレクトリを追加

.gitignore
# stroybook
storybook-static

9-1-5. ストーリーファイルを TypeScript に対応

src/stories/0-Welcome.stories.tsx
// React の読み込みを import as に変更
import * as React from 'react'

// 返り値に型を追加
export const ToStorybook = (): JSX.Element => <Welcome showApp={linkTo('Button')} />
src/stories/1-Button.stories.tsx
// React の読み込みを import as に変更
import * as React from 'react'

// 返り値に型を追加
export const Text = (): JSX.Element => (

// 返り値に型を追加
export const Emoji = (): JSX.Element => (

Storybook の型がよくわからないのですが、Warning がでてたので雰囲気で一応追加しました。
あと保存時に自動整形されますが、VSCode でない場合はコマンドで自動整形すると Lint 時の Error や Warning は消えます。

9-2. Addon の追加

9-2-1. Addon を設定に追加

.storybook/main.js
module.exports = {
  addons: [
    // 必要に応じて追加
    '@storybook/addon-knobs/register',
    '@storybook/addon-storysource',
    '@storybook/addon-viewport/register',
    '@storybook/addon-backgrounds/register',
  ]
}

9-2-2. レンダリングに関する設定

.storybook/preview.js
import { addParameters } from '@storybook/react'
// addon-console を追加した場合は読み込む
import '@storybook/addon-console'
// addon-viewport を追加した場合は読み込む
import { INITIAL_VIEWPORTS } from '@storybook/addon-viewport'
import 'sanitize.css'

addParameters({
  // addon-viewport を追加した場合は設定
  viewport: {
    viewports: INITIAL_VIEWPORTS
  }
})

sanitize.css を読み込んでいますが、Next.js の APP コンポーネントで読み込んだ sanitize.css は Stroybook では反映されない為、こちらでも設定する必要があります。

9-3. StroyShots の追加

StroyShots とは Storybook のコードを流用して Jest の Snapshot Testing を実行できるものです。予定外の変更を検知 することができるので、安全に開発を進めやすくなります。

また、Puppeteer を使った StroyShotsもあり、Storybook をスクレイピングしてイメージショット をとることで見た目の差分を検知することができます。これは実行速度や正確性などの問題もあるので、どのようにテストしていくかは難しいところではありますが、コードでは追うことのできない 想定外の影響が及ぼすスタイルの変化 を検知することができます。

どちらも、うまく使うことで安全性の高い運用が期待できるかと思います。

9-3-1. StroyShots を設定

StroyShots は test コマンド時に合わせて実行されます。-u オプションによる スナップショットのアップデート や、-t オプションによる ターゲットの指定 ができます。npm-scripts を通してオプションを渡すため npm test -- -u -t="TargetName" のようにコマンドを打つことで実行できます。

src/__tests__/storyshots.test.ts
import initStoryshots, {
  Stories2SnapsConverter,
  multiSnapshotWithOptions
} from '@storybook/addon-storyshots'

initStoryshots({
  test: multiSnapshotWithOptions(),
  stories2snapsConverter: new Stories2SnapsConverter({
    snapshotsDirName: '../__tests__/__snapshots__/'
  })
})

9-3-2. Puppeteer storyshots を設定

PC、タブレット、スマホの3サイズを用意しているので必要に応じて設定してください。

PC 用サイズを設定
src/__tests__/puppeteer-storyshots.runner.tsx
import initStoryshots from '@storybook/addon-storyshots'
import { imageSnapshot } from '@storybook/addon-storyshots-puppeteer'

initStoryshots({
  suite: 'Image storyshots: PC',
  test: imageSnapshot()
})
タブレット用サイズを設定
src/__tests__/puppeteer-storyshots-ipad.runner.tsx
import initStoryshots from '@storybook/addon-storyshots'
import { imageSnapshot } from '@storybook/addon-storyshots-puppeteer'
import devices from 'puppeteer/DeviceDescriptors'

const customizePage: any = (page) => {
  return page.emulate(devices['iPad'])
}

initStoryshots({
  suite: 'Image storyshots: iPad',
  test: imageSnapshot({ customizePage })
})
スマホ用サイズを設定
src/__tests__/puppeteer-storyshots-iphone8.runner.tsx
import initStoryshots from '@storybook/addon-storyshots'
import { imageSnapshot } from '@storybook/addon-storyshots-puppeteer'
import devices from 'puppeteer/DeviceDescriptors'

const customizePage: any = (page) => {
  return page.emulate(devices['iPhone 8'])
}

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

9-3-3. Jest の設定に追加

jest.config.js
module.exports = {
  // ストーリーファイルを読み込めるようにする
  transform: {
    '^.+\\.stories\\.tsx$': '@storybook/addon-storyshots/injectFileName'
  }
}

ストーリーファイルごとにスナップショットを取得 できる multiSnapshotWithOptions オプションを設定して、かつ、Component Story Format という新しい形式を使っている場合は、ストーリーファイルを読み込めるようにする為にこの設定が必要です。

9-3-4. npm-scripts を追加

package.json
{
  "scripts": {
    "puppeteer-storyshots": "jest src/__tests__/puppeteer-storyshots*.runner.ts"
  }
}

puppeteer-storyshots コマンドは -u オプションによる イメージショットのアップデート や、-t オプションによる ターゲットの指定 ができます。npm-scripts を通してオプションを渡すため npm run puppeteer-storyshots -- -u -t="TargetName" のようにコマンドを打つことで実行できます。こちらも同じく Jest を実行していますので、オプションは他のテストと同様のものになっています。

10. Husky を追加

Husky は Git フックを簡単に設定することのできるモジュールです。うまく Linter や test を走らせることで無駄なコミットなどを防ぐことができ、見通しのよいコミット履歴を保つことや不要な CI の実行などを減らすことができます。

$ npm i -D husky

10-1. Pre commit と Pre push を設定

package.json
{
  "husky": {
    "hooks": {
      "pre-commit": "npm run lint",
      "pre-push": "npm test && npm run puppeteer-storyshots"
    }
  }
}

git commit 時にLint を、git push 時に test を実行するようにしています。

テストに関しては以下の3種類があります。

  • Jest + React Testing Library(ユニットテスト)
  • StoryShots(スナップショットテスト)
  • Puppeteer storyshots(イメージスナップショット)

Puppeteer storyshots は storybook をスクレイピングして画面をキャプチャします。なので storybook を起動しないといけない のと、コンポーネントが増えると処理がより重くなっていきそうな気もするので、git push 時に入れるのはどうなんだろうとは思っています。まあ、とりあえず入れておこうという感じです。

まとめ

実際のプロジェクトとして運用していく上では、他にも追加する設定やライブラリなどもあると思います。環境変数や Fetch にエラーページなどなど。

今回追加したものは、はじめの方で説明したとおり 最新かつ人気のある構成 を重視して、それでいて 実装のブレがあまり大きくならないもの にしていているつもりです。ですので、サンプルファイルを含めるかどうか悩んだのですが、無いと結局そこで不明点がでて手が止まってしまう可能性があるかと思い、 テストなどはせめてひとつのファイルくらいは動作確認ができるようにサンプルを追加しました。

最小の構成ではないが、モダンで必要なものは大体揃っていて、記述もドキュメントに可能な限り寄せて調べやすくし、そこから好きなようにライブラリなどを追加して、様々なプロジェクトに分岐していけるギリギリのラインを超えるか超えないか 、くらいの形にはなったかと思います。

まあ、つまりはそこそこ満足しています。

プロジェクトが動き出して問題点や直したいところなどもでてくるでしょうし、なによりもスピードの早いこの世界においては、数ヶ月もあれば既に古い構成になっている可能性 すらあります。辛くもあり、楽しくもある素敵な世界ですね。

それでは、この環境が廃れて、また新たな環境を構築するその時まで。

419
382
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
419
382