🗒

レシピ管理アプリ開発振り返り

2021/08/21に公開

概要

レシピ管理サービス Recipon の開発の途中であれこれやってみたことの振り返りです。さらっと読めるように書くつもりなのでよかったら数分のお茶のお供にでもどうぞ 🍵 開発中に学んだこととか気づいたこととかを少し時間をおいて振り返ることで忘れにくくなったらいいなあというやつです。

免責事項

本題に入る前に、毎度おなじみのやつです。
この記事を執筆現在私はGoogle 働いています。記事内にGoogleのプロダクトが出てくるため所属を明確にしましたが、私は各プロダクトについて特別詳しい知識を持っているわけではありません。1コミュニティユーザーが書いた記事としてお楽しみください。

Spreadsheet で簡易DB?

最初データベースにはGoogle Spreadsheetを使うことを考えました。というのも、最初はユーザーの環境で完結するものを考えていたからです。その上で、スマホでもPCでも使えるという前提は崩したくなかったのでアカウントと紐づけられるSpreadsheetに着地しました。
もう1つの理由は最初は本当に小さなコードを構想していて、記入ページを作らずに管理できるようにしようかと思っていたからです。Spreadsheetなら直接DB触ってくれというよりも色々な層の人にとっつきやすいと思いました。(自分用に作っているものの、自分だけと思うと色々適当になって勉強の機会が失われそうなので他の人が使うことも少しは考えながら開発しました)
Sheets APIでなんとなく原型を作りましたが、結局開発中のデータ構造の変更に手間がかかることや、自分で使っていて普通にサービス上で記入ページが欲しいと感じてしまったので使い慣れているFirestoreに変更しました。

求めていたのはIntl.Segmenter

レシピって大体材料一覧と工程に別れていると思うのですが、携帯で料理を作りながらレシピを見ているとき、工程からいちいち材料欄に戻って次に使う材料の分量を確認するのが面倒でした。手間としては本当にちょっとですが、それでも面倒なのが人間です。料理番組みたいに最初に全部準備したらこういう悩みは無くなると思うのですが私は軽量を楽しむタイプではないので楽しくない部分は最小限で料理がしたい…!
そこで、工程に出てくる材料名をタップしたら分量が確認できたら便利だなと思いました。Firestoreからレシピ持ってくる時点でそういう処理をしておいたら話は早い気はするんですがもうある程度開発が進んでいたので今からコアの部分のデータ構造は変更したくないし、材料と工程のデータがあればそこから導き出せるものなのであえて保存しておく必要もないように感じました。
というわけでレシピデータ取得後に加工する方針に決め、工程の文章を単語で区切って材料が含まれていたら分量を表示するツールチップを出すことにします。ちょうどTC39のIntl.Segmenterの情報を数日前に目にしていて、これが使えるのではないかと思い、試したところまさに私が求めていたものでした。
うっすら気づいた方もいるかもしれませんが問題はブラウザサポートです。Intl.SegmenterはChromeとSafariのPreviewでは利用可能ですが特にメインユーザーの私がこの機能を必要としているiOS Safariではサポートされていませんでした。

ICUとの出会い

まず考えたのはPollyfillです。検索するとIntl Segmenter Polyfillというパッケージがヒットしました。デフォルトではベースで使っている ICU という言語処理のライブラリのwasmモジュールにタイ語データしか含まれていないということで、ローカルでクローンして日本語オプションに設定し直してwasmをビルドします…が上手くいきませんでした。オプション変更前後でタイ語の分割ができなくなっているのでwasmの中身自体はしっかり変わっているようなのですがどうにも日本語の分割が上手くいかずでした。ICUのデモで分割自体はしっかりできていたのでビルド設定かビルド後のJSの問題であって本来はできるものだと思います。
そのあたり試行錯誤している途中でIntl.Segmenter自体のことも少し知れました。普段Webサイトを見ているときに文字上をダブルクリックするといい感じに選びたい単語単位で選択してくれることがありますが、これは中でIntl.Segmenterが仕事してくれています。(Chromeの話なので、Safariだと挙動自体もちょっと違った記憶があります。)また、ChromeのIntl.Segmenterの内部もベースにはICUを使っているようです。
話を戻すと、Polyfillにかじりついてもっと掘り下げてもよかったのですがちょっとやってみたいことを試す口実が得られたように感じたので次に進みました。

Wasm 始めてやめました

