Cut0 Blog

Open Hack U で入賞できなかった話 〜技術編〜

はじめに

下記の記事の通り、Open Hack U で入賞することはできませんでした。
とはいえ、折角の機会だったので今回は技術的な話を中心にしたいと思います。
https://cut0-blog.vercel.app/r7fkj_gzpvre

全体のアーキテクチャ

全体のアーキテクチャ図は以下のようになります。
ハードウェアから Cloud Storage 上の mp3 ファイルをダウンロードすることで音声再生を実現しています。Firebase に乗っかった開発にすることで開発速度の高速化を目指しました。

NewMo! のアーキテクチャ

技術選定

ハッカソンであるため、ソフトウェアは下記のことに注意して技術選定を行いました。

  • 完全に無料であること
  • 柔軟で高速かつ比較的安全に開発できること

今回のハッカソンでは開発費が支給されませんでした。そのため、Next.js を利用していますが、Firebase Hosting 上に SSG させるようにしております。Firebase Hosting を利用した理由は下記のようになっております。

  • Vercel を Organization で利用すると有料になるため
  • Firebase に乗っかった開発をすることで開発の高速化を目指したかったため
  • Firebase Hosting の GitHub Actions の設定が公式から提供されており導入が比較的容易なため

https://firebase.google.com/docs/hosting?hl=ja

Heroku Container Registry を利用した理由は下記のようになっております。

  • Docker Container ベースのアプリケーションをそのままデプロイできるため
  • ハッカソン期間であればまだ無料プランを使えるため (無料プランは2022年11月頃に廃止されます)

https://devcenter.heroku.com/ja/articles/container-registry-and-runtime

また、クライアントサイドでもサーバーサイドでも 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

のようにすることで下記のように利用することができます。

フロントエンドで API コール
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 というパッケージを作成しています。
https://www.npmjs.com/package/openapi-typescript-codegen

swagger-ui-react

API のドキュメントを生成するために利用しました。 Vite 内でコンポーネントとして利用し、Firebase Hosting にデプロイすることでチームメンバーが誰でも閲覧できるようにしました。
https://www.npmjs.com/package/swagger-ui-react

rikabi-creators/hack-u-api

バックエンド用のリポジトリです。@rikabi-creators/hack-u-api-client を利用しているという点以外は特に変わったことはしておりません。
利用技術は下記のようになっております。

Prisma

TypeScript でバックエンドを開発する際に、ORM として TypeORM を利用するか迷ったのですが下記の理由から TypeORM の利用は見送らせていただきました。

  • 破壊的変更が多いため
  • デコレータによる記法が必要になるため
  • スキーマ定義を TypeScript のファイルで行っているため

また、Prisma を利用した理由は下記のようになります。

  • Prisma Studio が便利だったため
  • 公式のドキュメントが読みやすいため
  • 今回の開発ではそこまで DB 設計が複雑になることはないと考えていたため
  • Prisma を利用する際に必要な設定が少なくかつブラックボックス化されていないため
  • Prisma が提供する型定義や記法がわかりやすかったため

Prisma 自体はまだまだ成熟したライブラリとは言えないという問題点がありましたが、せっかくのハッカソンだということでチャレンジしてみました。
https://www.prisma.io/

Fastify

Nest.js を利用した開発も案として思い浮かんだのですが、下記の理由から見送らせていただきました。

  • デコレータによる記法が必要になるため
  • Nest.js という規約に従った開発ができるが、ブラックボックス化されてしまう点が多いため

そのため、express ライクに書けて高速であることを押し出している Fastify を利用しました。ドキュメントが express に比べて少ないという問題点はありましたが、今回の開発では特に問題ないと判断し利用しました。
https://www.fastify.io/

rikabi-creators/hack-u-web

Web フロントエンド向けのリポジトリです。こちらも特に変わったことはしておりません。どちらかというと開発フローを工夫しました。
利用技術は下記の通りになっております。

React.js + Next.js + TypeScript

定番の構成なのでまとめて書かせていただきました。今回は前述の通り、完全に無料で利用するために Firebase Hosting にデプロイする必要があったため next export を利用しました。
下記の記事では Cloud Functions を利用する構成になっておりますが、今回は SSG による静的生成さえできていれば問題ないと考えたので next export を利用しております。
https://zenn.dev/masakasuno1/articles/0988d547ab1de8
https://nextjs-ja-translation-docs.vercel.app/docs/advanced-features/static-html-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,
  );

https://swr.vercel.app/ja

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 構造から分割することで解決できたように感じます。
今回の開発の反省点になっております。
https://emotion.sh/docs/introduction

Vitest

Jest ライクに書けて高速であるため採用しました。
https://vitest.dev/

Storybook

コンポーネントに対して一つの storyfile を記載しました。VRT はハッカソンの期間の都合上導入できませんでしたが、普段の開発では導入したいと思っています。
Firebase Hosting にデプロイされるのでチームメンバーが誰でもコンポーネントを確認することができます。
https://storybook.js.org/

開発フロー

各リポジトリの開発フローについて解説します。

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日前の出来事だったため、とてもヒヤヒヤしました。
https://developer.mozilla.org/ja/docs/Web/API/MediaRecorder
https://developer.mozilla.org/ja/docs/Web/API/AudioContext

感想

ハッカソンという限られた期間での開発だったので、まだまだ改善できる点はあったかもしれません。とくに開発フローに関してはもう少し整えられたと思います。
とはいえ、技術選定は比較的うまくいっており、ほとんどスケジュール通りに開発することができました。また、今まで触ろうと思っていてもなかなか触れていなかった技術を触ることができたのが非常に良かったです。
今回のハッカソンで賞が取れなかったことは悔しかったですが、今後の開発にも生かしていきたいと思います。