39
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

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

この記事はNext.js Advent Calendar 2020の18日目の記事です。

はじめに

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

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

0. TypeScriptを使う

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

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

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

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

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

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

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

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}`,
    }),
  }),
};

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

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

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

GitHubのサンプルコードママ

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に反映される

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

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

image.png

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

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

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

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

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

image.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://www.netlify.com/blog/2016/07/20/introducing-deploy-previews-in-netlify/ から

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

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

APIの定義がSwaggerなどのOASを使っているプロジェクトならopenapi2aspidaopenapi-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" }
}

ビルドすると、

npx openapi2aspida

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

image.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を使用する場合は以下のように設定しています。

yarn add --dev @next/bundle-analyzer cross-env
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で環境変数ANALYZEtrueを入れる
    2. 次にnext buildされる際にenabledtrueになる
    3. Bundle Analyzerが仕事をする
    4. クライアントサイド, サーバーサイドのバンドルサイズがブラウザで確認できる

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

image.png

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

6. 画像を最適化する

Next 10系以前はnext/imageが無いため、自分でよしなにする必要があります。
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がよしなに整備されることを祈る or contributeしましょう。お願いします。

その他

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

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

最後に

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
39
Help us understand the problem. What are the problem?