試してみたかったこととはまさにwasmです。ICUのビルドを自分で1からやるでもよかったのですがビルドオプションを把握するだけでも結構大変そうだったので最初から日本語ターゲットのもう少し扱いやすいライブラリを対象にしようと思いました。もともとRustにも興味があったのでこれはいいタイミングだと思い、調べてヒットしたlinderaを使うことにします。
wasmのベースはMDNのチュートリアルを参考に進めました。シンプルなコードを実際に動かすだけならとっても簡単。linderaもシンプルで直感的なAPIなので文字列を受け取って分割して返す、とやること自体は難しくないのでRustほとんど書いたことなくてもサクサクできました。
完成してnpmのパッケージにするまでは良かったのですが、当たり前のことで辞書データ込みなのでパッケージサイズがなんと73MB(!!)になりました。特にスマホだと読込みして処理終わるまでに数秒かかるので現実的ではないかということでwasmはやめました。目的もなくチュートリアルやるよりちゃんと取り組めるのでこうして開発の途中で試せたことはラッキーだったなと思います。

Cloud Run で Hello, warp!

この段階でブラウザで完結した処理は諦めたので、このRustの資産を使ってサーバーサイドのエンドポイントを作ります。RustだとCloud Runなら動かせるので以下のようなシンプルなDockerFileを追加します。コメントしたところ以外はknativeのサンプルそのまんまです。

# バージョンをローカルと合わせる
FROM rust:1.53.0

WORKDIR /usr/src/app
COPY . .

RUN cargo build

ENV PORT 8080

# 自分の環境でのビルド成果物のpathに変更
CMD ["target/debug/reciponrust"]

それからHTTPサーバーとしての機能を追加します。ライブラリとしてはwarpを使いました。調べたところ少し前まで?はhyperを使えばOKという感じだったようですが、hyper自体が低レイヤを扱う非常に複雑な内容に成長したため、基本的なアプリケーションの機能ならwarpを使った方が良いということのようです。
さすがにこの段階になるとメソッドを切り出したりするようになり、これでも内容として難しいことは全然していないはずですが、ほぼ知識がなかったのでちゃんとコンパイルできるようになるまで多少時間がかかりました。(特に所有権とライフタイム!)前にRust本を頭からちょっとだけ読んでいたのですが、それと比べてやはり実際に手を動かして詰まったところベースで読む方がきちんと腑に落ちるし楽しくて自分には合っていると感じます。せっかくなので次はもうちょっとボリュームのあるものを書いてみたい。
できた感じがしたらCloud Runでちゃんと動くかローカルで確認するためにDockerでローカル起動してみます。これでlocalhost:4000でアクセスできたら大方デプロイしたときにちゃんと起動することが期待できます。

docker build -t reciponrust .
docker run -d -p 4000:8080 reciponrust

1つ意外なところで詰まったのが、通常Cloud Runのドキュメントの手順通りにやるとデプロイ前にgcloud builds submitを使うと思います。これがタイムアウトしてしまうことです。まあよく考えたらそもそもローカルで一回イメージをビルドしているんだからわざわざ1からビルドすることないんじゃないかということで以下のようにビルドしてあるイメージにtagをつけてpushする形式に切り替えました。

docker tag reciponrust gcr.io/recipe-notes-289303/reciponrust
docker push gcr.io/recipe-notes-289303/reciponrust
gcloud run deploy recipon --image gcr.io/recipe-notes-289303/reciponrust

Please Don't Sleep

レシピを見ている最中に画面が消えてしまうのは地味にストレスです。私の場合保存しているレシピの9割はパンなのでレシピをみたい時は大体手が汚れています。触りたくありません。ということで画面が消灯しないようにしたい。ネイティブじゃなくても(ネイティブでできるかどうかをそもそも知らないけど)できるんだろうか?と思いながら調べるとこの世にはScreen Wake Lock APIなるものがあることがわかります。iOS Safariは対応していません。
基本的にこれ以外となると動画などメディアを再生するしかなさそうです。この時点で音楽をかけたりしながら使えないことがわかったのであまり労力をかける気がせず、APIとメディア再生をブラウザによって使い分けてくれるNoSleep.jsを導入しておしまいとしました。

顔見知り Figma

今回はアプリの実装に入る前にFigmaで簡単にデザインを作成しました。Figmaは直感的な操作しかわからない程度の親しみですが、それでも事前にデザインをあれこれ考える上では結構役に立ちました。実装しながら考える時と比べて格段に手戻りが減るので個人用の小さな開発とかでもおすすめです。

Discussion