個人開発で全国のクマの出没場所を記録するWebアプリケーションを作った話

## 完成品

https://kumapdigital.com/

（登録したデータ、画面上のデータはテスト環境のものです。実際のクマ被害、目撃情報とは一切関係ありあません）
![how to use.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3927117/0ef000d6-f10c-65f7-a015-56f2754a903a.gif)

## 開発の経緯

2023～2024年にかけて、日本全国でクマ被害が多く発生しました。

https://www.nhk.or.jp/bousai/articles/31606/

被害情報は各都道府県でまとまっているものの、それらは各県において独立しており、横断的な情報が少ないのが実情です。また、クマの情報は被害が起こったり通報が起こったりしてから各県のマップに連携されるかたちになっており、私のような市井の人間が人的被害の発生する前に投稿するような仕組みがあれば良いのではないかと感じました。

* クマの出没情報を全国規模で集約する
* WEBサイトを訪れた人が、クマについて投稿できるようにする

この2つを一番大きな要件として開発を進めました。

## アプリケーション構成

以下のような構成になっています。

![ApplicationStructure.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3927117/64944b35-934b-4fe9-0b3c-d40461676b75.png)

## 各技術要素の選定理由

### Next.js

Reactの学習がしたかったからという極めて個人的な理由から本フレームワークを選択しました。
https://nextjs.org/

### Material UI

