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リストを作る

※ 参考コード

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 unsubscribeSound = () => {
  stopPlayingSound();
  get(soundUnsubscriber)?.();
};

心残りだったこと

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

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

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

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

おわりに

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

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

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

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

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