Rのつく財団入り口

ITエンジニア関連の様々な話題を書いているはずのブログです。

【感想】『りあクト! Firebaseで始めるサーバーレスReact開発』: #りあクト でmBaaSへ

表紙は親密度の上がった笑いあう二人。尊い…(違)

 技術同人誌の『りあクト!』3部作と続編も読んだので、5作目を読みました。

 今回はこれまでのReact開発の知見を活かし、BaaSあるいはmBaaSの代表格Firebaseにバックエンドをお任せし、世の中に公開していく実際のサービスをサーバーレスで開発していく本となっています。今回もまたまた本文は会話形式で読みやすいです。

booth.pm

第1章 プロジェクトの作成と環境構築

 Reactをマスターして作中で無事に一人前のフロントエンジニアに成長できた秋谷さん。しかし仕事で回ってくるのは既存プロダクトのフロントエンドのモダン化だったりサーバーサイドは別エンジニアにお伺いを立てねばならなかったりで、理想通りには行ってない模様。(このへん、自社サービス中心の会社さんだと実際どうなのでしょうね)

 そこへ、エライ人をノせるのが上手い柴埼先輩が持ってきたのがサービスとして当たれば大きい試験的プロジェクト。メンバーはこの2人で自由、バックエンドはBaaSの代表格Google Firebaseに全部お任せ。サービス名は Mangarel「マンガレル」。→よし、やろう! という事で物語が始まります。
 柴埼先輩は漫画好きという属性がここで明かされています。だから「~つらくないReact」開発3部作でコード内にあれだけネタが仕込んであった訳ですね……

f:id:iwasiman:20210618141322p:plain
りあクト! Firebaseで始めるサーバーレスReact開発

 1-1. 基本環境を作る

  • Google Cloud FunctionはNode.jsの12系に対応していないので10系も使うため、anyenv, nodenvを使って複数バージョンに対応。(Windowsならnvm-windowsでしょうか。)
  • Create React AppでTypeScriptのプロジェクトを新規生成。tsconfig.jsonはパイセン式の設定を追加。functions/というディレクトリも除外設定に追加。
  • ESLintPrettierでコードフォーマットを統一。
  • typesyncというライブラリを追加、> typesnyc で自動で必要なTypeScriptの型ファイルをpackage.jsondevDependenciesに追加してくれる。(これ便利そうですね)
  • 設定ファイル.eslintrc.js等々を準備。柴埼先輩スペシャル版が用意済み。
  • gitコミット時にコマンドを走らせられるライブラリ husky、コミット前にESLintを実行できる lint-staged をインストール、コマンドから実行できるようにpackage.jsonに設定追加。

 1-2. Firebase プロジェクトの作成と初期化

  • ブラウザからFirebaseの管理コンソールにログイン。この時URL末尾の /u/{0からの数字}/ がログインした順番になっているので、アカウントが複数ある場合は注意。(こういう話もありがたいです)
  • プロジェクト名を決める時にFirebase上で一意のプロジェクトIDも一緒に決まるので注意。誰かと被っていると末尾に乱数のポストフィックスがついてしまう。
  • リソースロケーション(≒AWSのリージョン)の設定は一度きりなので注意。東京ならasia-northeast1
  • Create React Appで作成済みのローカルのプロジェクトフォルダに行ってnpmから firebase-tools をインストール。これがFirebase CLIでログイン後にいろいろできる。
  • > firebase init で対話形式でいろいろ選んでいくとプロジェクトが初期化。Reactの初期画面はもう表示可能。

 1-3. Cloud Functions の環境整備

  • プロジェクトフォルダの functions/ の中がCloud Functionsの仕事場。サポートしているNode.jsのバージョンは最新が8だが10もベータ版サポート。ほぼ動くとのこと。
  • tsconfig.jsonを変えたりテストフレームワークJestを入れたりLintPrettierを入れたり.eslintrc.js を整備したり。このへんも柴埼先輩スペシャル版が用意済み。
  • functions/のように作ったフォルダは上の階層とは設定が別々になるのに注意。

 1-4. 独自ドメインを設定する

  • Firebaseコンソールから設定できる。
  • Firebaseプロジェクトの実体はGoogle Cloud Platformのプロジェクトとして存在しているので、GCPのコンソール画面からもAPIキーが再作成できたりする。

 さらっと流されていますが、柴埼先輩スペシャルの準備済み設定ファイル群はここまで揃うのにずいぶん苦労したのではないかなあと……
 Firebaseコンソールの管理画面はもう日本語されていて分かりやすいです。作中で秋谷さんが言っているように用語がけっこう独特ですね。

