はじめに
下記の記事の通り、Open Hack U で入賞することはできませんでした。
とはいえ、折角の機会だったので今回は技術的な話を中心にしたいと思います。
全体のアーキテクチャ
全体のアーキテクチャ図は以下のようになります。
ハードウェアから Cloud Storage 上の mp3 ファイルをダウンロードすることで音声再生を実現しています。Firebase に乗っかった開発にすることで開発速度の高速化を目指しました。
NewMo! のアーキテクチャ
技術選定
ハッカソンであるため、ソフトウェアは下記のことに注意して技術選定を行いました。
- 完全に無料であること
- 柔軟で高速かつ比較的安全に開発できること
今回のハッカソンでは開発費が支給されませんでした。そのため、Next.js を利用していますが、Firebase Hosting 上に SSG させるようにしております。Firebase Hosting を利用した理由は下記のようになっております。
- Vercel を Organization で利用すると有料になるため
- Firebase に乗っかった開発をすることで開発の高速化を目指したかったため
- Firebase Hosting の GitHub Actions の設定が公式から提供されており導入が比較的容易なため
Heroku Container Registry を利用した理由は下記のようになっております。
- Docker Container ベースのアプリケーションをそのままデプロイできるため
- ハッカソン期間であればまだ無料プランを使えるため (無料プランは2022年11月頃に廃止されます)
また、クライアントサイドでもサーバーサイドでも Node.js を利用しております。利用した理由は下記のようになります。
- バックエンドもフロントエンドも開発する必要があり、プログラミング言語の違いによって生まれる障壁を生みたくなかったため
- API Client (後述) により、TS の型定義を吐き出してフロントエンド・バックエンドで共通して利用できるようにするため
- 私が Node.js に少しだけ慣れていたため
リポジトリ構成
今回のハッカソンでは主に3つのリポジトリを利用しました。
rikabi-creators/hack-u-api-client
API の定義をドキュメントとして残したり、ドキュメントから API コール関数と型定義を自動生成し@rikabi-creators/hack-u-api-client
というプライベートパッケージに注入したりします。
フロントエンドとバックエンド用のリポジトリでは
yarn add @rikabi-creators/hack-u-api-client
のようにすることで下記のように利用することができます。
import {
AlarmService,
GetMyAlarmListResponse,
} from "@rikabi-creators/hack-u-api-client";
const response : GetMyAlarmListResponse = await AlarmService.getMyAlarmList({ idToken }).catch(
handleApiError,
);
import {
GetMyAlarmListResponse,
} from "@rikabi-creators/hack-u-api-client";
const getMyAlarmListResponse: GetMyAlarmListResponse = {
items: myAlarmList,
};
return reply.code(200).send(getMyAlarmListResponse);
バックエンドでは完全に型の安全性を保証できておりませんが、フロントエンドでは API の route をベタ書きする必要が無いため非常に役に立ちました。
最近では tRPC が流行っているようですが、API コール関数もパッケージに固めてしまいたかったので今回のような構成にしました。
利用技術は下記のようになっております。
openapi-typescript-codegen
OpenAPI の YAML や JSON から上記のような API コール関数を生成できます。OpenAPI Generatorは Java の環境が無いと動作しないのがネックだった点と今回は TypeScript 向けの型定義を吐ければ充分だったため、openapi-typescript-codegen を利用しました。
実際は GitHub Actions 上で動作させて @rikabi-creators/hack-u-api-client
というパッケージを作成しています。
swagger-ui-react
API のドキュメントを生成するために利用しました。 Vite 内でコンポーネントとして利用し、Firebase Hosting にデプロイすることでチームメンバーが誰でも閲覧できるようにしました。
rikabi-creators/hack-u-api
バックエンド用のリポジトリです。@rikabi-creators/hack-u-api-client
を利用しているという点以外は特に変わったことはしておりません。
利用技術は下記のようになっております。
Prisma
TypeScript でバックエンドを開発する際に、ORM として TypeORM を利用するか迷ったのですが下記の理由から TypeORM の利用は見送らせていただきました。
- 破壊的変更が多いため
- デコレータによる記法が必要になるため
- スキーマ定義を TypeScript のファイルで行っているため
また、Prisma を利用した理由は下記のようになります。
- Prisma Studio が便利だったため
- 公式のドキュメントが読みやすいため
- 今回の開発ではそこまで DB 設計が複雑になることはないと考えていたため
- Prisma を利用する際に必要な設定が少なくかつブラックボックス化されていないため
- Prisma が提供する型定義や記法がわかりやすかったため
Prisma 自体はまだまだ成熟したライブラリとは言えないという問題点がありましたが、せっかくのハッカソンだということでチャレンジしてみました。
Fastify
Nest.js を利用した開発も案として思い浮かんだのですが、下記の理由から見送らせていただきました。
- デコレータによる記法が必要になるため
- Nest.js という規約に従った開発ができるが、ブラックボックス化されてしまう点が多いため
そのため、express ライクに書けて高速であることを押し出している Fastify を利用しました。ドキュメントが express に比べて少ないという問題点はありましたが、今回の開発では特に問題ないと判断し利用しました。
rikabi-creators/hack-u-web
Web フロントエンド向けのリポジトリです。こちらも特に変わったことはしておりません。どちらかというと開発フローを工夫しました。
利用技術は下記の通りになっております。
React.js + Next.js + TypeScript
定番の構成なのでまとめて書かせていただきました。今回は前述の通り、完全に無料で利用するために Firebase Hosting にデプロイする必要があったため next export
を利用しました。
下記の記事では Cloud Functions を利用する構成になっておりますが、今回は SSG による静的生成さえできていれば問題ないと考えたので next export
を利用しております。
SWR
基本的に Get による HTTP リクエストは冪等であるため、SWR に載せました。SWR は大体下記のような構造で利用することが多いですが、useSWR の第一引数に null をセットすると検証が行われなくなるのが便利でした。
import {
AlarmService,
GetMyAlarmListResponse,
} from "@rikabi-creators/hack-u-api-client";
import useSWR from "swr";
const { data, error, mutate } = useSWR<GetMyAlarmListResponse, Error>(
authState.status === "login" ? getKey(authState.payload.idToken) : null,
fetcher,
);
Emotion
babel の適切な設定を行うことで、css props を利用し下記のインラインスタイルのように書けるため採用しました。Next.js 上の Storybook でスタイルを適用するには少し工夫が必要です。
render(
<div
css={{
backgroundColor: 'hotpink',
'&:hover': {
color: 'lightgreen'
}
}}
>
This has a hotpink background.
</div>
)
スタイルの命名をやめることで高速な開発ができると思っていましたが、複雑なデザインだとコードレビューの時に差分が追い辛くなってしまいました。このあたりは適切な粒度でのコンポーネント分割や vanilla-extract や CSS Modules のようにスタイルの実体を DOM 構造から分割することで解決できたように感じます。
今回の開発の反省点になっております。
Vitest
Jest ライクに書けて高速であるため採用しました。
Storybook
コンポーネントに対して一つの storyfile を記載しました。VRT はハッカソンの期間の都合上導入できませんでしたが、普段の開発では導入したいと思っています。
Firebase Hosting にデプロイされるのでチームメンバーが誰でもコンポーネントを確認することができます。
開発フロー
各リポジトリの開発フローについて解説します。
rikabi-creators/hack-u-api-client
基本的にエンドポイントの定義を追加後、main ブランチにマージし GitHub Packages のバージョンアップを行うことで @rikabi-creators/hack-u-api-client
の更新を行います。
rikabi-creators/hack-u-api
staging ブランチにマージ後、検証用の API が作成されるのでそこで検証を行い main ブランチにマージします。main ブランチにマージすると自動で本番環境が更新されます。
rikabi-creators/hack-u-web
PR 作成時に検証用の URL が発行されます。そこでの挙動と Storybook の整合性を確認後、main ブランチにマージします。main ブランチにマージすると自動で本番環境が更新されます。
音声の扱いに関して
最後に音声の扱いで少し躓いたので簡単に説明させていただきます。
我々は音声データを利用するために、MediaRecorder という Web API を利用していました。ところがこの API で生成できるファイル形式がブラウザごとに違うという問題点がありました。時に Chrome と Safari では全く異なる形式の音声ファイルを生成します。
通常のアプリケーションではその問題にも対応できるのですが、今回はハードウェアが絡んでいることもあり、mp3 ファイルでないとハードウェアから再生できなかったため、AudioContext という Web API に切り替えることで対応しました。
ハッカソン発表日の2日前の出来事だったため、とてもヒヤヒヤしました。
感想
ハッカソンという限られた期間での開発だったので、まだまだ改善できる点はあったかもしれません。とくに開発フローに関してはもう少し整えられたと思います。
とはいえ、技術選定は比較的うまくいっており、ほとんどスケジュール通りに開発することができました。また、今まで触ろうと思っていてもなかなか触れていなかった技術を触ることができたのが非常に良かったです。
今回のハッカソンで賞が取れなかったことは悔しかったですが、今後の開発にも生かしていきたいと思います。