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

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

SvelteKit + Firebaseでリアルタイムクイズ大会開催システムを作った話【後編① フロントエンド】

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

DX事業本部の小松です。

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

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

管理画面でクイズを作成

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

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

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

「感謝祭クイズ!」構成

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

本稿は【後編① フロントエンド】になります。
「感謝祭クイズ!」開発の基盤として使用した Svelte / SvelteKit について紹介し、加えてフロントエンドでクイズの進行に合わせて自動で効果音やバイブレーションを鳴らす機能についても簡単にご説明します。

興味がある方は、【後編② Firebase】も是非ご覧ください!

Svelte

Svelteは、ReactVue.jsAngularに続く比較的新しいWebフロントエンドフレームワークです。
パフォーマンスの良さや、短く宣言的なコード記述が可能な点が人気で、State of JS 2022において人気ライブラリチャートのフロントエンドフレームワーク部門でSをランク獲得しました。

以下、Svelteの主な特徴を紹介します。

Svelte独自の記法により、記述量が少なくて読みやすいコードを書ける

Svelteはフロントエンドフレームワークの中ではVue.jsに近く、単一ファイルコンポーネントというHTML、CSS、JavaScript/TypeScriptを1つのファイルにまとめて書く方法が基本です。
Svelteのコードで特徴的なのは、フレームワークを使うためのライブラリをインポートしたり、専用の関数やクラスを用いた「おまじない」や「ボイラープレート」と呼ばれるものが極力少なく済むという点です。

例えば、次のようなTODOリストを作るコードを、30行程度で作ることができます。

SvelteでTodoリストを作る

参考コード: https://svelte.dev/repl/7eb8c1dd6cac414792b0edb53521ab49?version=3.20.1

HTMLを書くマークアップ部では繰り返しや条件分岐などを直感的に表現できる独自の文法を利用することができます。
また、JS/TSを書くスクリプト部では、マークアップ部で変数を参照しているところを更新するのに特別な関数を使わず、その変数に「=」で代入を行ったときに自動で反映されます。
Svelteは、このような記法によって記述量が少なく読みやすいコードを書くことができることが人気の一要素となっています。

Svelteでコード量を減らすことの重要性については、こちらの公式ブログ記事が参考になります。

Svelteはコンパイラである

なぜ、Svelteはフレームワークのためのライブラリをインポートしたり、ボイラープレートを少なくすることを実現できているのでしょうか?

それは、Svelteはフレームワークでありながら、その実体がコンパイラであるためです。
Svelte独自の記法で書かれた「.svelte」ファイルを、ブラウザ上で動作するHTML、JavaScript、CSSといったファイル群にコンパイルすることがSvelte本来の機能なのです。
そのため、コンパイルする前の「.svelte」ファイルではシンプルで可読性の高い独自の文法を使うことが可能になっています。

また、Svelteのライブラリ自体を公開するアプリケーションファイルに含める必要もないため、多くの場合はReactやVue.jsといった他のフレームワークよりもバンドルサイズが小さく済み、Webサイト訪問時の初回ロード時間を短くすることができます。

このコンパイラとしてフレームワークを作るアプローチについては、こちらの公式ブログ記事が参考になります。

SvelteはリッチなUIを手軽に作る仕組みが豊富に提供されている

Svelteには、状態管理の仕組みアニメーションを実現する関数などが豊富に用意されており、単体でも動きの多いアプリケーションを簡単に実装することができます。

Svelteでの開発をサポートするライブラリも存在します。感謝祭クイズでは、BootstrapでスタイリングされたSvelteコンポーネントを提供してくれるsveltestrapや、フォーム作成を簡単にするfelteなどを利用しました。

これらの特徴から、SvelteはUXの良いアプリケーションを開発したり、アイデアを素早く形にするのに向いており、優れた開発者体験を提供してくれるフレームワークだと感じています。

また、手を動かしながら学ぶことができるチュートリアルも充実しており、すぐに開発を始めることができます。

本稿ではSvelteのさらなる詳細には踏み込みませんが、コンパイラの仕組みなどの深い部分の解説記事を公開している方も多くなってきておりますので、興味がある方は是非調べてみてください!

SvelteKit

SvelteKit は、SvelteをベースとしたWebアプリケーション開発フレームワークです。Reactに対するNext.js、Vue.jsに対するNuxtに相当するものです。

SvelteKitでは以下のような機能によって、高度なWebアプリケーション開発をさらに効率的に行うことができます。

  • Webサイトのルーティングが、ファイル名から自動で作成されます。
  • APIからのデータ取得とその表示をクライアントに送る前に行うことができる、サーバサイドレンダリングの仕組みが提供されます。
  • Firebase HostingやVercelなどのホスティング環境に応じてビルドを行う、ビルドアダプターの仕組みが提供されます。
  • プロジェクト作成時、ESLintなどのコードフォーマットツールや、テストツールであるVitestなどを自動で追加できます。

