🦒

【Next.js (App Router)】MSWとSWRを組み合わせたデータ取得

2024/04/18に公開

はじめに

今回はApp Directory(TypeScript)の構成でボタンを押すと、ハンドラーで作成した/msw-userからJSON形式のレスポンスを取得するように記述してみたいと思います。(MSWを使用してAPIのレスポンスをモックし、SWRを使用してそのデータをフロントエンドで取得しています。

MSWとは

ウェブアプリケーションの開発時に、実際のサーバーにリクエストを送らなくても、APIのレスポンスを模倣(モック)するためのツールです。
https://mswjs.io/

下記の3つの点で便利かなと思います。

  • バックエンドが完成していない段階でも、フロントエンドの開発を進めることができます。
  • 実際のサーバーを使わずに、APIが期待通りに動くかどうかを確認することができます。
  • 同じモック設定を、開発環境やテスト環境で共通して使用することができます。

MSWの仕組みは、ブラウザのService Worker技術を使って、アプリケーションからのネットワークリクエストを捕捉します。捕捉したリクエストに対して、プログラムした通りのダミーのレスポンス(モックレスポンス)を返すことができます。
https://qiita.com/poster-keisuke/items/00056b8d5d6275afdb1a

SWRとは

Next.jsを開発しているVercelによって提供されているReactベースのデータ取得のためのライブラリです。
https://swr.vercel.app/ja

もう少し詳しい記事です。
https://zenn.dev/nenenemo/articles/41279d9de4935c

インストール

MSWが主に開発やテスト中に利用されるため、--save-devオプションを使用して開発環境にMSWをインストールしています。

npm install msw --save-dev

下記コマンドでswrライブラリもインストールしてください。

npm install swr

MSWの初期化

MSWは、プロジェクトのパブリックディレクトリにサービスワーカースクリプトを配置する必要があります。

これを行うためにターミナルで次のコマンドを実行します。

通常publicフォルダに配置しますがプロジェクトによって異なる場合があるため、適切なディレクトリ名に置き換えてください。

npx msw init public

初期化プロセス中に、将来のワーカースクリプトの更新を容易にするために、package.jsonにワーカーディレクトリのパスを保存するかどうかを尋ねられます。

これにより、publicディレクトリに生成されるmockServiceWorker.jsファイルの場所がpackage.jsonに記録され、MSWの将来的なアップデートや再設定がスムーズに行えるようになります。

https://mswjs.io/docs/getting-started

INFO In order to ease the future updates to the worker script,we recommend saving the path to the worker directory in your package.json.
? Do you wish to save "public" as the worker directory? (Y/n) #Yesを選択

package.jsonに下記の記述が追加され、public/mockServiceWorker.jsが生成されます。

package.json
 "msw": {
    "workerDirectory": [
      "public"
    ]
  }

ハンドラーの作成

APIのモックリクエストに対するレスポンスを定義するためのハンドラー関数を格納するためのファイルを作成します。

src以下にmockディレクトリを作成し、handler.tsファイルを作成します。

mkdir -p src/mock && touch src/mock/handler.ts

ファイル構造は下記のようになります。

src/
└── mock/
    └── handler.ts

下記のように記述してハンドラーを作成します。
今回はわかりやすいように/msw-userというエンドポイントにしています。

handler.ts
import { http, HttpResponse } from 'msw';

export const handlers = [
  http.get('/msw-user', (info) => {
    if (info.request.headers.get('X-Error') === 'true') {
      return HttpResponse.error();
    }

    return HttpResponse.json({
      name: 'Mike',
      age: 30,
      job: 'engineer',
    });
  }),
];

MSWのモックサーバーをセットアップする

下記は、ブラウザ環境でMSWを起動するためのスクリプトです。

mockディレクトリにindex.tsファイルを作成します。

touch src/mock/index.ts

-pオプションは、必要に応じて中間ディレクトリも作成するオプションです。

ファイル構造は下記のようになります。

src/
└── mock/
    └── index.ts

下記の内容でセットアップを記述します。

src/mock/index.ts
import { handlers } from './handler';

// グローバル変数として worker を定義
export let worker: any;

// クライアントサイドで実行する場合のみ、mswをロードし、workerを設定する
if (typeof window !== 'undefined') {
  import('msw/browser')
    .then(({ setupWorker }) => {
      // MSWのworkerインスタンスを作成
      worker = setupWorker(...handlers);

      // 開発環境の場合にのみ、workerを起動する
      if (process.env.NODE_ENV === 'development') {
        worker.start();
      }
    })
    .catch((error) => console.error('Failed to load MSW:', error));
}

Next.jsアプリケーションでのMSWの起動

開発環境でのみMSWを起動するために、下記のようにlayout.tsxに設定します。

src/app/layout.tsx
'use client';
import { metadata } from '@/config/metadata';
import { useEffect } from 'react';

export default function Layout({ children }: { children: ReactNode }) {

useEffect(() => {
    if (process.env.NODE_ENV === 'development') {
      // 非同期インポートで worker をロード
      import('../mock/index')
        .then(({ worker }) => {
          if (worker) {
            worker.start();
          }
        })
        .catch((err) => console.error('Failed to start MSW:', err));
    }
  }, []);

  return (
    <html lang="ja">
      <head>
        <title>{String(metadata.title) || 'Default Title'}</title>
        <meta
          name='description'
          content={metadata.description || 'Default description'}
        />
      </head>
      <body>
        {children}  
      </body>
    </html>
  );
}

型安全を確保するためにString(metadata.title)は、metadata.titleを文字列に変換しています。

layout.tsxで'use client';を記述してエラーになる場合

例えば、先ほどのlayout.tsxではmetadataを使用していたのですが、その場合は'use client';を記述しているとエラーになります。

したがって、metadataを別のファイルに移動し、必要な場所からそのファイルをインポートして解決しています。

mkdir -p src/config && touch src/config/metadata.ts
metadata.ts
import type { Metadata } from 'next';

export const metadata: Metadata = {
  title: 'Create Next App',
  description: 'Generated by create next app',
};

確認

ブラウザでアプリケーションに移動し、開発者ツールでコンソールを開きます。

下記が表示されている場合は赤文字ですがエラーではなく、MSWが正常に起動して有効になっていることを示しています。

このメッセージが表示されない場合やエラーが表示される場合は、このページのすべての手順が完了していることをもう一度確認してみてください。

SWRを使用したモックデータの取得

ボタンを押すと、ハンドラーで作成した/msw-userからJSON形式のレスポンスが取得できると思います。

src/app/page.tsx
'use client';

import useSWR from 'swr';

export default function Msw() {
  const fetcher = (url: string) => fetch(url).then((res) => res.json());
  const { data, error, isValidating, mutate } = useSWR('/msw-user', fetcher, {
    revalidateOnFocus: false,
    shouldRetryOnError: false,
    revalidateOnMount: false,
  });

  const onClick = () => {
    mutate();
  };

  return (
      <div>
        <h1>MSW</h1>
        {error && <p>データの読み込みに失敗しました。</p>}
        {data && (
          <div>
            <p>名前: {data.name}</p>
            <p>年齢: {data.age}</p>
            <p>職業: {data.job}</p>
          </div>
        )}
        <button onClick={onClick}>モックデータを取得</button>
      </div>
  );
}

Module '"msw"' has no exported member 'rest'.ts(2305)

最新のMSWバージョンでは、httpが優先され、restが非推奨になりました。
下記を参考にしてください。

また、httpモジュールを使用する場合、応答リゾルバー関数のシグネチャが変更されています。新しいシグネチャでは、req、res、ctxの3つの引数を取るのではなく、infoオブジェクトを引数として受け取りHttpResponseクラスを利用してください。
https://mswjs.io/docs/migrations/1.x-to-2.x/
https://mswjs.io/docs/api/http-response/

Module '"msw"' has no exported member 'setupWorker'.ts(2305)

インポートパスについて、mswライブラリから直接ではなく、msw/browserからsetupWorkerをインポートしてください。
https://mswjs.io/docs/migrations/1.x-to-2.x/

修正前

import { setupWorker } from 'msw'

修正後

import { setupWorker } from 'msw/browser'

Package path ./browser is not exported from packageのエラーが出る場合

https://github.com/mswjs/msw/issues/1877

mswライブラリはクライアントサイドとサーバーサイドの両方のAPIモッキングをサポートしていますが、msw/browserブラウザ専用です。

ですが、Next.jsのビルドプロセスでは、サーバーサイドとクライアントサイドのコードが混在しており、この環境下でクライアント専用のモジュールをサーバー側のコードからインポートしようとすると問題が発生するようです。

終わりに

何かありましたらお気軽にコメント等いただけると助かります。
ここまでお読みいただきありがとうございます🎉

Discussion