【Next.js (App Router)】MSWとSWRを組み合わせたデータ取得
はじめに
今回はApp Directory(TypeScript)の構成でボタンを押すと、ハンドラーで作成した/msw-user
からJSON形式のレスポンスを取得するように記述してみたいと思います。(MSWを使用してAPIのレスポンスをモックし、SWRを使用してそのデータをフロントエンドで取得しています。)
MSWとは
ウェブアプリケーションの開発時に、実際のサーバーにリクエストを送らなくても、APIのレスポンスを模倣(モック)するためのツールです。
下記の3つの点で便利かなと思います。
- バックエンドが完成していない段階でも、フロントエンドの開発を進めることができます。
- 実際のサーバーを使わずに、APIが期待通りに動くかどうかを確認することができます。
- 同じモック設定を、開発環境やテスト環境で共通して使用することができます。
MSWの仕組みは、ブラウザのService Worker技術を使って、アプリケーションからのネットワークリクエストを捕捉します。捕捉したリクエストに対して、プログラムした通りのダミーのレスポンス(モックレスポンス)を返すことができます。
SWRとは
Next.jsを開発しているVercelによって提供されているReactベースのデータ取得のためのライブラリです。
もう少し詳しい記事です。
インストール
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
が生成されます。
"msw": {
"workerDirectory": [
"public"
]
}
ハンドラーの作成
APIのモックリクエストに対するレスポンスを定義するためのハンドラー関数を格納するためのファイルを作成します。
src
以下にmock
ディレクトリを作成し、handler.ts
ファイルを作成します。
mkdir -p src/mock && touch src/mock/handler.ts
ファイル構造は下記のようになります。
src/
└── mock/
└── handler.ts
下記のように記述してハンドラーを作成します。
今回はわかりやすいように/msw-user
というエンドポイントにしています。
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
下記の内容でセットアップを記述します。
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
に設定します。
'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
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
};
確認
ブラウザでアプリケーションに移動し、開発者ツールでコンソールを開きます。
下記が表示されている場合は赤文字ですがエラーではなく、MSWが正常に起動して有効になっていることを示しています。
このメッセージが表示されない場合やエラーが表示される場合は、このページのすべての手順が完了していることをもう一度確認してみてください。
SWRを使用したモックデータの取得
ボタンを押すと、ハンドラーで作成した/msw-user
からJSON形式のレスポンスが取得できると思います。
'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
クラスを利用してください。
Module '"msw"' has no exported member 'setupWorker'.ts(2305)
インポートパスについて、msw
ライブラリから直接ではなく、msw/browser
からsetupWorker
をインポートしてください。
修正前
import { setupWorker } from 'msw'
修正後
import { setupWorker } from 'msw/browser'
Package path ./browser is not exported from packageのエラーが出る場合
mswライブラリはクライアントサイドとサーバーサイドの両方のAPIモッキングをサポートしていますが、msw/browser
はブラウザ専用です。
ですが、Next.jsのビルドプロセスでは、サーバーサイドとクライアントサイドのコードが混在しており、この環境下でクライアント専用のモジュールをサーバー側のコードからインポートしようとすると問題が発生するようです。
終わりに
何かありましたらお気軽にコメント等いただけると助かります。
ここまでお読みいただきありがとうございます🎉
Discussion