firebase.google.com

第2章 Seed データ投入スクリプトを作る

 2-1. データベースの作成と Admin 環境の整備

  • モデル(≒RDBのテーブル、Firebase上の実態はコレクション)は本のbooks、著者のauthors、出版社のpublishersの3つということで進んでいきます。
  • データベースの新規作成はFirebaseコンソール画面から。Realtime Databaseは古くCloud Firestoreが今。
  • 秘密鍵が生成できてローカルのfunctions/配下に置くので、これはGitHubなどに置かないように注意。

 2-2. データ投入スクリプトの作成

  • Firebaseでのデータ自動投入の仕組みはありそうで実はないということで、Node.js界隈でコマンドを作る際のライブラリの代表格、Commander.jsを使って自作していきます。

qiita.com www.npmjs.com

  • ここから呼ばれるコマンドの実体、dbseed.tsの中ではCSVファイルをパースするライブラリcsv-parseも利用。秘密鍵を使ってFirestoreに接続、CSVの中身をパース、1件ごと登録……というのをNode.js 10から使える新しい構文を活用したサンプルコードになっています。

qiita.com www.npmjs.com

  • FirestoreではRDBのテーブル→コレクション。RDBの1レコード→1ドキュメント。こういう用語回りは本当にややこしいですね……。(AWSDynamoDBだと、RDBのテーブル→テーブル、1レコード→1アイテム)
  • Firestoreは統計処理周りが弱くてレコード数のカウントが全件検索でないとできない、同一ドキュメントの書き換えは1秒1回までなど制限あり。

 2-3. npm scripts として登録する

  • VSCodeのコマンドパレット、Command(Ctrl) + shift + b で立ち上がるビルドを使ってウォッチタスクで実行する方法。
  • DBへの登録状況はFirebaseコンソールからも確認できるということで、ドキュメント全件削除の命令がコンソール画面上で間違って実行しやすいところにあるという怖い話も載っています。
  • 一括削除はCLIからやるのが普通ということで、オプションを付けると子供のコレクションも再帰的に削除可能。

第3章 Cloud Functions でバックエンド処理

 3-1. 簡単な HTTP 関数を作ってみる

  • Google Cloud FunctionAWSLambda相当で後を追ってリリースされたGCPのサービス。その後、Firebaseの機能のひとつとして改めてリリースされたのがCloud Functions for Firebaseで、よりFirebaseの機能と統合・最適化されている。
  • Cloud Functionsの関数の本体はsrc/index.tsなどに書いたJS(TS)コード。firebase-functions, firebase-adminをimportして、初期化→リージョン指定→コレクションのこれを取得します→レスポンスに設定 を書く。
  • 秋谷さんが驚嘆しているようにコードはけっこう短いです。Lambda関数と似た感じですね。
  • デプロイはかなり時間が掛かるので注意。
  • > firebase serve を使うとローカル上でコマンド実行でURLが出力されるので、そこをブラウザで見てもローカルホスト上で結果が確認できる。

 3-2. クローラーをスケジュール設定関数にする

  • Amazon楽天APIを定期的に叩いて新刊を見ていきます。このスクレイピング用途では、『りあクト!TypeScriptで極める現場のReact開発』でも登場したPuppeteerが一番人気。プロジェクトフォルダのfunctions/の下でインストール、index.tsの中で定期実行するコードを書いていきます。
  • スケジュール指定のコードがcrontabと同じなのが面白い。タイムゾーンAsia/Tokyoを指定することに注意。
  • オプション指定がけっこう多いのと、起動時のメモリ指定もコードの中で指定します。デフォルト256MBというのもLambda関数と同じで、Puppeteerが動くには1-2GBはいるとのこと。
  • PuppeteerAPIもいろいろあるそうですが、本書のサンプルにある基本的な関数を使えばDOM要素の取得には足りるとのことです。
  • また、クローリングした後でFirebaseに登録する前にFirebase上の対象コレクションを全検索、ない場合にのみ登録という冪等性の担保を行っています。
  • スケジュール設定した関数を手元で動かすときはFunctions Shellというコマンドから行えるツールあり。

 クラウドとサーバーレス全般で「冪等性」はよく登場しますが、Firebaseでも同じく重要なのだなと。
 スクレイピングといえば言語的にはPythonなイメージがあったのですが、JS界隈だとPuppeteerがデフォルトなんですね。
 細かいネタとしては秋谷さんは『炎炎ノ消防隊』が好きなのが判明します!

