Next.jsの自分なりのベストプラクティスを6点紹介する

この記事は[Next.js Advent Calendar 2020](https://qiita.com/advent-calendar/2020/nextjs)の18日目の記事です。

# はじめに

タイトル通りです。
業務でNext.jsを約1年ぐらい触ってきて、毎回設定する項目についてまとめました。

いわゆるNext.js文脈ではなくReact, Web技術界隈のプラクティスも一部含まれますが優しい気持ちで御覧ください。


## 0. TypeScriptを使う

使いましょう。
TypeScriptを使う際の公式ドキュメントも充実しています。

https://nextjs.org/learn/excel/typescript

公式ドキュメント通りにやると`tsconfig.json`が`strict: false`になっているので、`strict: true`にするのをお忘れなく。

## 1. パスをまとめる: パスの中身を楽に書き換えられるように

※Next 10系からは`as`が自動的に解釈されるようになったので、よしなに読み飛ばしてください。
https://nextjs.org/blog/next-10#automatic-resolving-of-href

よくNextのドキュメントで以下のような記述をみますが、一般的にパスを直書きするとのちのち辛い思いをすることは明らかです。

```tsx
<Link href="/categories/[slug]" as="/categories/books">
```

雑なサンプルですが以下のよう`href`, `as`を返す関数を実装します。

```ts
export type LinkPropsLike = {
  href: string;
  as: string;
};

export const pathBuilder = {
  posts: () => ({
    index: (): LinkPropsLike  => ({
      href: '/posts',
      as: `/posts`,
    }),
    detail: (postId: number): LinkPropsLike => ({
      href: '/posts/[postId]',
      as: `/posts/${postId}`,
    }),
  }),
};
```

使用例としては以下のようになります。

```tsx
<Link {...pathBuilder.posts().detail(1)}>最初の記事</Link>
```

ちなみにそれをよしなにやってくれるライブラリもあります。下手に作るよりライブラリ等に則った方が秩序が保たれるので良いかもしれません。
https://github.com/yarnaimo/next-typed-path

GitHubのサンプルコードママ

```ts
import { $dynamic, $route, createRoutes } from 'next-typed-path'

const routes = createRoutes({
  about: $route,
  users: {
    index: $route,
    [$dynamic]: {
      index: $route,
      posts: {
        [$dynamic]: {
          index: $route,
        },
      },
      settings: {
        index: $route,
        lang: $route,
      },
    },
  },
})

routes.about // => '/about'
routes.users.index // => '/users'
routes.users('123').index // => '/users/123'
routes.users('123').posts('456').index // => '/users/123/posts/456'
routes.users('123').settings.index // => '/users/123/settings'
routes.users('123').settings.lang // => '/users/123/settings/lang'
```

クールですね。

TypeScript 4.1のTemplate Literal Typesがアツいので、そのうちTSだけでよしなにできるようになるかもですね。
Next.jsでそのうちサポートしてくれると非常嬉しい。

## 2. Vercelを使う: ホスティングやCDの設定, PRでの確認を楽に

**複雑なことをやらない**のであればたいていのパターンで脳死でデプロイできます。
ポチポチでSSL暗号化からCDのインテグレーションの設定までできるので重宝しています。

Vercelの紹介は別の記事や公式に任せるとして、個人的に特にアツいのは`Preview deployments`です。
Vercelは以下のような開発フローを推奨しています。

1. Pull Requestでいわゆるステージング環境を確認する
2. マージするとProductionに反映される

https://vercel.com/docs/platform/deployments

後述しますが、NetlifyでPull RequestのブランチでStorybookデプロイするようにしておくとレビュー作業がはかどります。それに加えてVercelでデプロイプレビュー確認ができるとリモートにもってきて確認作業をする必要がかなり減ります。

以下のようにGitHub等のPull Request上で確認することができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/91558/4e53a496-798b-764f-b2d9-4d0197baf909.png)

画像は https://armno.in.th/2020/05/07/vercel/ から


※Previewで確認できるサイトのドメインはVercelがよしなに設定するため、動作にドメインの制約があるようなサービスは以上のワークフローの設定は行えません。


## 3. Storybookでコンポーネントを管理する: コンポーネントの確認を楽に

実装時にコンポーネントを確認しながら実装したいですよね。Storybookを入れましょう。

Storybookは以下のスクショのようにコンポーネントを確認できるツールです。
アドオンをいれることによってイベントを確認できたり、`props`の中身を書き換えて確認ができたり強力なことができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/91558/18d89474-554c-1458-405b-b828f086cdcf.png)

Storybookの導入を推奨する文脈は以下のとおりです。

- コンポーネント単位での開発時に用いる
- コードレビュー時に後述のNetlifyにデプロイされたStorybookを確認する
- 共同開発者がコンポーネントを流用できるように(コミュニケーションコストを減らせる)
- 挙動の確認として

設定はわりと簡単に済みます。

Next.js ▲ + Typescript + Storybook The Really Simple Guide 2019 - DEV
https://dev.to/aprietof/next-js-typescript-storybook-the-really-simple-guide-2019-fei?signin=true


Netlifyを用いてPull RequestのブランチでStorybookをデプロイするように設定しておくとよいです。

以下のようにPull Requestで作成されたコンポーネント等をクリックするだけで確認することができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/91558/b268dde9-ebf3-510a-2e27-d03a50920b94.png)

画像は https://www.netlify.com/blog/2016/07/20/introducing-deploy-previews-in-netlify/ から



## 4. APIクライアントによしなに型をつける: OASでAPIの型定義を書いて楽する

Next.jsをプロダクトで使う多くの場合、APIを参照するはずです。