SvelteKitは2022年12月、ついにバージョン1.0が公開されました。
これからますます利用者が増えていってくれたら嬉しいなと思います。

効果音・バイブレーション機能

2回目のクイズ大会開催時には、より参加者の皆様に盛り上がってもらうための演出として、クイズの進行に合わせて自動で効果音やバイブレーションを鳴らす機能を追加したいと考えました。例えば、運営側が正解発表をして、参加者の画面が正解発表画面に自動で切り替わった時、正解なら「ピンポン+バイブレーション」不正解なら「ブブー」を再生します。

しかし、効果音やバイブレーションはユーザーからクリックされるなどのインタラクションイベントによってのみ鳴らすことができます。そうでなければ、例えば電車に乗っているとき、あるサイトを訪れた瞬間勝手に音楽が再生されるなどしたら大変迷惑です。

そういった制約があり、クイズ大会の進行に合わせて自動で実行させるには一工夫必要でした。

基本的なアプローチは、ユーザに最初の1回だけ効果音・バイブレーション機能をONにしてもらうインタラクションイベントを発生させてもらい、あとは自動で効果音やバイブレーションを鳴らせるようにしようというものです。

効果音・バイブレーション機能を有効にするトグルスイッチ

これらのトグルスイッチがONにされた時のイベントハンドラの中で、インタラクションイベントが有効なうちに、Svelteのstore機能を使って好きなタイミングで効果音再生やバイブレーションを実行できるようにします。

効果音再生機能の実装例
(ブラウザの種類やバージョンによっては動作しない場合があります。参考までに。)

import type { Unsubscriber, Writable } from "svelte/store"
import { writable, get } from "svelte/store"

const lastPlayedSoundWritable: Writable<HTMLAudioElement | null> = writable(null)
const soundSubscriber: Writable<string | null> = writable(null)
const soundUnsubscriber: Writable<Unsubscriber | null> = writable(null)

// 音声の再生が有効である時、音声ファイルのパスを渡すと再生します
export const playSound = (audioPath: string | null) => {
  // 同じパスを入力されたらもう一度再生を実行できるように、一度nullを入れる
  soundSubscriber.set(null)
  soundSubscriber.set(audioPath)
}

// 再生中の音声を停止します
export const stopPlayingSound = () => {
  const lastPlayedSound = get(lastPlayedSoundWritable)
  if (lastPlayedSound !== null) {
    lastPlayedSound.pause()
    lastPlayedSound.currentTime = 0
  }
}

// 音声の再生を有効にします
// ユーザインタラクションで発火する関数内で実行して下さい
export const subscribeSound = () => {
  const audio = new Audio()
  const audioCtx = new AudioContext()
  audioCtx.currentTime
    
  const unsubscriber = soundSubscriber.subscribe((audioPath: string | null) => {
    if (audio.played) audio.pause()
    if (audioPath !== null && audioPath !== "") {
      audio.src = audioPath
      audio.currentTime = 0
      audio.play().catch(() => { return })
      lastPlayedSoundWritable.set(audio)
    }
  })

  soundUnsubscriber.set(unsubscriber)
}

// 音声の再生を無効にします
export const unsubscibeSound = () => {
  stopPlayingSound()
  get(soundUnsubscriber)?.()
}

心残りだったこと

最後に、「感謝祭クイズ!」を作ってきた中で改善しておきたかったと思う点を挙げます。

まず、開発環境はDockerで構築できるようにしておけばよかったと思っています。複数人で開発する時、ホストマシンのNode.jsのバージョンを揃えたり、Firebaseのエミュレータを手動でインストールするのがちょっと手間でした。

また、フロントエンドの自動テストを書いておらず、手動での動作確認が多くなってしまったのも反省点です。

そして、今回作ったアプリでは複数の管理者が別々にクイズ大会を開催することを想定しておらず、サービスとして展開できるようにするには根本的な改修が必要なものになってしまったことも心残りでした。

おわりに

「感謝祭クイズ!」では、「SvelteKit」を使うことでアイデアを素早く形にすることができました。

Firebaseを用いたバックエンド側の技術について興味がある方は、【後編② Firebase】の記事も是非ご覧ください。

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

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

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

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

クイズ番組といえば...?

DX事業本部の小松です。

突然ですが、皆さんはクイズ番組といえば何をイメージされるでしょうか?
私は「クイズ$ミリオネア」や「クイズ!ヘキサゴン」をよく見ていました。これは年代が分かってしまう質問かもしれませんね笑