fireforce-anime.jp

 3-3. スケジュール設定関数をデプロイする

  • けっこうなコード量になったindex.tsをデプロイしていきます。
  • 前述のFunctions Shellから関数を実行するとまずインデックスがないと怒られる→
    示されたURLに行くとコンソール画面に繋がってそこでインデックスが作成→
    無料枠だとGoogle以外のネットワークに繋がらない制限あり(!)なのでFirebaseを有料プランに変更→
    > firebase deploy のコマンド実行でデプロイ。
  • 知らないでやったら躓きそうなところがけっこうあります……

 3-4. Cloud Functions 中上級編 Tips

  • > firebase functions:config:set {キー}={"値"}環境変数を指定。
  • > firebase functions:config:get.runtimeconfig.json と指定すると環境変数をファイルに落とせる。firebaseのコマンド群はこのファイルを見てくれる。関数内からも参照可能。変数名は大文字不可。
  • 本番実行時に process.env の中を見るとNODE_ENV: 'production'が入っているが、ローカルサーバーだとundefinedになってしまう。
  • スケジュール設定のコード内でタイムゾーンを指定しても本番サーバー内のタイムゾーンは変わらない。柴埼先輩テクだとdate-fns-timezoneライブラリを使う手がある。
  • 作りたいサービスのReactアプリ本体とfunctions/配下のCloud Functionsで関数を共通化して使いたい場合は、柴埼先輩がいろいろ試してたどり着いた邪道テクではシンボリックリンクを張る手法で解決。Reactアプリ本体側のコードが本体、functions/配下はそこへリンクを張る。
  • Cloud Functionsは最初の起動時にindexにある関数をすべてロードしてしまうので、index.tsが巨大化していくと遅くなる。分割しておくとよい。また起動時にprocess.env.FUNCTIONS_TARGET という変数に関数名が入るので、その対象の関数だけexportsするというテクがある。

 大まかにはAWS Lambdaでサーバーレス開発をしていく時と似たような雰囲気ですね。
準備された設定ファイルや細かなTipsなどなど、柴埼先輩のこれまでの試行錯誤(と、その裏にある本書の作者さんの苦労)が入っているので本としてはスラスラ進んでいます。でもFirebaseは進化中のサービスであるだけにけっこう細かいところにまだまだ躓きポイントや罠があるんだなあという感じです。

firebase.google.com

第4章 Firestore を本気で使いこなす

 4-1. Firestore と RDB の違いと各種制限について

  • RDBのテーブルと似ているがコレクションは基本的にスキーマレスなので好きなフィールドを定義できてしまう。柴崎先輩はTypeScriptの組み込み変数Partial型を使ったテクを使用。
  • ドキュメントID以外のフィールド値はユニーク制限がない。RDBのレコード同士が外部キーで結合しているようなパターンは、データを非正規化して片方のドキュメント(≒レコード)に持たせる。更新が掛かったら後から定期的にCloud Functionで更新するなど。
  • count()ができない弱点は…作中の2人の予想通り、後からBigQueryへのエクスポートや連携ができるようになった。

