kk-web

オンライン版プランニングポーカーをリリースしました

2023-03-03

Planning Poker

オンライン版プランニングポーカーをリリースしました。

まだクオリティはベータ版くらいですが、ぼちぼち動作するとは思います。

ぜひぜひ使っていただけると嬉しいです。

以下雑記です。


今現在所属している会社ではスクラムを導入しているのですが。

バックログリファインメントにおいてプロダクトバックログにポイントを振る際、プランニングポーカーを使用しています。

で、自身を含め開発メンバーはリモートワーカーが結構多いので、オンライン版のプランニングポーカーを使用しているのですが。

まーなかなか使いやすいサービスがなく、どれもダサい!!

ということで、自分で作ってみました。


今回使用した主な技術は以下のとおりです。

  • firebase v9(Firestore のみ)
  • firebase-admin v11(Firestore のみ)
  • next v13.2
  • react v18.2

以下今回得られた知見や感想です。

onSnapshot すごい

数年ぶりに Firebase を使用して実装を行いました。

というのも、Firestore には onSnapshot という、擬似的?な streaming api が提供されていることをはじめて知りまして。

『これを使えば簡単にリアルタイムアプリケーションが作れるのでは?』と思いプランニングポーカーを作成してみました。

で、実際に onSnapshot を使用し実装を行ったのですが、まー驚くほどスムーズに動いてくれ、久しぶりに感動しました。

今後もリアルタイム性が求められる簡単なサービスでは onSnapshot を使用して実装してみようかなーと思いつつ。

もう少し複雑性が必要であれば SkyWay とかが選択肢に挙がってくるんですかね?

Firebase のドキュメントが見づらい、少ない

Firebase の公式ドキュメントは相変わらず絶妙に見づらいですね。

ほぼ数年間まともにアップデートされていない印象を受けます、もう少し要点をまとめてほしいなーと想いつつ。

公式ドキュメントから理解できないことについては個人のブログなどを調べたのですが、最新の firebase や firebase-admin に関するドキュメントが全然見つからなかったです。

いまだにまともに型のつかない Firebase を使用するメリットもほぼ存在しないですし、あのレガシーな仕様で開発を行うのは、なんだかんだで厳しいよなぁと。

いい加減型セーフにすれば良いのにと想いつつ、多分もう一生変わらないんだろうなーと。

Firebase のエミュレーター良い感じ

はじめて Firebase のエミュレーターを使ってみたのですが、めちゃくちゃ良いですねこれ。

ほぼ無設定で使用できて使いやすく、まったく文句ないです。

firebase-admin でのみ更新を行う場合は write は許可しなくて良い

firestore の rule の認識が今まで曖昧だったんですが、対象は firebase のみとのことです。

firebase-admin でのみ更新を行う場合は、write は許可する必要がありません。

なので以下のルールで問題ないってことですね。

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read: if true;
      allow write: if false;
    }
  }
}

続いて Next.js × Firebase の実装方法をザクッと書いておこうと想います。

クライアントサイドでエミュレーターに接続する

import { connectFirestoreEmulator } from "firebase/firestore";
import db from "@/libs/db";

if (process.env.NODE_ENV === "development") {
  connectFirestoreEmulator(db, "localhost", 8080);
}

export default function RootLayout({
  ...

今回は Firestore のみ接続しました。

サーバーサイドは未確認ですが、この書きっぷりで行ける気もします。

onSnapshot をカスタム hooks でラップする

import {
  DocumentData,
  Query,
  QuerySnapshot,
  onSnapshot,
} from "firebase/firestore";
import { useState } from "react";
import { useBoolean, useEffectOnce } from "usehooks-ts";

export type QuerySnapshotParams<T> = {
  query: Query<T>;
};

export type QuerySnapshotData<T> = {
  data?: QuerySnapshot<T>;
  loading: boolean;
};

export default function useQuerySnapshot<T = DocumentData>({
  query,
}: QuerySnapshotParams<T>): QuerySnapshotData<T> {
  const [data, setData] = useState<QuerySnapshotData<T>["data"]>();
  const { setFalse: offLoading, value: loading } = useBoolean(true);

  useEffectOnce(() => {
    const unsubscribe = onSnapshot<T>(query, (snapshot) => {
      offLoading();

      setData(snapshot);
    });

    return () => {
      unsubscribe();
    };
  });

  return { data, loading };
}

Reference についてもほぼ同じ実装なので省略します。

useEffectOnce が実装的にちょっと汚いので、もっとシンプルに書けるとは想います。

呼び出し側は以下のとおりです。

const { data: usersData } = useQuerySnapshot<Firestore.User>({
  query: collection(db, "rooms", roomId, "users"),
} as QuerySnapshotParams<Firestore.User>);

もっと良い実装方法もあると想います。

Api Routes で firebase をエミュレーターに接続する

import { connectFirestoreEmulator, doc, updateDoc } from "firebase/firestore";
import db from "@/libs/db";

type Params = {
  params: {
    roomId: string;
  };
};

export type PatchRoomsRoomIdAdminIdBody = Pick<Firestore.Room, "adminId">;

export type PatchRoomsRoomIdAdminIdData = void;

export async function PATCH(
  request: Request,
  { params: { roomId } }: Params,
): Promise<Response> {
  if (process.env.NODE_ENV === "development") {
    try {
      connectFirestoreEmulator(db, "localhost", 8080);
    } catch {}
  }

  const body = (await request.json()) as PatchRoomsRoomIdAdminIdBody;
  const docRef = doc(db, "rooms", roomId);

  await updateDoc(docRef, body);

  return new Response();
}

型など未精査な部分も多いので、参考程度に…。

Api Routes で firebase-admin をエミュレーターに接続する

firebase-admin の時点で、基本的には Api Routes で使用することが多いと思いますが。

{
  "scripts": {
    "dev": "FIRESTORE_EMULATOR_HOST=localhost:8080 next dev"
  }
}

これだけ!めちゃくちゃ楽ですね。

もしかしたら firebase もこれでいける?未確認ですが。

firebase および firebase-admin のインスタンスの作成

import { initializeApp } from "firebase/app";

const firebaseConfig = {
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
  appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
  messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
  storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
};
const app = initializeApp(firebaseConfig);

export default app;
import { getFirestore } from "firebase/firestore";
import app from "@/libs/app";

const db = getFirestore(app);

export default db;
import { cert, getApps, initializeApp } from "firebase-admin/app";
import { getFirestore } from "firebase-admin/firestore";

if (!getApps().length) {
  initializeApp({
    credential: cert({
      clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
      privateKey: (process.env.FIREBASE_PRIVATE_KEY || "").replace(
        /\\n/gm,
        "\n",
      ),
      projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
    }),
  });
}

const adminDb = getFirestore();

export default adminDb;

そんな感じです、参考になれば幸いです。

今回のリポジトリはここに公開しています。

© 2018 kk-web