mswを導入し、お手軽にモックAPIを作る(React)

## はじめに
フロントエンド開発を進める中で、機能実装を進めたいけど、バックエンドの実装がまだ完了していない、というケースは結構あると思います。

APIの設計さえ決まっていれば、モックを用意して、機能開発を進めることが出来ます。その際に、msw(Mock Service Worker)が結構便利だったので、備忘録として残しておきます。

開発してると以下のケースって割と遭遇すると思います。

・早めにフロントエンド開発に着手したいけど、APIがまだ完成してない。
・テストで使用するモックデータが必要

この場合、mswは結構便利かなと思います。

## mswって何？

mswは、ブラウザ側からのAPIリクエストをService Workerがインターセプトして、任意のmockデータを返すためのライブラリです。特徴の一つとして、ブラウザでもnode.js上でも動作する点が挙げられるかと思います。これは、テストコードやStorybook等を利用してるプロジェクトでも活かせる利点となります。

https://mswjs.io/

導入は簡単です。
詳細を知りたい方は上記公式サイトを参照してみてください。

## Reactプロジェクト作成〜msw導入

```terminal:sampleAppの雛形
$ npx create-react-app msw_sample --template typescript
$ cd msw_sample
$ npm install msw --save-dev
```

## モックの定義
公式サイトを読むと、モック定義の管理に厳密なルールはないようですが、関連モジュールを単一のディレクトリにまとめることが推奨されています。

```terminal:mocksディレクトリの作成し、handlerを記述するファイルを作成
$ mkdir src/mocks
$ touch src/mocks/handlers.ts
```
次にインターセプトする際のハンドラーの処理を実装していきます。
REST APIリクエストを処理するには、HTTPメソッドとパス、レスポンスを指定する必要があります。

以下に例を示します。

```ts:handlers.ts
import { rest } from "msw";
import { User } from "../common/types/User";

export const handlers = [
  rest.post("/login", (_, res, ctx) => {
    sessionStorage.setItem("is-authenticated", "true");

    return res(ctx.status(200));
  }),

  rest.get("/user", (_, res, ctx) => {
    const isAuthenticated = sessionStorage.getItem("is-authenticated");

    if (!isAuthenticated) {
      return res(
        ctx.status(403),
        ctx.json({
          errorMessage: "Not authorized",
        })
      );
    }

    return res(
      ctx.status(200),
      ctx.json({
        username: "Taro",
        age: 30,
        role: "admin",
      } as User)
    );
  }),
];

```

リクエストに対してレスポンスを返すようにするには、リゾルバー関数を使用してモックされたレスポンスを指定する必要があります。
リゾルバー関数は、以下の引数を指定できます。

**req**
今回は特に記述していないですが、リクエストの情報が入ってくるので、ここの値に応じて、レスポンスを変更することも可能。

**res**
レスポンスを作成するための機能ユーティリティ。

**ctx** 
モックされた応答のステータスコード、ヘッダー、本文などを設定するのに役立つ関数のグループ。


## Service Workerのコードを生成
mswはService Workerを使用して、APIリクエストをインターセプトします。そのService Workerのコードをプロジェクトの公開ディレクトリに追加するコマンドが用意されています。Reactは、`./public`になるので、そちらに追加しましょう。

```terminal:mockServiceWorkerをpublicディレクトリに生成する
 $ npx msw init public/ --save
```
![スクリーンショット 2021-11-22 22.16.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33632/376bc971-4d23-e8d5-ecd0-0745383393d6.png)

生成されたmockServiceWorker.jsには、Service Workerでのイベント処理が書かれています。

## Service Workerを起動するファイルを作成

続いて、Service Workerを生成して起動するために必要なファイルを作成します。

```terminal
$ touch src/mocks/browser.ts
```

生成したファイルにはリクエストハンドラーを渡してワーカーインスタンスを作成する処理を書きます。

```ts:src/mocks/browser.ts
import { setupWorker } from "msw";
import { handlers } from "./handlers";

export const worker = setupWorker(...handlers);

```

## エントリポイントにworkerをimportする
mswを使用するためには、アプリケーションのエントリポイント( `index.tsx` )にService Workerのスタート処理をインポートする必要があります。開発環境でのみ、src/mocks/browser.tsファイルをインポートします。(本番環境でmockを使うことはまず無いでしょう)

```ts:src/index.tsx
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter } from "react-router-dom";
import App from "./App";

if (process.env.NODE_ENV === "development") {
  const { worker } = require("./mocks/browser");
  worker.start();
}

ReactDOM.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>,
  document.getElementById("root")
);
```

これで開発時にプロジェクトを立ち上げると、モックも動作するようになりました！

## 実際に使ってみる

```terminal:アプリケーション起動
$ npm run start
```

consoleに以下の表示がされていれば、mswは正常に動作しています。
![スクリーンショット 2021-11-23 9.33.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33632/49c82415-e2f3-9aec-f9a6-24c21e29dd51.png)


あくまで簡易的なものですが、モックをリクエストしてるコンポーネントの実装は以下の通りです。(ログインボタン押下 → Topページへ遷移)

```tsx:src/pages/Login/index.tsx
import React from "react";
import { useNavigate } from "react-router-dom"; //React-Router ![Something went wrong]()
V6 

export const Login = () => {
  const navigate = useNavigate();
  const USER_PAGE = "/user";

  const login = () => {
    fetch("http://localhost:3000/login", { method: "POST" }).then(() =>
      navigate(USER_PAGE)
    );
  };

  return <button onClick={login}>login</button>;
};

export default Login;

```

```tsx:src/pages/Home/index.tsx
import React, { useEffect, useState } from "react";
import { UserCard } from "../../common/components/UserCard";
import { User } from "../../common/types/User";

export const Home: React.FC = () => {
  const [user, setUser] = useState<User>({ username: "", age: null, role: "" });

  useEffect(() => {
    const fetchUser = async () => {
      await fetch("http://localhost:3000/user")
        .then((res) => {
          return res.json();
        })
        .then((res) => {
          setUser(res);
        })
        .catch((res) => {
          console.log(res.errorMessage);
        });
    };
    fetchUser();
  }, []);

  return (
    <>
      <h1>Hello</h1>
      <UserCard user={user} />
    </>
  );
};

export default Home;
```


```tsx:src/common/components/UserCard/index.tsx
import React from "react";
import { User } from "../../types/User";

type Props = {
  user: User;
};

export const UserCard: React.FC<Props> = (props: Props) => {
  const { user } = props;
  return (
    <>
      <p>
        氏名: {user.username}/{user.age}歳
      </p>
      <p>役割: {user.role}</p>
    </>
  );
};
```

DevToolのNewworkタブを見てみましょう。
インターセプトされたリクエストが確認できるでしょうか？

![スクリーンショット 2021-11-23 10.05.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33632/873030ad-c25e-f00e-8211-56bb3ce73750.png)

![スクリーンショット 2021-11-23 10.05.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33632/5daa98ce-3108-eaf8-c762-2d2a4c1be5e1.png)

![msw2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33632/7b440ef2-e0ff-3893-7d11-a4bb686acd19.gif)

## 最後に
以上、mswの利用例でした。