MSW(Mock Service Worker)でモックデータを永続化する

## はじめに
最近、[MSW](https://mswjs.io/)というモックサーバーがあることを知り、軽く触ってみたところ、使えそうだなと思ったのと同時にPOSTで登録したデータなど次回起動以降も使いまわせたら便利そうだなと思いました。

今回はMSWでデータを永続化する方法について紹介します。

## 前提
MSWについてはある程度の理解があることを前提とします。  
下記あたりの記事をご覧いただければ、どんなことができるかわかると思います。

https://zenn.dev/t_keshi/articles/ok-ihave-mockapi

https://zenn.dev/ryo_kawamata/articles/mock-api-server-with-msw

## 実装
永続化の実装としては[こちらのリポジトリ](https://github.com/alan2207/bulletproof-react)の`src/test/server/db.ts`から拝借します。
非常にコードがわかりやすいので特に解説はしないですが、一応、コメントだけつけています。
※簡略化のため、`models`は必要最低限としています。

```typescript:db.ts
import { factory, primaryKey } from '@mswjs/data'

const models = {
  user: {
    id: primaryKey(Number),
    name: String,
    email: String,
  },
}

export const db = factory(models)

export type Model = keyof typeof db

// localStorageからデータ取得
export const loadDb = () =>
  Object.assign(JSON.parse(window.localStorage.getItem('msw-db') || '{}'))

// localStorageへデータ保存
export const persistDb = (model: Model) => {
  const data = loadDb()
  // @ts-ignore
  data[model] = db[model].getAll()
  window.localStorage.setItem('msw-db', JSON.stringify(data))
}

// モックDBの初期化
export const initializeDb = () => {
  const database = loadDb()
  // DBの復元
  Object.entries(db).forEach(([key, model]) => {
    const dataEntres = database[key]
    if (dataEntres) {
      dataEntres?.forEach((entry: Record<string, any>) => {
        model.create(entry)
      })
    }
  })
}

// localStorageをリセット
export const resetDb = () => {
  window.localStorage.clear()
}

initializeDb()
```

### モック用API
データ登録するための処理を作ります。
```typescript:users.ts
import { rest } from 'msw'

import { db, persistDb } from '@/mocks/db'

export const usersHandlers = [
  // getについては省略
  rest.post('/api/users', async (req, res, ctx) => {
    const data = await req.json()
    const id = db.user.count() + 1
    const result = db.user.create({
      ...data,
      id,
    })

    // データを追加したので保存（PATCHやDELETEも同様）
    persistDb('user')

    return res(
      ctx.status(200),
      ctx.json(result),
    )
  }),
]
```

### ディレクトリ構造
最終的にMSWに関するディレクトリ構造はこのような感じになりました。
```
src
└── mocks
     ├── handlers
     │    └── index.ts
     │    └── users.ts
     ├── db.ts
     └── browser.ts
```

## APIを叩いてみる
ここまで準備できたら、実際にモック用のAPIを叩いてみます。
MSWだからといって特に意識することなく、通常通りのコードを書けばOKです。
```jsx:App.tsx
import { useEffect, useState } from 'react'
import { Button, Container, Flex, Input, List, MantineProvider } from '@mantine/core'
import { useForm } from '@mantine/form'

import { resetDb } from '@/mocks/db'

type User = {
  id: number
  name: string
  email: string
}

type Form = {
  name: string
  email: string
}

function App() {
  const [users, setUsers] = useState<User[]>([])
  const form = useForm({
    initialValues: {
      name: '',
      email: '',
    },
  })

  // user一覧取得
  const fetchData = async() => {
    const data = await fetch('/api/users').then((res) => res.json())
    setUsers(data)
  }

  // user情報登録
  const handleSubmit = async(values: Form) => {
    const data = await fetch('/api/users', {
      method: 'POST',
      body: JSON.stringify(values),
    }).then((res) => res.json())

    setUsers([...users, data])
    form.reset()
  }

  useEffect(() => {
    fetchData()
  }, [])

  return (
    <MantineProvider withGlobalStyles withNormalizeCSS>
      <Container>
        <form onSubmit={form.onSubmit(handleSubmit)}>
          <Flex my="md" gap={5}>
            <Input placeholder="UserName" {...form.getInputProps('name')} />
            <Input placeholder="Email" {...form.getInputProps('email')} />
            <Button type="submit">Send</Button>
            <Button color="red" onClick={() => { resetDb() }}>Reset</Button>
          </Flex>
        </form>

        {!users.length && (
          <p>user情報がありません</p>
        )}

        <List>
          {users.map(user => (
            <List.Item key={user.id}>
              {user.name}({user.email})
            </List.Item>
          ))}
        </List>
      </Container>
    </MantineProvider>
  )
}

export default App
```

### データ追加
POSTすると、localStorageに値が追加されていることが確認できます。

![msw01.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/359978/74d2f591-f401-9bc4-ac61-dbae0ed7e2a1.gif)


### 画面更新・再起動
ブラウザをリロードしたり、アプリケーションを再起動してもlocalStorageに保持されているデータを取得できていることが確認できます。

![msw02.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/359978/abbcce4b-36dc-d6af-87e9-2f4fc3442599.gif)


## 注意事項
この記事を読んでいる読者層的には今更なことかもしれませんが、下記の点に注意が必要です。

- **localStorageは同じドメイン（今回ローカル環境なのでport番号も含む）で共有されるため、値を上書きされないよう注意**
  - プロジェクトごとにkeyを変更する（今回の場合`msw-db`になっています）
    - ただし、localStorageには容量制限があるため、同じドメインにデータを多くは保持できない
  - そもそもプロジェクトごとにport番号を変えてしまう
- **ブラウザを変えたら値は共有されない**
  - どうしてもデータを引き継ぎたいのであれば、開発者ツールからコピペなどでデータを移しましょう

## 参考

https://mswjs.io/

https://github.com/alan2207/bulletproof-react

https://github.com/mswjs/data

## 最後に
### GoQSystemでは一緒に働いてくれる仲間を募集中です！
ご興味がある方は以下リンクよりご確認ください。

https://goqsystem.recruit.site/?qiita=9eda23a489c8e6e3bf27