一方、私の上司は「オールスター感謝祭」が好きとのことでした。
オールスター感謝祭は、大体半年に1回放送されるクイズ番組です。
クイズは選択式の通常問題に加え、解答者が参加するミニマラソンなどの結果を予想する問題が出題されます。
いくつかのクイズをまとめて「ピリオド」と呼び、繰り返されるピリオドごとに、チャンピオンが賞金を獲得します。

こんなクイズ大会を社員総会の出し物にしたら盛り上がるはず!と持ちかけられ、私も面白そうだと思ってメインの開発を担当させていただきました。

開発したアプリは、名付けて「感謝祭クイズ!

「感謝祭クイズ!」ロゴ

(ロゴを作成する力がなかったので、「5000兆円ジェネレーター」で作成しました)
…なんともそのまんま感があるアプリ名ですね。。。

ちなみに、オールスター感謝祭のルールに忠実には作っておらず、回答者が脱落してしまう「予選落ち」ルールをなくすなど、社員総会の参加者全員が楽しめるようにと、上司と相談して仕様を決めました。

完成した「感謝祭クイズ!」アプリは参加者の皆様から評判が良く、開発者体験の観点からも面白かったので、本ブログで紹介させていただく運びとなりました。

本記事は「感謝祭クイズ!」アプリに関するトピックの【前編】となっており、技術構成に関する簡単な説明と、どんなアプリを作ったのかについて紹介していきます。
【後編】の記事では、開発上使用した技術の詳細や、工夫について説明します。

「感謝祭クイズ!」の構成

「感謝祭クイズ!」アプリはWebアプリケーションとなっており、以下のような構成となっています。

「感謝祭クイズ!」構成

開発が必要なのは基本的にフロントエンドだけで、バックエンドはFirebaseにお任せな構成です。

フロントエンドはSvelteKitというフレームワークを使って開発しました。
SvelteKitはSvelteというWebアプリケーションフレームワークがベースのメタフレームワークで、ReactでいうNext.js、Vue.jsでいうNuxt.jsに相当するものです。
Svelteはフロントエンドのロジックを宣言的かつ簡潔に書くことができ、他のフロントエンドフレームワークと比べてパフォーマンスも比較的良いというところが気に入っており、採用しました。
さらにSvelteはTypeScriptをサポートしており、型システムを簡単に導入して開発することができます。

また、デザインはBootstrapをベースに、BootswatchというBootstrapテーマパッケージと、SveltestrapというSvelte用Bootstrapコンポーネントライブラリを導入し、なるべく既存のUIコンポーネントを組み合わせていくことで、開発にかかる時間を短縮することができます。

バックエンドにはFirebaseを採用しました。
クライアント側でリアルタイム更新を可能とするデータベースサービスをはじめ、認証サービス、画像などを配置・配信可能なストレージサービスなど、バックエンドに必要な一通りの機能が提供されています。
クイズ大会のリアルタイム進行を可能にするために、Firebaseは不可欠でした。

Svelte・SvelteKitのより詳しい説明や、役に立つと思ったこと、工夫した点などは【後編】で書いていこうと思いますので、お楽しみに!

「感謝祭クイズ!」はどんなWebアプリ?

ここからは、上記のような構成によって開発したWebアプリがどのようなものかご紹介していきます。

まず、「感謝祭クイズ!」には主に3種類の画面があります。

  • 「管理画面」:管理者がクイズや景品を設定します。
  • 「進行画面」:管理者がクイズ大会を進行させます。
  • 「参加者画面」:参加者がクイズ大会に参加し、クイズに解答します。

これらの画面を、以下の流れで利用します。

「感謝祭クイズ!」各画面利用の流れ

最初に管理者は、自身を管理者として登録します。
管理者登録が完了したら、管理画面でクイズを作成していきます。

「感謝祭クイズ!」クイズ作成

次に、クイズ大会そのものを意味する「感謝祭」を作成します。
感謝祭の作成画面では、複数の「ピリオド」というクイズのグループを作成し、それぞれにクイズと景品をセットします。

「感謝祭クイズ!」感謝祭作成

また、全ピリオドを合算した総合成績上位者に対する景品を設定することもできます。

「感謝祭クイズ!」景品設定

感謝祭の設定ができたら、感謝祭を開催します。

「感謝祭クイズ!」感謝祭開催

クイズ大会は、以下の流れで進行します。 管理者は、「次へ」ボタンを押してフェーズを進めていくことができます。

「感謝祭クイズ!」各フェーズの流れ

