見出し画像

Next.jsを導入した時のトラブルシューティング

三菱UFJフィナンシャル・グループ(以下MUFG)の戦略子会社であるJapan Digital Design(以下JDD)でフロントエンドエンジニアをしている小笠原です。

本記事では、既存のSingle Page Application(以下SPA)サービスにNext.jsを導入した時のトラブルシューティングについてご紹介させていただきます。


はじめに

昨年度に投稿したSPA関連のトラブルシューティングの記事はこちらになります。

日頃からSPAを開発しており、プロジェクトでNext.jsを導入してみたい、あるいは既に導入済みで日々改善に挑んでいる方も多いと思います。

今回、弊社でも既存のReactアプリケーションにNext.jsを導入し日々奮闘しております。Next.jsを導入する前後では構造や概念の観点で大きな変化や知識のアップデートが必要とされるので、一気に導入せずフェーズを分けることにしました。予測できないような状況や、考慮漏れ等が発生した場合、将来的にその問題が負債となってしまいます。まずはチームで議論し、どこまでを最初のゴールとするのかを明確にしてから導入することをお勧めします。例えば、Next.jsを将来的に剥がすことをチームとして想定しているのであれば、その前提ありきのフェーズの分け方があると思います。

そもそもSPAにNext.jsを導入する目的としては、Server Side Rendering(以下SSR)したい、あるいはServer Actionsを使いたい、などといった従来のSPAでは解決できないことが発生したことが挙げられると思います。(なお、弊社でNext.jsを採用した理由は、プロジェクト内で既にNext.jsが動いている実績があったことや、他にも導入に際して肯定的な様々な理由がありました。)
しかしNext.jsを導入する、という類の記事はネット上に巨万とあるので今回は「トラブルシューティング」という題目でいくつかご紹介したいと思います。


Next.jsの導入

説明の簡略化のため、細かいライブラリはここでは省いています。また、移行の際、技術変更は極力行っていないので、将来的に更に良い形へブラッシュアップしていく必要があると思っています。

パッケージ

CSS

Emotion の導入について

上記ドキュメントによると Emotion は未サポート。今回は移行コストを鑑みるとNext.js導入の初期フェーズでは CSS-in-JS を変更せず、 Emotion を動作させる必要が生じました。
なお、Next.jsでは以下のCSS-in-JSがサポートされています。(2024年8月29日現在)

例えば `next.config.js` 内で下記のように、 `isServer` フラグを使ってClient Side Rendering(以下CSR)の時のみ emotion の babel-plugin を追加するようにするなどし、既存のコードと比べて挙動に差が出ないように対策しました。

// next.config.js

// コンパイラーの設定で emotion を false にする
compiler: {
  emotion: false
}

// webpack の設定で server ではない時に emotion をバンドルに含める
webpack: (config, { isServer, defaultLoaders }) => {
  // 既存の設定を記述

  if (!isServer) {
    // emotion の設定を追加
  }
}

開発中のトラブルシューティング

Error: Server Functions cannot be called during initial render. This would create a fetch waterfall. Try to use a Server Component to pass data to Client Components instead.

`use client` から `use server` のコンポーネントを呼ぶと発生するため、そのようなコードがないか確認します。

Text content does not match server-rendered HTML

エラーが発生した理由はドキュメントに記載されており、それに対する簡単な対処法も記されています。それぞれの状況に応じて対応してあげるのが良いでしょう。
元々CSRで構成されたコードの場合、SSR化したタイミングでHydration不一致の問題は発生することがよくありますので、上記のドキュメントは参考になるでしょう。

Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:

`if (typeof window !== 'undefined')` だったらフロントコードを実行させるようにして、フロントとサーバーでコードを分離する必要があります。

Warning: React has detected a change in the order of Hooks called by {Component_Name}.

フックの呼び出し順序などがおかしくなると発生します。例えば、コンポーネント内で throw する条件などを見直してリファクタリングすることで解消できます。

Error: `headers` was called outside a request scope.

`next/headers` などの動的なAPIをリクエストの外で呼び出すとエラーになります。Next.jsの動作としては、レンダリング中に動的関数を検出したタイミングでルート全体を動的にレンダリングするようにできているため、リクエストコンテキストが存在しない状態で呼び出すとエラーになってしまいます。

