サーバーレス(React+Firebase)で新型コロナウイルスの感染者数マップを作った話

Firebaseを使ってサーバーレスで新型コロナウイルス(COVID-19)の感染者数の推移を地域別・日別で見られるWebサイトを作成しました。
https://covid19-visualization.web.app/
![map_screen.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/513800/256db333-6529-f766-cd1d-c97252fdfb87.png)


以下のような技術を使って制作しています。

| システム・ツール                           | 役割                                        |
| ------------------------------------------ | ------------------------------------------- |
| React                                      | フロント側の画面の構築                      |
| Google Maps Platform - Maps JavaScript API | 地図の表示                                  |
| Firebase Hosting                           | ホスティング                                |
| Cloud Functions for Firebase               | APIサーバー・感染者数データのスクレイピング |
| Cloud Firestore                            | データベース                                |
| Google Cloud Scheduler                     | スクレイピングを定期実行するためのトリガー  |



個人開発のプロセスとして、企画編・開発編・リリース運用編としてそれぞれ書いていますので、よかったら参考にしていただければと思います。

具体的なコードはほとんど載せていないのですが、作成をするときに参考にしたサイトや公式サイトのリファレンス等のリンクは載せていますので、そちらを参考にしていただければと思います。



## 企画編

#### 作ろうと思ったきっかけ

ニュースを見ていても、その日だけの新規の感染者数や累計での感染者数の報道が多く、今現在どの程度感染が拡大しているのかが分かりづらいと思い、視覚的に感染者数の推移を確認できるものがほしいと思って作りました。



※ 他にも感染者数の推移を確認できるサイトはいろいろとあります。以下のようなサイトもよければご覧ください。

