GREE DX Tech Blog

グリー株式会社のDX事業本部のエンジニアブログ

SvelteKit + Firebaseでリアルタイムクイズ大会開催システムを作った話【後編② Firebase】

「感謝祭クイズ!」についておさらい

DX事業本部の小松です。

以前投稿した「感謝祭クイズ!」アプリ【前編】の記事では、技術構成に関する簡単な説明と、どんなアプリを作ったのかについて紹介しました。

おさらいすると、「感謝祭クイズ!」はリアルタイムクイズを開催するためのアプリです。管理画面でクイズを作り、進行画面でクイズ大会を進め、参加者画面でクイズに解答することができます。

管理画面でクイズを作成

進行画面でクイズ大会を進め、参加者画面が自動で切り替わっていく

参加者画面でクイズに解答する

そんな「感謝祭クイズ!」は、主にSvelteKitとFirebaseを用いて以下のような構成で開発されています。

「感謝祭クイズ!」構成

本稿では【後編】として、「感謝祭クイズ!」で開発上使用した技術の詳細や、役に立つと思ったこと、工夫した点などについて説明していきます。
文量が多くなってしまったので、さらに後編を【後編① フロントエンド】【後編② Firebase】に2分割してお届けします。  

本稿は【後編② Firebase】になります。 「感謝祭クイズ!」のバックエンドとして選定したFirebaseの紹介と、その大きな特徴である2つのリアルタイムデータベースの違い、ストレージ機能を使った画像の取り扱い、そしてリアルタイムデータベースとのやりとりで簡単に型情報を付加できる自作ライブラリ「firesnake」について説明していきます。

興味がある方は、【後編① フロントエンド】も是非ご覧ください!

Firebase

感謝祭クイズでは、バックエンドにFirebaseフル活用しました。

Firebaseはリアルタイムデータベースをはじめ、認証、ストレージ、機械学習と言った様々なバックエンドに必要な機能を提供しているサービスで、「mBaaS (mobile Backend as a Service)」というものに分類されます。

感謝祭クイズで利用したFirebaseのサービス

以下、感謝祭クイズでFirebaseを利用する中で参考になりそうと思ったことをご紹介します。

Cloud FirestoreとRealtime Databaseの違い

感謝祭クイズを作る上で不可欠だったのが、これらのリアルタイムデータベースです。
これら2つはよく似ていますが、どのような点で異なるのでしょうか?
現時点での主な違いを以下にまとめてみました。

Cloud FirestoreとRealtime Databaseの主な違い

大きな違いは、データ構造がコレクションとドキュメントの繰り返しによるものか、純粋なJSONに相当するものか、という点です。
スペックや機能ではFirestoreに軍配が上がるため、どちらを使うか迷った場合は、まずFirestoreから検討してみるのが良いと思います。

ちなみに感謝祭クイズでは、クイズの管理画面にFirestore、参加者画面にRealtime Databaseを使っています。
これは、開発開始前に2つのデータベースのリアルタイム更新が反映されるまでの速度を比べてみたところ、Realtime Databaseの方が体感で分かるほどに速く、リアルタイム性がより強く求められる参加者画面にはRealtime Databaseを使おうと考えたからです。しかしその後、Firestoreのロケーション設定がasia-northeast1(東京)でなかったことが分かり、設定を直したらどちらもほぼ変わらないほどの速度になりました。
ですが、ドキュメントによるとRealtime Databaseの特徴としてレイテンシが非常に低いことが挙げられているので、今回のクイズアプリのようにリアルタイム性が求められる場合は、FirestoreよりもRealtime Databaseが向いているといえるかもしれません。

より詳細な使い分けについては、Googleの公式ドキュメントを参照して下さい。

Firebase Cloud Storageにおける画像の取り扱い

クイズなどに使う画像は、Firebase Cloud Storageへアップロードし、クライアントからアクセスできるようにしました。

しかし、画像のURLが「/question1/answer」のような予測しやすいパスだけでアクセスできてしまうようなものでは、参加者に先に見られてしまいます。