firebase.google.com support.google.com

  • データのUPDATE,DELETEは1件づつ。batchは500件まで。
  • RDBはカラムを指定してデータの一部を取得できるが、Firestoreはドキュメント全体しか取得できない。
  • 他にもドキュメントの最大容量やフィールド数、書き換えは1秒に1回まで...など制限いろいろ。

 4-2. Firestore のクエリーとインデックス

  • クエリーは等価評価と範囲比較の2種があり、1つのクエリーでは範囲比較はひとつのフィールドでしか行えない。(!)
  • OR検索や != もなし。検索用フラグを持たせたりしてうまくやる必要がある。
  • なおOR検索に相当する inarray-contains-any が後から提供された。

nabettu.hatenablog.com

  • RDBのように create index しなくても元から全フィールドにインデックスがある。昇順と降順が別。配列用はまた別インデックス。範囲の比較と等価の比較を同時に行うときはまた別インデックス。
  • コレクションの親子関係をまたがって検索するときはコレクショングループインデックスというのがある。
  • コレクション定義の外側で別にインデックスが定義されているらしく、コレクションを削除してもインデックスは消えない。(RDBのテーブルだとdrop tableで一緒に消える)

 4-3. Firestore の配列の取り扱い

  • 配列検索の array-contains, 配列要素追加の arrayUnion(), 要素削除の arrayRemove() がコード中から書ける。
  • array-contains はひとつのクエリーで1回しか使えない。booksコレクションの中でauthorIdsの配列を見てauthorIdがA先生、は検索できるがA先生かB先生両方、は検索できない。(!)
  • ここで柴崎先輩が編み出したのが昔の公式推奨だった「Trueマップ」方式。配列でなくMapで
    authorMap: {'A先生のID': true, 'B先生のID': true}
    のように定義すると、検索句で==, trueを2回書けば実現できる。

 4-4. Firestore で日時を扱う際の注意

  • JS/TSのDate型でデータを突っ込むと、Firebase独自のfirebase.firestore.Timestamp型に変換されて保持、その後検索時もそのまま。TypeScriptを使うときはモデル定義で最初からこのTimestamp型で定義しておいたほうがよい。
  • Firebase側のFieldValue.serverTimestamp()の返す型がTimestamp型でなかったり、そのままでは値の比較ができずDate型やミリ秒に変換する必要がある、などの罠もある。

 4-5. Firestore のデータモデリング

  • 歴史のあるRDBのように標準的な方法はまだ定まっていない。
  • コレクションは階層化の親子関係がとれるが、むやみに使わない。
  • IDはユニークに定義できるものがあればなるべく使い、ない場合のみFirestoreが自動生成するランダムなIDを使う。こちらは人間の目にわかりにくいため。
  • 公開するセキュリティレベルが別だったらそれぞれ別のドキュメントにする。
  • 重複を恐れずに非正規化する。
  • 検索用のフラグなどを事前に持たせ、定期実行のCloud Functionで更新するなどして検索を工夫する。
  • 1:1のリレーションには {コレクション名}/{IDの値} で表される「疑似リレーション」を両方のコレクションに持たせるのがよい。
  • 1:Nのリレーションにはふつうに外部IDをフィールドに持たせる。
  • N:Nのリレーションでは前述のTrueマップを使う方法が健在。RDBならID同士の関係を持たせる別途のテーブルを用意するところだが、FirestoreだとJOINができない。

自分はRDBはよく使ってきたのでこういう話題になるとテーブル構造とかSQLが頭に浮かんでくるのですが、Firestoreだとけっこう独特で制限が多いですね…! 作中で秋谷さんが言っているように、気を付けないとハマるポイントがあちこちにありそうです。

 4-6. Firestore だけで全文検索を実現する

  • 全文検索の公式推奨はAlgoliaのような外部の検索サービスを使うこと。しかし高く連携設定が面倒。他にはElasticsearchを使う事例も。
  • 方針として外部サービスを使わないでおきたい柴埼先輩は、なんと全文検索を自前で実装してしまった。

Algoria

天才だ…天才がおる…!ということでこの4章の目玉はこの自前の全文検索エンジン実装です。簡単に言うと、