感謝祭を開催すると「参加者募集フェーズ」が始まります。
参加者は、共有されたURLかスマホ等からQRコードによってアクセスし、Googleアカウントでログインして感謝祭に参加します。

「感謝祭クイズ!」参加者募集フェーズ

参加者が集まったら、管理者はイベントの雰囲気に合わせてクイズ大会を進行していきます。
参加者画面は、管理者の進行に合わせて、各フェーズが自動でリアルタイムに進行していくので、参加者の皆様が一体感を持ってクイズ大会を楽しむことができます。
クイズは「準備」「解答」「正解発表」の3つのフェーズからなります。
「準備」フェーズでは、参加者の皆様に出題に備えてもらいます。

「感謝祭クイズ!」準備フェーズ

「解答」フェーズでは選択式クイズに解答し、「正解発表」フェーズに移行すると時間切れとなり、正解が発表されます。

「感謝祭クイズ!」解答・正解発表フェーズ

正解発表画面では、選択肢ごとに何%の参加者が選んだのかが分かるようになっているのもポイントです。

また、クイズには「ライブ問題」という「オールスター感謝祭」のような、その場で行われるミニゲームの結果を予想する形式のものもあります。
例えば、「感謝祭クイズ!」アプリに追加で実装された、タイピングゲームの結果を予想する問題は、大変盛り上がりました。

「感謝祭クイズ!」タイピング対決ゲーム
(ミニゲーム参加者のタイピングの様子がリアルタイムで表示されます。お題と合っているところまで青文字になります。)

このようにしてクイズを繰り返し、ピリオドごとの成績、及び総合成績で順位付けを行い、上位入賞者には景品がプレゼントされます。

「感謝祭クイズ!」総合結果発表フェーズ

順位の決め方は、正解数が多い人の方が上位で、同じ正解数の人は正答時間の合計が短い人の方が上位というものになります。

「感謝祭クイズ!」順位の決め方

したがって、「正しく」「早く」解答できた人が上位となる仕組みになっています。

「終了フェーズ」では、参加者全員のランキングと解答履歴を確認できます。

「感謝祭クイズ!」成績順位発表

これまでに2回、社員総会で「感謝祭クイズ!」を使ったのですが、2回目の開催時には、以下の機能を追加し、クイズ大会がさらに盛り上がりました。

「感謝祭クイズ!」追加機能

以上が、今回私たちが開発した「感謝祭クイズ!」の主な機能紹介でした。

社員総会で「感謝祭クイズ!」を使ったときの様子

社員総会の様子1

社員総会の様子2

社員総会の様子3

ライブ問題のミニゲーム、タイピング対決で勝者が決まった時の様子

上述したように、これまで2回、社員総会でこのWebアプリを使ったのですが、いずれもオンライン・オフライン問わず100人近くの方が参加されていたため、大きな不具合が発生しないか、毎回ドキドキでした。

しかし、管理者としてクイズ大会を進行していくたびに目の前の参加者の皆様が一喜一憂し、大盛り上がりする様を見ていると、「あぁ、動いているんだ!」と安心すると同時に、運営側の自分たちも大きな高揚感を得ることができ、とても楽しかったです。

また、社員総会のイベントレポートもぜひご覧ください!
【社内イベントレポート】FY23上期社員総会

おわりに

クイズ大会は大変盛り上がり、終了後には「とても楽しかった」「1回目より2回目はかなり進化していた」「これは、もはや仕事」「事業にしてもいいかもしれない」と、有難いお褒めの言葉を沢山頂きました。
また技術的にも、比較的新しいフロントエンドフレームワークであるSvelteを使ったり、バックエンドとしてFirebaseをフル活用するといったことが、面白い試みとして認めて頂けたことも嬉しかったです。

GREEのDX事業本部では、このように「身近な人をハッピーに」する、新しく魅力的なプロジェクトに挑戦していく仲間を募集しています。
次回【後編】では、今回活用した技術要素の紹介や、役に立つと思ったこと、工夫した点などについて書いていきますので、お楽しみに!

Social Pittの予約投稿機能

DX事業本部の木村です。

Socialpittでは2022年10月にInstagramのフィード・リール予約投稿機能がリリースされました。

今回はそのシステムのインフラ、サーバーサイドを含むシステム全体について紹介いたします。

予約投稿について

Social Pittの構成

Social Pitt の紹介とサービス支える技術 で紹介した通り、Social PittはCoreと各Serviceに分かれています。予約投稿機能は Socail Pittアカウント運用 の機能で、今回紹介する予約投稿のシステム自体は全てCoreにおける話になります。