Cloud StorageのファイルへアクセスするURLには固有のトークンがついているため、そのような事態を防げます。このURLは、Cloud StorageのSDKに用意されているgetDownloadURLという関数で取得することができます。このURLのトークンはずっと変わらないため、丸ごとFirestoreなどに保存して利用することができます。
一度クイズを出題してしまえば画像のURLは分かってしまいますが、少なくとも出題前に画像を見ることはこれで難しくなると思います。

Cloud Storageのファイルを取得するURLについては、こちらのブログ記事が参考になります。

Firebase Local Emulator Suite

本番用のFirebaseを開発に使っていると、複数人で開発するときや、開発用にFirebaseとやりとりするデータに課金されたくないという時に不便です。

そこで、Firebaseの全てのサービスと同等のものをローカル環境で動かすことができるエミュレータが用意されています。

導入も簡単にできます。

Firebase CLIをインストールしたら、次のコマンドでエミュレータをインストールします。

firebase init emulators

次に、利用したいサービスのエミュレータを上下キーとスペースで選びます。

利用したいサービスのエミュレータを選択

各エミュレータをローカルで立ち上げる時のポート番号の設定と、エミュレータを管理するUIを有効化するかの選択を行い、エミュレータをダウンロードします。

ポート番号・UI有効化の設定

あとは、ローカルで実行している時に各種Firebaseサービスのインスタンスを「connect〇〇Emulator」へ渡します。
ローカルで実行しているかは、アクセスしているページのポートが3000番であるかどうかで判定しています。

import { initializeApp } from "firebase/app"
import { getAuth, connectAuthEmulator } from "firebase/auth"
import { getDatabase, connectDatabaseEmulator } from "firebase/database"
import { getFirestore, connectFirestoreEmulator } from "firebase/firestore"
import { getFunctions, connectFunctionsEmulator } from "firebase/functions"
import { getStorage, connectStorageEmulator } from "firebase/storage"

const app = initializeApp({
  apiKey: "...",
  authDomain: "...",
  databaseURL: "...",
  projectId: "...",
  storageBucket: "...",
  messagingSenderId: "...",
  appId: "..."
})

export const auth = getAuth(app)
export const firestore = getFirestore(app)
export const database = getDatabase(app)
export const storage = getStorage(app)

const isEmulating = typeof window === "undefined" || window.location.port === "3000"
if (isEmulating) {
  connectAuthEmulator(auth, "http://localhost:9099")
  connectDatabaseEmulator(database, "localhost", 9000)
  connectFirestoreEmulator(firestore, "localhost", 9001)
  connectFunctionsEmulator(getFunctions(app), "localhost", 5001)
  connectStorageEmulator(storage, "localhost", 9199)
}

Firestoreなどに保存されるデータは、ローカルに保存しておくことができます。
エミュレータ用のデータを入れておくディレクトリを作成し、エミュレータ起動時にそこからデータをインポートするようにオプションを指定します。また、エミュレータを終了するときにエクスポートします。

さらに、エミュレータの起動とsvelte-kitのローカル起動を連動させることができます。

mkdir emulator_data

# このコマンドは、package.jsonのscriptsに登録しておくと便利です。
firebase emulators:exec 'svelte-kit dev' --ui --import=./emulator_data --export-on-exit

エミュレータが有効になっている時は、画面の下にエミュレータモードで実行されていることが分かるようになっています。

エミュレータモードでの実行

また、エミュレータのUI機能が有効になっている時には、localhost:4000 などにアクセスすると実際のFirebaseコンソールと同等の機能を持つ画面から各種サービスを操作することができます。

エミュレータのUI

エミュレータの詳細については、公式ドキュメントを参照して下さい。

自作ライブラリ「firesnake」

TypeScriptでは、型情報が適用されている変数やオブジェクトなどに間違った値を入れようとするとコンパイラがエラーとして教えてくれたり、VSCodeなどのIDEでは自動補完機能を使うことができたりと色々便利です。

しかし、Cloud Firestore および Realtime Databaseと、フロントエンドの間でデータを送受信するときには基本的に型情報を使うことができません。データを送信する時にはどんなオブジェクトでも送れてしまいますし、受信するデータは何が入っているか分かりません。

この問題に対処するため、Firestoreの方にはFirestoreDataConverter interfaceというものが提供されているのですが、ドキュメントの種類ごとに変換用のConverterを書かなければならないようで、記述量が多くなってしまいそうなことが懸念でした。