-単語の形態素解析にもいろいろあるがその中の一つ、Javaで開発されたOSSの日本語形態素解析エンジン Kuromoji がある。これのJavaScript版のkuromoji.jsがnpmで公開されているのでこれを使う手もあるが、辞書ファイルが巨大になるデメリットあり。

www.atilika.com

  • 形態素解析より簡単な、隣り合う文字に分解するN-gram(エヌグラム)の方法を柴埼先輩は採用。漫画の名前が「七つの大罪」だったら、JSのライブラリn-gramを活用してドキュメントの登録時に名前を"七つ""つの""の大""大罪"のように分解して、1ドキュメントごとにTrueマップで格納するようにする。
  • 検索条件で「七つの大罪」が渡ってきたらこれもN-gram分解して配列に変換、先ほどのTrueマップと等価比較すると実質全文検索になる。
  • ソートはできないのでReact側のクライアントソートで対処。

コード例も一緒に載っているのですが、よくこんなアイデア思いつくな……!と感嘆しました。作中の柴埼先輩も成功したとき自分で感動したと言っていますが、こういういろんな工夫を駆使して開発していくのですね。

ja.wikipedia.org

第5章 React でフロントエンドを構築する

 5-1. フロントエンド環境の整備

  • 今回は状態管理のReduxは使わず、react-routerCSSEmotion回りなどの最小構成。
  • Firebaseとの接続に必要なAPIキー、アプリIDの値は管理コンソールから取得、プロジェクトルート直下の設定ファイル .env の中の REACT_APP_* の項目に記述。

 5-2. Context でグローバルな State を持つ

  • src/index.tsx <FirebaseApp>でくるんだ中に<ThemeContext>、その中に<App>CSS的なテーマなどのコンテキストは別ファイルで定義、React HooksのcreateContext()で定義。
  • src/FirebaseApp.tsx firebase.firestore()して取得したFirestoreオブジェクトを持ち、こちらもコンテキストで使いまわす。

 5-3. Hooks で副作用処理を行う

  • 発売カレンダーはコンポーネントにしているが、下のuseBooks()を呼ぶだけ。
  • Custom HookのuseBooks()を定義。戻り値はオブジェクトにして本の配列、ローディング中か、エラーを持つ。
    • useState()を活用して本のリストなどは保持。
    • useEffect()の副作用を活用して初期表示時にFirestoreと接続、発行日が未来日のもうすぐ発売の本一覧を検索。
  • もうひとつCustom HookのuseBookSearch()を定義。
    • こちらもFirestoreと接続して検索条件をもとに検索する。useEffect()の第2引数に検索条件のテキストボックスの値を入れているので、1文字でも変わった瞬間ごとにクエリーが走ってくれる。

 Reduxを使わない選択をした話で、柴埼先輩がHooks登場でReactが完全にいらない子になったのではなくて、「使わない選択を正しくできるようになった」のではないかと述べているのが興味深いです。習得も難しくコード量も増えるReduxで問題を解決しようとすると、解決できるけどオーバーキルになることが多かったとのこと。
 確かに本章のReact Hooksの副作用を活用したコード、ぱっと見でもきれいに分割されてスッキリしている印象です。今後はこちらの路線が増えていくのかなあと思います。

第6章 Firebase Authentication によるユーザー認証

 6-1. 認証機能を導入するための準備

  • 説明を始めるんだけどもう柴埼先輩が先に作っちゃったといういつものムーブに、秋谷さんが慣れてきてだんだん投げやりになっていますw
  • たくさんのSNSとやたら連携するのではなく、漫画文化と親和性が高いTwitterに限ったというのがサービス開発の見地に立っていて興味深いです。
  • Twitter DeveloperのサイトにアクセスしてAPI KeyAPI secret keyの2つを取得、Firebaseコンソール側に反映する手順がスクショ入りで詳しく解説されています。

 6-2. ログイン機能の実装

  • createContext()するファイルの中で、Firestoreオブジェクトのほかに認証情報、ユーザ情報も追加。
  • JSXで一番外側でくるんでいるsrc/FirebaseApp.tsx にログイン機能大幅追加。認証情報が変わったら書き換える処理。このコンポーネント内ではuseState()を使っているので中身が保持、さらに外側ではContextを使っているので、子供のコンポーネント群からもこのユーザ情報が参照できる。
  • ログイン画面も別のコンポーネントとして実装。react-firebaseuiというライブラリを使うと、JSXに所定のタグを書くだけでログインボタンの表示をやってくれる。