SSRからのAPI通信がエラーになる

CSRの場合だとAPI通信が疎通できるため少しハマったのですが、SSRするサーバーからのアクセスも許可する必要があります。

Storybookのトラブルシューティング

ERR_UNKNOWN_URL_SCHEME

通常の `<img>` ではなく、 `next/image` を使うことで解消できます。
`next/image` を使うことで自動画像最適化機能が備わっていますが、一方で画像がキャッシュされるまでは表示が遅く感じるといった課題もあります。
状況に応じて、`next/image` のドキュメントに記載の https://nextjs.org/docs/app/api-reference/components/image のオプションや、組み込みの画像最適化のライブラリ https://nextjs.org/docs/messages/install-sharp を導入して対応しました。

App Router の useSearchParams を動かす

以下のようにしてあげると、 next/navigation 側が正常に挙動します。

export const Default: Story = {
  parameters: {
    navigation: {
      pathname: '/animals',
      query: {
        genre: 'dog'
      }
    }
  }
}

Vitestのトラブルシューティング

SyntaxError: The requested module 'vite' does not provide an export named 'parseAstAsync'

Testing: Vitest のマニュアルセットアップをすると発生するので、 Vitest のバージョンを 0.34.6 に戻すことで発生しなくなりました。バージョン起因の動作不具合に関しては、Next.jsでも今後対応されると思います。

その他

robots タグ を SSR で動的に変更したい

noindex, nofollow にしたい場合の例。(実際はもっと汎用的なものにする必要があります。)

// SSRでページ毎に動的に変更するための関数を作っておく
const buildRobots = ({ index, follow, indexifembedded }: Robots): Metadata => ({
  robots: {
    index,
    follow,
    indexifembedded,
  }
});
// A) layout.tsx で呼び出して使用する
export async function generateMetadata() {
  return buildRobots({ index: false, follow: true, indexifembedded: false });
}

// 実際に表示されるHTML
<meta name="robots" content="index, nofollow" />

通常は indexさせず、 iframe の時だけ index させたい時
https://developers.google.com/search/blog/2022/01/robots-meta-tag-indexifembedded?hl=ja

// A) layout.tsx で呼び出して使用する
export async function generateMetadata() {
  return buildRobots({ index: true, follow: false, indexifembedded: true });
}

// 実際に表示されるHTML(iframeの時だけindexさせたい)
<meta name="robots" content="noindex, follow, indexifembedded" />

SSRでページ共通のヘッダを設定したい

ページ全部に跨るヘッダに関わる処理の場合は、Middleware を利用すると良さそうです。ただし、src配下に設置しないとうまく動作しません。使い方は下記のコードの通りで、Next.jsが受け取った `NextRequest` を操作しながら `NextResponse` をページに送信し、ページでは、受け取った `NextResponse` を使って処理をすることができます。
ただし、 middleware を利用すると Next.js への接続時に必ず動作してしまうので、抑制したいページなどがある場合は分岐するか、そもそも別の解決策も模索した方が良いかもしれません。

// src/middleware.ts
export function middleware(request: NextRequest) {
  const requestHeaders = new Headers(request.headers)
  requestHeaders.set('x-alice', 'Alice')
 
  const response = NextResponse.next({
    request: {
      headers: requestHeaders,
    },
  })
 
  response.headers.set('x-bob', 'Bob')
  return response
}
// page.tsx
export async function geenrateMetadata() {
  console.log(headers().get('x-alice');
  console.log(headers().get('x-bob');
}

さいごに

今回Next.jsを導入してみて、その技術の進化に驚きながら作業を進めました。初期フェーズではNext.jsのパワーを完全には使いこなせませんでしたが、将来的にはベストプラクティスを模索し、対応していきたいと思います。

最後までご覧いただきありがとうございました。

この記事は、2024年8月29日時点の情報を元に記載されております。


Japan Digital Design株式会社では、一緒に働いてくださる仲間を募集中です。カジュアル面談も実施しておりますので下記リンク先からお気軽にお問合せください。

この記事に関するお問い合わせはこちら

Technology & Development Division
Senior Engineer
Sinnosuke Ogasawara