Reactベースのフレームワークの中で最も人気があったのと、使用経験のある [Quasar](https://quasar.dev/) とドキュメントの構成が似ていたため本フレームワークを選択しました。
[TextFieldコンポーネントの中にGoogle Mapの検索欄を模した例がある](https://mui.com/material-ui/react-text-field/#using-the-theme-style-overrides-api) のも非常にありがたかったです。
https://mui.com/

### Maps API, Places API

Google Mapの機能を使うのに必要でした。Places API は住所の検索において使用します。
https://developers.google.com/maps?hl=ja

### Google Cloud

Maps API や Places API を使用するのに Google Cloud が必要であったため、その流れで使用しました。
ホスティング自体には App Engine を使用しています。`app.yml` の編集とコマンドだけで使用できるため、非常に使いやすかったです。
https://cloud.google.com/appengine?hl=ja

### express.js

フロントエンドの人間でバックエンド技術に明るくなかったため、フロントエンドと同じ Node.js で動作し、書きなれたTypeScriptが使えるということでこちらになりました。
https://expressjs.com/ja/

### webpack

express.jsで構築されたバックエンドアプリケーションをホスティングする際、何らかの手段でバンドルする必要性が生じました。[Vite](https://ja.vite.dev/) や [gulp](https://gulpjs.com/) といったツールを使う手もあったのですが、今回はインターネット上でよく解説を見る（印象のある）webpackを使うことにしました。
https://webpack.js.org/

### Google Firestore

こちらも No SQL のデータベースで Google Cloud にあるものを探しました。`geopoint` 型という位置情報（緯度経度）を格納できる型があり、 google map API と若干の親和性があるのかな？と思いました。
https://cloud.google.com/firestore?hl=ja

## 動作環境

* node.js　ver 18.18.2
* next.js　ver 14.0.3
* react　ver 18
* mui/material　ver 5.16.7
* express.js　ver 4.18.2

## 開発期間

4人月くらいでした。

## 開発で詰まったポイント

正直無限にあるのですが、今回は自分を大きく困らせたものを優先して紹介します。

### テキストボックス内でEnterキーを押すと画面全体のリロードが走ってしまう

material UI の `TextField` で Enterキーを押したとき、ページ全体にリロードが走ってしまう事象が発生しました。

**解決策**

form要素の `defaultEvent` が原因です。
以下のように、Enterキー押下時に `defaultEvent` の発生を抑制するコードを埋め込むことで解決しました。

```tsx
type Props = {
    text: string;
    onChange: (textInput: string) => void;
    onKeyDown: () => void;
};

export function TextBox(props: Props) {
    function onKeyDown(event: React.KeyboardEvent<HTMLDivElement>): void {
        // NOTE: Enterキー押下時にページ全体にリロードが走る事象の抑制
        if (event.key === "Enter") {
            event.preventDefault();
            props.onKeyDown();
        }
    }

    return (
        <TextField
            value={props.text}
            sx={{ ml: 1, flex: 1 }}
            placeholder="場所を検索"
            inputProps={{ "aria-label": "場所を検索" }}
            onChange={(event) => {
                props.onChange(event.target.value);
            }}
            onKeyDown={(event) => onKeyDown(event)}
            variant="standard"
        />
    );
}
```

【参考】

https://qiita.com/TK_WebSE/items/af0bed8c6274419e0c5a

### スクロールバーのあるDrawerを下までスクロールした後、再表示時に先頭に戻っていない

こちらは独自記事としてまとめました。

https://qiita.com/ga-maru/items/9c99613df99f5ec5aee9

### 初期表示がめっちゃ遅い

このようなコード（不必要な同期処理）を

```tsx
useEffect(() => {
    (async () => {
        const loader = new Loader({
            apiKey: process.env.NEXT_PUBLIC_MAP_API_KEY as string,
            version: "weekly",
            libraries: ["maps", "marker", "places"]
        });
        const { Map, InfoWindow } = await loader.importLibrary("maps");
        const { PlacesService } = await loader.importLibrary("places");
    })();
}, []);
```
以下のように修正（非同期的にimportする）することでかなりマシになりました。

```tsx
useEffect(() => {
    (async () => {
        const loader = new Loader({
            apiKey: process.env.NEXT_PUBLIC_MAP_API_KEY as string,
            version: "weekly",
            libraries: ["maps", "marker", "places"]
        });
        const [{ Map }, { PlacesService }] = await Promise.all([
            loader.importLibrary("maps"),
            loader.importLibrary("places")
        ]);
    })();
}, []);
```

### production環境で、postメソッドにおいてcorsエラーが発生する

Preflight request が飛んでいるのが原因です。 `Content-Type` は `application/json` のまま使いたかったため、OPTIONSメソッドの方で許可する設定を加えることにしました。
以下の処理をサーバーサイド（express.js）側に加えました。

```ts
app.options("<許可したいPOSTメソッドのURL>", async function (_, res) {
    res.setHeader("Access-Control-Allow-Origin", "<クライアントサイドのURL>");
    res.setHeader(
        "Access-Control-Allow-Headers",
        "X-Requested-With, X-HTTP-Method-Override, Accept, Origin, Authorization, Content-Type"
    );
    res.writeHead(200);
    res.send();
});
```

【参考】

https://qiita.com/Shoma0210/items/4405898205a1822f5826

### production環境での初期表示時、fontawesomeのアイコンが読み込めない

事前にCSSをインポートする必要があります。 `/pages/_app.tsx` に以下の内容を記載します。

```tsx
import type { AppProps } from "next/app";

// NOTE: fontawesomeをproduction環境で読み込めるようにするための記載
import { config } from "@fortawesome/fontawesome-svg-core";
import "@fortawesome/fontawesome-svg-core/styles.css";

config.autoAddCss = false;
// ここまで

export default function App({ Component, pageProps }: AppProps) {
    return <Component {...pageProps} />;
}
```

【参考】

https://docs.fontawesome.com/web/use-with/react/use-with#next-js

## 開発において重要視したこと

### エフォートレス

完全に自分の造語ですが、「頑張らなくても理解できる」を目指しました。
難解な用語や一目でわかりづらいUIを廃し、なんとなく触っているだけで使い方が分かるようなアプリケーションにしたいと考えました。そのため、画面は多くの方に使用経験があるであろう Google Map に似せ、文字についても極力少なくなるようにしました。

↓ 初期表示時の画面（開発中のため、画面に表示されている情報は虚偽のものです）
![chibabear.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3927117/476d5ca8-cc07-77a1-f1b0-fda433d5d467.png)

### リーンソフトウェア開発

開発中は、常に「本当に必要な機能か？」を考え、バックログをリファインメントするようにしていました。
バックログ上には上がっていたものの、最終的に実装を見送った機能には以下のようなものがあります。

* 画面右側に各アイコン（目撃、駆除など）のボタンを配置し、そのピンのみでソートできるようにする
* いたずらと思われる投稿を削除する
* 場所のテキストを入力した時、検索候補を表示する
* 地図上のあり得ない場所にピンが登録された時、バリデーションチェックでそれを防ぐ

開発時点でアプリケーションを使っていただけるかは分からなかったので、最初に立てた要件（「クマの出没情報を全国規模で集約する」「WEBサイトを訪れた人が、クマについて投稿できるようにする」）との関連性が薄いものについては実装を見送りました。

### 新機能実装 < リファクタリング

ここが最も気を付けたポイントでした。
個人開発では他者のレビューを経ずに `main` にマージされます。その結果、「当時は最良のコードだと思っていたけど、今見るとなんか違う…」が量産されることになります。
コードの保守性を一定に保つため、リファクタリングしたい箇所に気づいた時には必ずそれを **新機能実装より上の優先度** にすることを心がけていました。

![productBacklog.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3927117/038a74bc-3eda-b9ed-1b58-636b61582b87.png)

## 反省点

### 必要性の薄い「目的地に戻る」ボタン

小さなことですが、画面右上に実装した「目的地に戻る」ボタンは今見ると不要だな……と思いました。
そもそも使っていただけるか分からないアプリケーションなのに、要件を果たすのではなく小手先の便利機能を実装しても……と感じました。

![currentPosition.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3927117/706c24d3-c75f-1270-90ad-4aec88d1ccab.png)

## まとめ

個人開発はモチベーションの維持が大変ですが、自分ひとりでアプリケーションを作り上げる達成感は他では得られないものがありました。
だいぶポエム寄りの記事になってしまい申し訳ございません。
これを読んでいる方も、自分の持っている技術で開発してみてください！