この整ったコードにたどり着くのに柴埼先輩はかなりハマったそうなので、その裏の作者さんもかなり試行錯誤されたのでは…と思います。

 6-3. Firestore にセキュリティルールを適用する

  • このままではAPIキーなどの値を抜き取るとMangarelサービス用のFirestoreに悪意ある第三者もアクセスできてしまうのでセキュリティルールを適用する話。
  • ブラウザでFirebaseコンソール上の設定画面から、プログラム言語のコードっぽい構文でさまざまに設定できる。
    • match /{コレクション名}/{ドキュメントのID項目} の形でパス指定、1ドキュメント取得/ドキュメントのリスト取得/作成/更新/削除 を定義できる。
    • if文が書けるので、usersコレクションのidが自分のものと等しい1ドキュメントしか更新不可、などが細かく定義できる。
    • 作成時にユーザ名null不可、などのバリデーションルールもコードからかなり細かく設定可能。ドキュメント数、1ドキュメントの中のフィールド数や正規表現などなども使える。
    • コンソールにもシュミレーターがある。

 もうほとんどバリデーション処理をクライアントサイド/サーバサイドの何らかの言語でコードで書いているのと同じような感覚ですが、Firebaseはバックエンド不要のBaaSなのでこういうことも設定画面からできるわけですね。突き詰めるとかなり細かくいろんなことができそうです。

firebase.google.com

まとめ:Firebaseを使ったサーバーレス開発がわかる本

 今回も内容はかなり濃く密度の高い本でした。設定ファイルのサンプルやコード例、問題解決の手法など作中では柴埼先輩が過去ハマったことをチラ見せしつつ割とさらっと正解を示していますが、その裏では作者さんはかなり試行錯誤されて調べながら実際のサービスまで完成させたのでは思います……

 技術同人誌などでもFirebaseはキーワードとしてよく登場しますし、本書のあとがきでも「フロントエンドエンジニアをエンパワーする技術」だという一節があります。個人開発ベースやスモールスタートな事業での開発、バックエンドのエライい人やインフラを気にせず少人数でスピード優先で作っていくような開発に向いていそうですね。
 一方Firebase自体については、他の書籍類でも見てきたのですがけっこうクセが強い技術だなと改めて思いました。これのよりRDBライクな版が出たら丸く収まりそうですが、そういう訳にもいかないんでしょうね……笑

 今回も100ページちょっとですしお値段もお手軽、気軽に読める技術同人誌です。読んでみるとReact+Firebaseのサーバーレス開発のイメージが湧くのではないでしょうか。
 なおネタバレですが最後の著者紹介のところに、今回はデスクツアー的に作者さんの執筆環境の机の写真が載っています。あっ確かにモニターの色合いが目に優しいSolarized Lightっぽい。そしてキーボードが2つに割れててトラックボールも左右に2つある! これ一体どうやって操作しとるんぢゃ……

f:id:iwasiman:20210618141322p:plain
りあクト! Firebaseで始めるサーバーレスReact開発

関係書籍など

本書のサンプルコードのリポジトリ

github.com

正誤表。 github.com

ご感想はTwitterハッシュタグ #りあクトへ。

りあクト! TypeScriptで始めるつらくないReact開発 第3.1版【Ⅰ. 言語・環境編】 oukayuka.booth.pm りあクト! TypeScriptで始めるつらくないReact開発 第3.1版【Ⅱ. React基礎編】 oukayuka.booth.pm りあクト! TypeScriptで始めるつらくないReact開発 第3.1版【Ⅲ. React応用編】 oukayuka.booth.pm

りあクト! TypeScriptで極める現場のReact開発 booth.pm りあクト! Firebaseで始めるサーバーレスReact開発 booth.pm