既存のライブラリなども探してみましたが、簡単に使えて、かつ大きく書き方が変わったWeb SDK Version 9に対応したものはなかなか見つからず、ここは自作ライブラリを作ろうと考えました。

初めは、Ruby on RailsのActiveRecordのような、クラスを使った多機能なO/Rマッパーのようなものを作ろうと思っていました。しかし最終的には、カスタマイズした型をルートオブジェクトに渡して、メソッドチェーンで各種ドキュメントにアクセスする形態に落ち着きました。

このライブラリの使い方について、簡単に紹介します。

まずは、database.ts といった名前のファイルにカスタマイズした型をスキーマとして記述していきます。

ここでは例として、Userドキュメントのコレクションが、サブコレクションとしてArticleドキュメントのコレクションを持っているという場合を考えてみます。

firesnake-firestoreのスキーマの例

dataにはドキュメントの持つ属性、subにはサブコレクションを書いていきます。
Firestoreのルート型であるDatabaseにはUserのコレクション型、UserにはArticleのコレクション型を持たせることで、Firestoreのスキーマをシンプルに記述できています。
Database型とfirestoreインスタンスをFirestoreDatabaseクラスに渡してインスタンス化し、exportすることで、便利なfsdb(FireStoreDataBase)オブジェクトの完成です。

fsdbオブジェクトは、「_」メソッドでコレクションとドキュメントを交互に掘り進んでいき、どの階層でも型情報の恩恵を受けることができます。今回の例では、「users」コレクションの「0123abcd」というIDを持つドキュメントには「article」というサブコレクションがある、というところまで型の補完が効いていることが確認できます。

firesnake-firestoreで型補完

このように、「_」メソッドを使ったメソッドチェーンでデータ構造を掘り進んでいく様子がスネークケースに見えることから、この自作ライブラリの名前を「firesnake」としました。

firesnakeでは、以下の使用例のようにデータの追加・取得といったあらゆる操作に対して、database.tsで作成した型を反映することができます。

firesnake-firestoreでデータの追加・取得

また、Web SDK Version 9では、メソッドチェーンで追加・取得といった機能にアクセスしていたWeb SDK Version 8とは違って、機能ごとに関数をインポートする書き方になっています。これによってアプリケーションに必要な機能に絞ってビルドに加えることで、バンドルサイズを小さくすることができるのが利点であるようです。

しかし、個人的には今までのようにメソッドチェーンで各種機能にアクセスできた方が、import文を少なくできるので好きな書き方でした。
firesnakeではWeb SDK Version 9以降を内部的に利用していますが、使用する側はWeb SDK Version 8のようにメソッドチェーンを使って各種機能にアクセスできるので、コードの記述量を減らすことができるというのもメリットの一つです。

実は「感謝祭クイズ!」開発期間の半分はこの「firesnake」を作るのに費やしていました。
それだけの価値は十分にあったと私は感じていて、コーディングのミスを大きく減らし、記述量も少なくすることができ、アイデアの実装に集中することができました。

firesnakeは、Cloud Firestore用のものとRealtime Database用のものをそれぞれnpmパッケージとして公開していますので、もし興味のある方は是非使ってみてくださると嬉しいです!

# Cloud Firestore用
npm install -D firesnake-firestore

# Realtime Database用
npm install -D firesnake-database

(現在はバージョン0.0.1で機能が限定的ですが、SDKのより多くの機能を使えるようにしたり、テストを追加したりしてバージョン1.0.0として公開したいです!)

おわりに

「感謝祭クイズ!」では、「Firebase」そして自作ライブラリの「firesnake」といった、データの永続化とリアルタイム変更検知を簡単に実現できるツールを使うことで、バックエンドに関する開発負荷をかなり小さくすることができました。

SvelteKitを用いたフロントエンド側の技術について興味がある方は、【後編① フロントエンド】の記事も是非ご覧ください。

今後も面白いアプリやサービスを作りながら、触れたことがなかった技術にキャッチアップしていけたら楽しいなと思います。

GREEのDX事業本部では、このように「身近な人をハッピーに」する、新しく魅力的なプロジェクトに挑戦していく仲間を募集しています。

今後とも、よろしくお願いいたします。