Social Pitt アカウント運用では以前からTwitterの投稿の予約投稿には対応していましたが、10月にインフラをGCPからAWS、サーバーサイドの使用言語をPythonからRubyへ移行しており、この機会に予約投稿のシステム全体も1から作り直し、Instagramの予約投稿にも対応することにしました。

Social Pitt Coreの概要

Social Pitt Coreの構成

Coreは外部サービスからAPIでデータを収集するワーカーと、その収集したデータを返す内部APIからなっています。ワーカーはSQSとShoryukenを利用した仕組みを利用しており、簡単にスケールすることが可能になっています。

SQSのキューには、ECS Scheduled Taskによって定期的にメッセージをエンキューしたり、サービス側からエンキューしたりすることで非同期にジョブを実行できます。

予約投稿システム

予約投稿のシステムについても、Coreの仕組みに乗せ、予約投稿時間になったらエンキューし、それを消化するワーカーを動かすようにしてスケールさせるような構成にすることにしましたが、既存の仕組みをそのまま使用するにあたってはいくつか問題がありました。

Coreの仕組みで予約投稿システムを実現するにあたって問題になるのは以下の2点です

  • ECS Scheduled Taskでメッセージをエンキューする際の遅延
  • At-least-onceの配信による重複実行

ここからはそれらの問題をどのように解決したかについて紹介していきます。

ECS Scheduled Taskでメッセージをエンキューする際の遅延

上図の通り、既存のシステムでは定期実行タスクについてECS Scheduled Taskを利用してFargateのタスクを実行しメッセージをエンキューしていました。

Scheduled Taskでメッセージをエンキューする場合には

  • コンテナの起動時間による遅延
  • Fargateは頻繁に起動すると、それなりに起動に失敗することがある

という問題があります。

非同期にバックグラウンドでデータを収集する用途ではこの仕組みで全く問題はないのですが、設定した予約投稿時間中にタスクを実行したい予約投稿システムではこれらは問題がありました。

この問題を解決するには

  1. エンキューのためのECSサービスを動かし続ける
  2. ECS on EC2を利用する

などの方法がありますが、今回は1に近い方法かつ新たにサービスを作成せずに行いました。

具体的にはこのようなシステム図になります。

予約投稿システムの構成

EventBridgeのAPI destinations 機能を用いてCoreのAPIのエンキュー用のエンドポイントに毎分リクエストし、APIのサービスから予約投稿のメッセージをエンキューの処理を行います。APIサービスは常に稼働しているため、1の仕組みを新たなサービスを追加せずとも実現することができました。

この仕組みの場合には予約設定時間の0秒ちょうどに動く保証はないため、求められる要件によっては1分前にエンキューし、ワーカーでは予約時刻まで待ってから投稿を行うなどの工夫が必要になります。

At-least-onceの配信による重複実行

EventBridgeのメッセージ配信、およびSQSのメッセージ配信は共にat-least-onceを保証しており、メッセージが重複して送信される可能性があります。

そもそも、Shoryukenを利用したワーカーを書く際には冪等性を意識する必要がありますが、予約システムではそれに加えて重複実行させないことをどこかで保証できるようにする必要がありました。今回はexactly-onceをワーカー側で保証するようにしました。

予約投稿のワーカーでは以下の順で処理を行います。

  1. キューからメッセージを受け取り、処理する投稿IDを取得
  2. 投稿IDでDBを検索し、投稿のステータスがscheduledでなければここで処理を終了
  3. DBのロックを取り、scheduledであることを確認してから、job_in_progress にステータスを変更
    • ロックを取れない、ロックを取った後のステータスの確認でscheduledでなければ処理を終了
  4. 予約投稿処理を実行

Railsのコードとしては、以下のようにシンプルに実現できます。

post = Post.scheduled.find(message["post_id"])
post.with_lock do
  raise DuplicateJobError unless post.scheduled?
  post.job_in_progress!
end

このようにDBレイヤーでロックを取りつつステータスを変更することで、重複配信された場合でもscheduled ⇒ job_in_progressにステータスを変更できる1プロセスのみで予約投稿処理が実行され、重複実行を避けることができます。

その他に考慮した点

予約投稿機能を作成する上でその他に考慮した点です。

投稿の添付メディアの事前アップロード

予約投稿時間になってから画像や動画のアップロードと投稿の公開を行うと、サイズが大きい場合にアップロード自体に時間がかかり、予約投稿時刻に間に合わなくなってしまいます。Instagram, Twitter共にメディアを事前にアップロードし、そのidを用いて投稿を作成することができるため、添付メディアは予約投稿時刻の10分前に事前にアップロードを行うようにしています。Instagramの場合は事前アップロードのコンテナの有効期限は24時間のため、予約投稿時刻を未来に更新する場合にはidを無効化する必要があり、細かいところで注意する必要があります。