- [厚生労働省 - 新型コロナウイルス感染症 国内事例](https://mhlw-gis.maps.arcgis.com/apps/opsdashboard/index.html#/c2ac63d9dd05406dab7407b5053d108e)
- [朝日新聞 - 新型コロナウイルス感染者数の推移](https://www.asahi.com/special/corona/)
- [東洋経済ONLINE - 新型コロナウイルス国内感染の状況](https://toyokeizai.net/sp/visual/tko/covid19/)
- [都道府県別新型コロナウイルス感染者数マップ](https://gis.jag-japan.com/covid19jp/)



#### 主な機能

主な機能としては、地図で感染者数の推移を確認できる機能とグラフで確認できる機能があります。

##### 地図表示機能
![map_screen.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/513800/46f86c1c-9e12-ea15-b79b-35094e2168d2.png)
都道府県別に感染者数を円で表示しています。感染者数の数値は[厚生労働省の報道発表資料](https://www.mhlw.go.jp/stf/seisakunitsuite/bunya/0000121431_00086.html)から取得しています。本当は市区町村単位での表示にしたかったのですが、公開されているデータが必ずしも市区町村別になっていないため、都道府県単位で集計をして各都道府県の県庁所在地に円をプロットしています。

感染が拡大しているのか縮小しているのかが視覚的にわかるように、日別にデータを表示することができます。また、画面右側にある「日付を自動で切替え」のボタンを押すことで自動で日付が切り替わるようになっています。


##### グラフ表示機能
![graphe_screen.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/513800/64aa7961-fb57-b60b-2e8f-a4f25b1dba8d.png)

日ごとの全国の感染者数の累計をグラフで表示しています。

都道府県別でも見られるようにしたかったのですが、まだ間に合っておらず作成できていません。今後作成予定です。



## 開発編

ここからがQiita的には本編の記事になります。

開発編として設計と実装に分けて説明をしていきます。



### 設計

個人での開発とはいえ、今後も機能拡張はしていきたいので、メンテナンスや機能追加のしやすいものを作りたいと思います。そのためにある程度の設計はしてから進めます。



#### 画面設計と機能の洗い出し

まずは画面設計です。

画面設計を最初に行うのはユーザビリティとして問題なさそうかを確認するという目的ももちろんありますが、

- 必要な機能の洗い出し
- 必要なデータの洗い出し

という目的もあります。



ワイヤーフレームを設計するためのツールとして、今回は[JUSTINMIND](https://www.justinmind.com/)というものを使いました。JUSTINMINDはデスクトップアプリが提供されています。他のツールにはオンラインで使えるものもいろいろありますが、オフラインでも作業ができた方がストレスなくできるため、今回はオフラインでも使えるこのツールにしました。

作成したワイヤーフレームは以下です。
![app-screen-design.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/513800/bcfb38ed-849e-0b63-553f-b183fea0aa55.png)

作成をした画面を見ながら、機能に漏れがないかの確認やどのようなデータをどういった形式で保存するのかを検討していきました。

また、この段階でコンポーネントをどの単位で分割をするのかもある程度決めておきます。以下のようにしました。(コンポーネント名もこの段階で決めてしまいます。適当な名前で進めてしまうと、気に入らないとなったときにあとから変更をするのが面倒ですので。)
![app-screen-design-component.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/513800/6c7e84ae-db19-da77-670f-bfe28dd0bf54.png)



#### システム構成

次に作成をした画面と機能一覧を見ながらシステム構成を検討します。

バックエンド側には以下のような機能が必要になります。

- APIサーバー: フロントエンド側からの呼び出しにより、感染者数のデータを返却します。

- スクレイピングサーバー: 厚生労働省のホームページからスクレイピングにより感染者数のデータを取得します

- データベース：スクレイピングにより取得したデータを保存します。

  

これを踏まえ作成したシステム構成は以下になります。
![system-architecture.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/513800/6313df63-8a8f-049c-358d-769c4b41fc5f.png)


クライアント側はReactで作り、サーバー側はFirebaseで作っています。

管理画面は初期の検討段階では無かったのですが、後述する実現性の検証をした際に必要そうなことがわかったため、システム構成に入れました。



#### 実現性検証

実装に入る前に、難易度が高そうなところや実現性がわからない機能についてはあらかじめ実現性の検証をしていきます。

今回のシステムで言えば、スクレイピングでのデータ取得ができるのかというのができるかがわからないところがありましたので、この部分について検証をしました。



感染者数のデータは厚生労働省ホームページの[こちらの報道発表一覧のページ](https://www.mhlw.go.jp/stf/seisakunitsuite/bunya/0000121431_00086.html)から取得できます。この一覧から、[例えばこちらの詳細ページ](https://www.mhlw.go.jp/stf/newpage_10358.html)へのリンクが貼られています。一覧ページと詳細ページをスクレイピングしていけばデータは取得できそうです。

もう少し検証を進めて、スクレイピングによりすべてのデータが正しく取得できるかを検証したところ、以下のような課題がありました。

1. 感染者数の数値がすべて1つのdivタグに囲まれており(下記に貼った画面キャプチャ参照)、CSSセレクタを指定してのデータ取得ができない
2. 都道府県単位でのデータになっていたり市単位でのデータになっている
3. 「東 京 都：患者７例」というように、「東」と「京」の間にスペースが入っている場合があったり、「患者○例」となっていることがほとんどなのに、たまに「患者○名」となっている
4. 最近(3月ごろ)のページのフォーマットと初期(1月から2月始めごろ)のページのフォーマットが異なる

1についてはスクレイピングで全文を取得したあとに、正規表現でテキストを抽出することにしました。

2〜4については正規表現やデータフォーマットの変換で対応できるものについてはできるだけ対応をしつつ、スクレイピングした結果を目視で確認することとしました。(データ補正用の管理画面が必要となったのはこのためです。)

**データ取得元のページ**
![scrapingpage.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/513800/802ae52a-0b96-533b-712a-3c18f4b2bafa.png)


### 実装

ここからは実際の実装作業について、順を追って説明をしていきます。



#### 環境構築

最初にフロントエンド側、バックエンド側ともに環境を構築します。

- フロントエンドはReactで開発をします。[Create React App](https://ja.reactjs.org/docs/create-a-new-react-app.html#create-react-app)でプロジェクトを作成します。
- ホスティングの設定もします。[Firebase Hostingのチュートリアル](https://firebase.google.com/docs/hosting/quickstart?hl=ja)を参考にしてプロジェクトを初期化します。
- バックエンドはFirebaseのCloud Functionsで開発をします。[Cloud Functionsのチュートリアル](https://firebase.google.com/docs/functions/get-started?hl=ja)を参考にしてプロジェクトを初期化します。



ここまでできたら一旦デプロイをして、環境が正しく構築できているかを確認します。無事に環境が構築できてCreate React Appで作成した初期ページが表示されることが確認できました。



**使用したツール・システム**

- [Create React App](https://ja.reactjs.org/docs/create-a-new-react-app.html#create-react-app)
- [Firebase Hosting](https://firebase.google.com/docs/hosting?hl=ja)
- [Cloud Functions for Firebase](https://firebase.google.com/docs/functions?hl=ja)



#### ディレクトリと空のファイルの作成

次に大まかなディレクトリ構成を決めて、空のファイルだけを作成しておきます。

開発をする中で変更をしたり足したりしたものもありますが、最終的には以下のような構成となっています。

```bash
covid19-visualization
├── backend
│   ├── firebase.json
│   ├── firestore.rules
│   ├── functions
│   │   ├── constants/ # 定数(都道府県と都道府県コードの対応表など)
│   │   ├── database/ # Firestore接続部分の機能
│   │   │   └── Firebase.js
│   │   ├── index.js
│   │   ├── models/ # モデル層
│   │   ├── package.json
│   │   ├── routes/ # ルーティング層
│   │   └── utils/ # 汎用機能(主にスクレイピング関連)
│   └── package.json
└── frontend
    ├── build
    ├── firebase.json
    ├── package.json
    ├── public
    └── src
        ├── constants # 定数(都道府県ごとの座標値など)
        ├── index.js
        ├── state
        │   ├── modules
        │   │   ├── app-state # アプリの状態を扱う機能
        │   │   │   ├── actions.js
        │   │   │   ├── index.js
        │   │   │   ├── operations.js
        │   │   │   ├── reducers.js
        │   │   │   ├── selectors.js
        │   │   │   └── types.js
        │   │   ├── covid19data/ # 画面に表示するデータを扱う機能
        │   │   ├── covid19data-edit/ # 編集用のデータを扱う機能
        │   │   └── index.js
        │   ├── store.js
        │   └── utils/
        └── views
            ├── App.js
            ├── admin # 管理画面に表示するコンポーネント
            │   ├── Admin
            │   ├── Login
            │   └── Table
            ├── map # 地図関連のコンポーネント
            │   ├── GraphViewer # といいつつ、グラフも入っている。命名が良くなかった。
            │   ├── Map
            │   ├── MapController
            │   ├── MapLayout
            │   ├── MapViewer
            │   └── Title
            ├── settings # 設定関連のコンポーネント
            │   ├── Settings
            │   ├── SettingsFooter
            │   ├── SettingsItem
            │   ├── SettingsItemAutoPlay
            │   ├── SettingsItemDisplayMode
            │   └── SettingsItemSelectedRegion
            └── utils # 汎用機能(日付のフォーマット変換等)
```

フロントエンド側はRe-ducksの構成に従っています。[発案者が書いた記事](https://www.freecodecamp.org/news/scaling-your-redux-app-with-ducks-6115955638be/)がありますので、詳しくはこちらを参考にしてください。



#### スクレイピングの実装

ここまで来て、ようやく具体的な機能の実装を始めます。

最初にスクレイピング部分の実装です。
![scraping.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/513800/33bb95bd-1a5a-d8eb-c8b9-82bce41c29eb.png)




##### スクレイピング

スクレイピングは大まかには以下のようなロジックです。

1. [厚生労働省の報道発表一覧のページ](https://www.mhlw.go.jp/stf/seisakunitsuite/bunya/0000121431_00086.html)からデータが記述された詳細ページのURLを取得する
2. [詳細ページ(例えばこちらのページ)](https://www.mhlw.go.jp/stf/newpage_10358.html)から感染者数のデータを取得する
3. 取得したデータをFirestoreに保存する



Firestoreへの保存については、[公式サイトのチュートリアル](https://firebase.google.com/docs/firestore/quickstart?hl=ja)を参考にして実装しまいた。



実装をして、ローカル環境では問題なかったのですがデプロイをしたところ以下のようなエラーとなりました。

```
Error: memory limit exceeded. Function invocation was interrupted.
```

どうやらメモリ不足のようです。

デフォルトで割り当てられているメモリや256MBになっているようですので、[Firebase公式サイトのこちらのページ](https://firebase.google.com/docs/functions/manage-functions?hl=ja#set_timeout_and_memory_allocation)を参考にして、メモリを1GBに増やしました。



##### スクレイピングの呼び出し

スクレイピングの呼び出しは、Firebaseの[関数のスケジュール設定](https://firebase.google.com/docs/functions/schedule-functions?hl=ja)のページを参考にして実装します。



**使用したツール・システム**

- [Puppeteer](https://github.com/puppeteer/puppeteer) (Node.jsのスクレイピング用ライブラリ)
- Cloud Functions for Firebase ([Google Cloud Scheduler](https://cloud.google.com/scheduler/?hl=ja)を使用した定期実行)
- [Cloud Firestore](https://firebase.google.com/docs/firestore?hl=ja)



#### APIの実装

次にバックエンド側のAPIを実装します。
![apiserver.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/513800/5721ca90-6a41-09bb-7f7a-57f860360325.png)


Cloud FunctionsをHTTP経由で呼び出せるようにします。[公式サイトのチュートリアル](https://firebase.google.com/docs/functions/http-events?hl=ja)を参考にして実装します。Express アプリをそのままHTTP関数に渡すことができますので、今回はこの方法を採用しました。



**使用したツール・システム**

- [Cloud Functions for Firebase (HTTP経由での呼び出し)](https://firebase.google.com/docs/functions/http-events?hl=ja)
- [Express](https://expressjs.com/ja/)



#### 画面の作成

ここからはフロントエンド側の実装に入ります。

最初にまずは画面を作ります。
![screen.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/513800/61120eae-ed78-6136-9839-d38745f840ab.png)



UIをゼロから作るのも時間がかかりそうだったので今回はUIライブラリを使いました。今回使用をしたのは[Material-UI](https://material-ui.com/)というライブラリです。Reactで簡単にマテリアルデザインのUIを作ることができます。

設計時にコンポーネントの分割方法は決めていますので、この内容に沿って実装をしていきました。



**使用したツール・システム**

- [Material-UI](https://material-ui.com/)

  

#### データ取得機能の実装

次にAPI経由でデータを取得する部分を実装します。

特別なこともなく、淡々と実装をしていきます。



**使用したツール・システム**

- [axios](https://github.com/axios/axios)



#### 地図表示の実装

ここまで来てようやく地図表示部分の実装になります。

地図表示機能は主に以下の2つになります。

- Google Mapを表示する機能
- Google Map上に感染者数の大きさを表す円をプロットする機能



Google Mapを表示する機能については、[公式サイトの解説](https://developers.google.com/maps/documentation/javascript/tutorial?hl=ja)を参考にして進めていきます。Google Map APIをES6で使う方法についてはこちらの[【JavaScript・ES6】Google Mapの埋め込みスニペットのページ](https://blog.yuhiisk.com/archive/2018/05/01/google-map-snippet.html)も参考にさせていただきました。

円をプロットする機能についてはこちらも[公式サイトのVisualizing Data](https://developers.google.com/maps/documentation/javascript/earthquakes?hl=ja)にあるチュートリアルを参考にして進めました。



地図上に情報を表示するためのフォーマットとして、[GeoJson](https://ja.wikipedia.org/wiki/GeoJSON)というものがあります。Google Map上に円を表示する際にも、このGeoJsonというフォーマットに変換をしています。



**使用したツール・システム**

- [Google Maps Platform - Maps JavaScript API](https://developers.google.com/maps/documentation/javascript/tutorial?hl=ja)



#### グラフ表示の実装

データをグラフで表示する機能の実装です。[Recharts](http://recharts.org/en-US/)というライブラリを使用しています。



**使用したツール・システム**

- [Recharts](http://recharts.org/en-US/)



#### 管理画面の作成
以下の管理画面部分を作成します。
![adminscreen.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/513800/5cc3353c-9892-9c46-9c78-e5194c6f4dbb.png)


管理画面は以下のような感じです。
![admin.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/513800/58873069-0a62-97e5-eb36-58a3afede8a0.png)


ユーザーが使用する画面と同様にMaterial-UIを使用して実装しています。

またテーブル表示には、Material-UIを拡張して作られた[material-table](https://material-table.com/#/)というライブラリを使用しています。



**使用したツール・システム**

- [Material-UI](https://material-ui.com/)
- [material-table](https://material-table.com/#/)



## リリース・運用編

#### デプロイ

実装が終わり、無事にテストも終わったところでデプロイをします。

Firebase HostingのデプロイもCloud Functionsのデプロイも以下のコマンドを打つだけで簡単にデプロイできます。

```bash
xxx$ firebase deploy
```



#### 予算上限・アラートの設定

そんなに利用者数も多くないため、Firebaseの無料枠の範囲内で収まるとは思いますが、念のため予算の設定をしておきます。

Firebaseには1ヶ月間の使用料金が超過したときにアラートを知らせてくれる機能があります。Firebaseの[使用量と制限](https://firebase.google.com/docs/firestore/quotas?hl=ja)のページを参考にして設定しました。

また、1日あたりの料金もアラート設定の説明ページと同じページにある[1 日あたりの費用制限を設定する](https://firebase.google.com/docs/firestore/quotas?hl=ja#set_spending_limits)に書いてある方法で設定できるようです。

(これも設定をしたかったのですが、このページに書いてあるとおりに進めても設定をするページが現れず、設定できませんでした。。。Googleのサポートに電話をかけても１時間待ってもつながらず、結局設定できていません。。。どなたかやり方をご存知でしたらコメント欄などで教えていただけると嬉しいです。)



ということで、完成したのが[こちらのページ](https://covid19-visualization.web.app/)になります。ご質問、ご要望などございましたらコメント欄などでお気軽にお知らせください！



## 今後の予定

当初は以下のような機能も実装をしようと思っていたのですが、時間が足りずに残念ながらまだ実装できていません。今後実装をする予定です。

- 地域別に感染者数を表示する機能

- 表示期間を設定する機能

- 感染予防のためのお役立ち情報集

  

(他にもこんな機能があったらいいというのがあれば、ぜひコメントください！)