APIの定義がSwaggerなどのOASを使っているプロジェクトなら`openapi2aspida`や`openapi-generator`を使うことを強く推奨します。要は**API定義ファイルから型付きのAPIクライアントを生成**します。

例えば、バックエンドチームがAPIとAPI定義ファイルを提供してフロントチームがそれから実装するみたいなフローが考えられます。ツールを使わない場合はフロントの人がAPI定義ファイルからAPIクライアントのレスポンスの型をよしなにTypeScriptで記述する必要があります。(つらい)

ところが`openapi2aspida`を導入すると楽になります。流れとしては以下のようになります。

まず、APIの定義を書きます。今回はサンプルとして以下のAPI定義を用います。
https://github.com/OAI/OpenAPI-Specification/blob/master/examples/v2.0/yaml/petstore.yaml

```petshop.yml
...
paths:
  /pets:
    get:
      summary: List all pets
      operationId: listPets
      tags:
        - pets
      parameters:
        - name: limit
          in: query
          description: How many items to return at one time (max 100)
          required: false
          type: integer
          format: int32
      responses:
        "200":
          description: A paged array of pets
          headers:
            x-next:
              type: string
              description: A link to the next page of responses
          schema:
            $ref: '#/definitions/Pets'
        default:
          description: unexpected error
          schema:
            $ref: '#/definitions/Error'
...
```

`openapi2aspida`の設定ファイルを書いて

```aspida.config.js
module.exports = {
  input: "api",
  outputEachDir: true,
  openapi: { inputFile: "./petshop.yml" }
}
```

ビルドすると、

```bash
npx openapi2aspida
```

これだけで型付きのAPIクライアントが使える! うれC!

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/91558/d5c97b9c-6e79-c879-ee48-f605050bd8cd.png)

いわゆるスキーマ駆動開発が非常に効率的に行うことが可能になります。

`aspida`については製作者様のQiitaの記事がわかりやすいのでご覧ください。
https://qiita.com/m_mitsuhide/items/68406158d35a14fa0aa2


## 5. Bundle Analyzerを導入する: ページが重くなっていないか確認できるように

Code SplittingをよしなにやってくれるのがNext.jsのかなりの旨味です。ページに必要じゃないソースとかを絡めてもってこないから軽くできるよといった機能です。

ただ、野良ライブラリを使用するとNextのCode Splittingでも重くなることが稀に生じます。
そのため、Bundle Analyzerを仕込んでおいてライブラリ導入時等に確認することが推奨されます。

※`next build`時にNextが各ページのサイズを出力してくれるので、サイズ確認だけできればよいのであれば不要です。

`@next/bundle-analyzer`を使用する場合は以下のように設定しています。


```bash
yarn add --dev @next/bundle-analyzer cross-env
```

```js:next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
});

module.exports = withBundleAnalyzer({
  // 他の設定
})
```

```package.json
{
  "scripts": {
    "analyze": "cross-env ANALYZE=true next build",
    "build": "next build",
    ...
  }
  ...
}
```

確認時は以下のような動きになります。

1. `yarn build`してバンドルファイルを作成
2. `yarn analyze`して解析結果をブラウザで確認する (内部の処理としては以下のよう)
  1. `cross-env`で環境変数`ANALYZE`に`true`を入れる
  2. 次に`next build`される際に`enabled`が`true`になる
  3. Bundle Analyzerが仕事をする
  4. クライアントサイド, サーバーサイドのバンドルサイズがブラウザで確認できる

以下のように、視覚的にバンドルファイルに占めるコードを確認することができます。
以下の例では`lodash`が大きくバンドルファイルを占めていることが確認できますね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/91558/805ee9c4-9ba7-ba2a-1695-c1965e7ebb86.png)

画像は https://www.npmjs.com/package/webpack-bundle-analyzer のREADMEのものです。


## 6. 画像を最適化する

Next 10系以前は`next/image`が無いため、自分でよしなにする必要があります。
[next-images](https://www.npmjs.com/package/next-images)をいれるのが結構楽だったりします。

画像サイズによってインライン展開させることができるのでHTTPリクエストを減らせます。

```next.config.js
const withImages = require('next-images')
module.exports = withImages({
  inlineImageLimit: 16384,
  webpack(config, options) {
    return config
  }
})
```

また他には以下のような手法があります。画像はウェブの大半の通信を食うのでできることはやったほうがよいです。Lighthouse等もここらをやっているかどうかで大きく点数が変わってきます。

- 画像はCloudinaryやimgix, ImageFluxなどの画像よしな系SaaSで提供する
- Lazy Loadの設定を行う
- WebPを使ってあげる
- Nextから提供する画像はTinyPNG, SVGOMG等の圧縮の限りを尽くす

10系移行は`next/image`で良いでしょう。
ちなみに`next/image`は現時点ではStorybookは相性が悪いです。`next/image`が普通に画像を渡しているわけじゃないからです。
https://github.com/vercel/next.js/issues/18393

[@next/plugin-storybook](https://www.npmjs.com/package/@next/plugin-storybook)がよしなに整備されることを祈る or contributeしましょう。お願いします。

## その他

強いて言えば以下を考慮しています。

- `pages/*.(jsx|tsx)`には責務を持たせない
- コンポーネントをNext.jsに依存させない
- CSSに(ある程度の)秩序をもたせるように頑張る (ex: Sass, CSS in JS等)

# 最後に

Reactに塩梅の良い秩序と平穏をもたらしてくれるNext.jsは非常にうまいこと自らの責務の中で成長していると思います。Next.jsの責務のそれを超えた今回の(設定|プラクティス)が(どこか|誰か)のプロジェクトの参考になれれば幸いです。