予約投稿のワーカーでは、事前アップロードのidがあればそれを用いて投稿を作成し、(予約時刻直前に予約した場合などで)idがなければアップロードを行なってから投稿を行なっています。

リトライ

Shoryukenを用いている場合にはSQSのVisibility TimeoutとMax Receive CountでSQS側でリトライが行われますが、こちらが要件にマッチしているかは考慮する必要があります。現在Social PittではRails側で生じた例外は全てキャッチして、投稿の失敗をユーザーにメール通知しリトライは行わないようにしています。

おわりに

今回はSocial Pittの予約投稿機能の裏側について紹介いたしました。

予約投稿機能がリリースされたことにより、社内のSNS運用代行における予約投稿率も上昇し業務効率化につながったり、ユーザーからも予約投稿機能について喜びの声をいただいたりとこの機能を無事リリースできて良かったです。

個人的には既存のシステムに乗せたこの仕組みについては満足しているのですが、予約投稿機能のリリースの直後に発表されたAmazon EventBridge Scheduler を利用することでもっとシンプルに予約投稿機能を実現できるかもしれないと気になっているところです。(予約投稿時間の変更など考慮すべき点は色々ありそうですが……)

【イベントレポート】クリエイターツール「QUANT」とデータ分析基盤でみるGlossomの技術的挑戦

2022年10月25日、グリー株式会社およびグリーグループ各社は、「GREE TECH CONFERENCE 2022」を開催しました。グリー株式会社およびグリーグループ各社では、これまでゲーム・アニメ事業をはじめ、メタバース、コマース、DX、マンガとさまざまな事業領域でサービスを開発・運営するとともに、技術的な挑戦に数多く取り組んできました。

「GREE Tech Conference」では、グリーグループ各社から、エンジニアが集い、技術的情報を共有、発信する機会として、毎年開催。各社で取り組んでいるさまざまなチャレンジで得られた知見や、これから取り組むチャレンジを紹介しています。今年のカンファレンスは、3年ぶりのリアル会場での実施に加え、史上初めてのオンラインとのハイブリッド開催となり、リアル・オンラインともに、大いに盛り上がりました。

今回はそのなかから、DX事業を手掛けるGlossom株式会社のエンジニアが登壇した「クリエイターツール「QUANT」の開発の話 & クライアントに寄り添ったデータ分析基盤の構築」の講演についてお伝えします。

 

 

 

Glossomが提供するクリエイターツール「QUANT」の開発について

前半は、エンジニアマネージャーの菊井昭夫から、クリエイターツール「QUANT」の開発について紹介しました。

 

Glossom株式会社 プロダクト事業本部
開発チーム シニアマネージャー
菊井 昭夫

 

「QUANT」とはインフルエンサー向けのソーシャルコマース支援サービスです。インフルエンサーの仕事管理として、案件依頼や進捗、売上の管理ができるほか、QUANTの担当者が案件をマッチング。有名企業やブランドからの案件紹介を受けることができます。

 

Cloud Bigtabeを採用

QUANTでは、特色ある実装として、Cloud Bigtabeを採用しています。Cloud Bigtableとは、最大 99.999%の可用性で大規模な分析ワークロードにも運用ワークロードにも対応できる、フルマネージドでスケーラブルな NoSQLデータベースサービスです。Cloud Bigtabeを実装したことで、評価できるポイントは、大きく2つあります。1つ目は、データの書き込みです。Cloud Bigtableの方でexpireを適切に設定することで、データの削除を気にすることなく、追記更新を常に続けることができます。2つ目は、データの読み込みです。読み込み時にフィルタを利用することで、追記更新した値の最新バージョンのみを返すよう指示しており、結果的に読み込み書き込み問わず、削除を考慮せずに利用できます。全体としては、アプリケーションレイヤーでのデータ削除や、追記更新時や読み込み時に既存のキーのデータ重複を考慮せずに実装できるので、KVSとして使いやすく、性能に現在も満足して利用しているといいます。

QUANTのフロントでは、Railsを採用しており、今回は特色のあるGemについて紹介します。view_componentを採用した経緯としては、より良いサービスにするため日々機能を追加していった結果、業務ロジックが複雑化し、Viewのソースコードが肥大化。テストコストや、ロジックの可読性が落ちてしまいました。そこでview_componentを採用することで、コンポーネント化でロジックを分割することで、コンポーネント単位のテストが可能になりました。

 

view_componentの実装例

view_componentの実装例は下記の通りです。index.html.hamlで検索のUIを表示する実装になるが、上のコードを下のコードに実装を差し替えるだけで、view_componentの実装をしています。

index.html.haml
# before
= render 'search'

index.html.haml
# after
= render Campaigns::SearchComponent.new(form: form)

 

Campaigns search_componentでは、classと表示されるhamlを対にして配置する形になっています。こちらも、業務ロジックを分割した内容を書くことで、簡単に分離することが可能です。view_componentによって処理を分割することで、スペックもRSpec.configureでtype::componentを追加する必要がありますが、type::componentを指定することで、コンポーネント単位でのテスト実装ができます。

 

# search_component.rb
class Campaigns::SearchComponent < ApplicationComponent
  attribute :form
end

# search_component.html.haml
= simple_form_for form, url: campaigns_path do |f|
  = f.input :hogehoge

# search_component_spec.rb
# RSpec.configure で type: :componentを追加する必要があります
RSpec.describe Campaigns::SearchComponent, type: :component do
  describe ......
end

 

view_componentを採用して評価できる点としては、巨大なView表示するための、大量のテストデータの用意や、分割することでSpecが書きやすくなったことが挙げられます。また、リリース当初は採用していませんでしたが、こちらを採用しコンポーネント化したことにより、ロジックの整理が進んだほか、テスト品質を保つことができました。Viewが肥大化した際には、こちらのGemの使用がおすすめです。

 

 

Railsの実装例

「QUANT」のサービスは、プロフィール管理画面は pf.quant.jp、マイページ画面は quant.page、ショップ画面は shop.quant.page というように、複数のドメインで構成されています。楽に開発管理や実装共有をしたかったので、Rails Projectを分けずにsubdomain実装により実現してます。

実装例としては下記のようになっています。routesのファイルに対して、constraints subdomainで実際にサブドメインを割り当て、その先のscope moduleで、サブドメインごとの処理を記載します。これによりRailsプロジェクトを分けずに、複数のドメインに対してサービスを展開することが可能です。あえてサービスを実装上分けたくない場合に、こういった記載がおすすめです。

 

routes.rb
constraints subdomain: ENV.fetch('SUBDOMAIN', 'subdomain').to_s do
  scope module: :'subdomain' do
    resources :tops, only: [:index]
  end
end

 


クライアントに寄り添ったデータ分析基盤の構築

後半は、データエンジニアの飯山誠也から、クライアントに寄り添うデータ分析基盤構築について紹介しました。

 

Glossom株式会社 DXC事業本部
データ・エンジニアリングチーム
飯山 誠也

 

データ分析基盤はGCP環境を使用しています。そのなかで各種データはCloud Composerを介して、BigQueryに書き出しています。また、Kubernetes EngineやDateflowを使い分けることでクライアントの環境に適したデータの書き出しを実現しています。BigQueryに集約したデータは、Looker Studioで可視化しています。

 

データ分析基盤の主要ツール Airflow



Cloud Composerは、Airflowをベースとしたワークフロー管理ツールです。Airflowは、DAG形式で、Task間の依存関係を管理できます。それぞれのTaskは、データを取り込んだり、他のシステムを呼び出したりする処理を実行することが可能です。Airflowには、さまざまな処理を行う機能が標準で設けられ、標準オペレーターを使用すると手軽にデータ取り込みが可能です。データ取り込みがすべてAirflowの標準オペレーターで実現できれば、運用しやすいデータ基盤を構築することができます。

 

クライアントごとに異なるIT活用状況に寄り添う

しかし現状は、そのようにはいきません。それはクライアントによって、ITの活用状況が異なるためです。データ管理の体制が整っておらず、データの保管先が点在しているケースや、連携するファイル数を把握できていないといった状況が挙げられます。データを所定の保管先に連携できる担当者やエンジニアが社内にいないというケースも珍しくありません。さらに、データの保管先が社内政治的にすでに決まっている場合もあります。この現状を打破するためには、クライアントごとに異なるITの活用状況に寄り添った、データ取り込みが必要なのです。

 

 

データ取り込み手法の使い分け

Airflow Custom Operatorは、開発が比較的容易に行えるため、ライブラリを活用して取り込む場合に有用です。GKEのPodを活用する場合は、リソースを切り離せるほか、文字コードの変換に対応できるため、UTF-8以外の文字コードの場合や、リソースの使い分けが必要な場合に、より有用性が増します。Dataflowでは、リソースを切り離せるほか、大規模データの取り込みに対応しているため、ファイル数やファイル容量が大きい場合に採用しています。

 

今後の展望

クリエイターツールQUANTでは、今後のインフルエンサーマーケティング市場の発展に貢献できるよう、計測したデータをより活用することや、インフルエンサーを支援する機能を追加する予定です。
データ分析基盤では、機能面の充実化を図る予定です。環境構築後のワーカーCPUやメモリのオートスケーリングが可能なDataflow Prime、Dataformやdbtなどのデータリネージツールの導入を検討しています。このようにGlossomでは、今後もより良いサービスの提供を目指し、新たな機能やツールの導入や実装を進めていきます。

 

 

Social Pitt の紹介とサービス支える技術

DX事業本部の長谷川です。

今回は、DX事業本部のグリーライフスタイル株式会社で提供している「Social Pitt(ソーシャルピット)」というサービスを支える技術についてご紹介します。

Social Pittとは? = ソーシャルマーケティングSaaSです

Social Pittは、グリーライフスタイルにおけるSNSアカウント運用事業を効率化させるための内部ツールとして開発されました。

主に以下のような機能で運用チームをサポートしてきました。

  • クライアントとの投稿(クリエイティブ、テキスト)確認
  • アカウントの分析
  • キャンペーン実施時のコメント抽出
  • 競合アカウント分析
  • ハッシュタグ分析

社内の効率化をしていく一方で、業界をみると、当時まだまだExcelやLINEで企業間のコミュニケーションをとっている企業は多かっため、このツールを他の企業に利用してもらうことでマーケティングのDXにつながると判断し、SaaS化していくことにしました。

Social Pitt の構成

SNSアカウント運用のツールとしてスタートしたSocial Pittですが、今ではUGC機能やクリエイター管理機能を追加し、Eコマース事業者向けのマーケティングSaaSとして拡大しています。

コードは、以下のようにコア機能とアプリケーション機能でわけて管理されています。

Core

Coreの責務は2つです。

  1. 外部サービスと接続し、データを収集すること
  2. 各Serviceに対して、APIをインターフェイスとしてデータを提供すること

外部サービスとのI/OをCoreに集約することで、各Serviceにおいて重複実装することを防いでいます。

Service

Serviceは機能毎に独立したRailsアプリケーションになっています。

CoreのRubyクライアントを利用してデータを取得、認証もFirebase Authenticationを利用するために共通のRubyクライアントを利用しています。

Serviceを開発する人員は基本的に兼務はせず、それぞれがオーナーシップを持って担当プロダクトの開発に取り組んでいます。

Social Pittを支えるOSS

ここからはSocial Pittで利用しているOSSの紹介とその選定理由について説明します。

Ruby on Rails

Social Pittは、全てRuby on Railsで実装されています。技術スタックが統一になることで共有のgemが利用でき、実装工数の削減につながっています。

全てRails7で動いており、動的な表示にはStimulusやTurboを利用しているところもあります。Turboは画面遷移時に意図せず不具合を生み出しがちで、まだ完全には使いこなせていません。

Vue.js

アカウントの運用の分析画面はVue.jsで実装されています。期間や並び順を切り替える毎に、Highchartsで実装されたグラフを描画し直します。

Social PittはSPAでは作成されていなかったのですが、グラフに関してはUX向上を目指し、部分的にSPAにし、日本で採用事例が多いVue.jsを利用しました。

Svelte

SvelteはUGC機能のギャラリー表示の実装に利用されてます。UGC機能はECサイトのLPにウィジェットとして埋め込み、CTR/CVRを計測しながら、LPのCVRを向上させることができます。

外部サイトで読み込まれるため、なるべく軽量で高速に動作するSvelteをフレームワークとして採用しました。

Tailwind CSS

CSSのフレームワークはTailwind CSSを採用しています。今までは管理画面を作る際にはBootstrapを採用することが多かったのですが、Railsが押していること、Bootstrapよりも柔軟性があることを理由に選定しました。

コンポーネントを実装する際は、Tailwind CSSのコンポーネントを探して、一旦コピペしてきて、そこからアレンジするといったこともでき、時間の削減に貢献してくれています。

Social Pittの今後・課題

Social Pittの特徴として、外部サービスから多くのデータが溜まっていきます。今はそのデータを上手に活用できておらず、本来であれば機械学習を利用して付加価値のあるデータに転換できると考えています。

例えば、インフルエンサーをアサインする際には、インフルエンサーのフォロワー属性が重視される(女性インフルエンサーには必ずしも女性のフォロワーが多くない)ため、投稿内容を分析してフォロワー属性を推定するということも考えられます。

また、Socialリスニングで、自社商品のSNS上の反応はポジティブなのかネガティブなのか、どのような意見があるのか、をセンチメンタル分析でわかるようにするということも検討しています。

DX事業は、単なる業務の効率化に留まらず、データを活用して付加価値をつけることに価値があると考えており、今後チャレンジしていきたいと思います。