Next.js で人気投票・分析サイトを作った話
はじめに
Next.js での開発、楽しいです。
今回は妻がハマっているゲームのキャラクター人気投票・分析サイトを作りました。
分析画面 左:ページ上部 棒グラフ部分、 右:ページ下部 折れ線グラフ部分
私のアカウントでログインした際の投票画面
半月で 494 人が投票して下さいました、ありがとうございました!
(ログイン・投票が出来ない方、一度ログイン関連の処理を全面的に見直しましたので、もしかしたら今は出来るようになっているかもしれません...)
アクセスが最も集中したリリース後2日間、1時間当たりの投票数の遷移
この記事では Next.js アプリケーションを作りたい人向けの技術スタック例や、ちょっと変わったアンケートサイトの内部ロジックについてご紹介します。
モチベーション等の非技術的な部分も書いてしまいましたが、折りたたんで残しておきます。
技術スタック
-
Next.js v15(RC)
- Server actions を積極的に使用します
- 投票ページ(要サインイン)はセッション情報を用いた Dynamic Rendering をします
- 分析結果ページは Static Rendering + 5分毎の revalidation をします
- データ集計にリソースを消費するので(特に過去日付時点までの集計結果1か月分の計算)、ここを Dynamic Rendering にするとサーバが忙しくなってしまいます
- Static Rendering & Dynamic Rendering についてはこちら
- Server component でデータ取得するので、fetch, useSWR, tRPC 等は未使用です
- React (SPA) から Next.js 勉強し始めた際は絶対必要だと思い込んでいました
- Server component, client component についてはこちら
-
Auth.js v5
- 様々な OAuth プロバイダ対応の認証用ライブラリです
- 今回 Nginx リバースプロキシを使用しました、設定については以前の記事にもまとめています
https://zenn.dev/daiius/articles/49793eacae822f
-
Chart.js
- インタラクティブな折れ線グラフ表示に使用します
-
Tailwind CSS
- Next.js と相性の良い CSS フレームワークの一つです
- Mobile first, responsive design を意識して設定しました
-
Headless UI
- モーダルやボタン等について、フォーカス・キー操作等のロジックが実装されています
(headless の名の通り見た目部分は空または素なので、Tailwind CSS で作ります)
- モーダルやボタン等について、フォーカス・キー操作等のロジックが実装されています
-
dnd kit
- 投票画面で一部ドラッグアンドドロップ挙動を実装するために使用します
-
Drizzle ORM
- TypeScriptによるデータベースマイグレーションができます
-
MySQL
- 言わずと知れた RDB(リレーショナルデータベース)です
Githubリポジトリ
何を対象にするのか
対象にするのは、某1~4までシリーズが出ている女性向けの恋愛シミュレーションゲームです。
モチベーションについて...
男性ゲーム実況者もこのゲームを紹介しています。妻が隣でプレイするのを見ていても面白いです。
攻略キャラ、非攻略キャラ合わせて60人以上の登場人物がおり、一人一人がとても魅力的です。
このゲームの登場人物に対して人気投票 + α が出来るサイトを作りたいと考えました。
人気投票 + α を目指して
普通の人気投票であれば公式も行っていますし目新しさがありませんので、複数の推しを投票可能にして、あるキャラを推す人が他にどのキャラを推しているのか分析したいと考えました。
X(旧Twitter)でファン同士交流している妻曰く、何か傾向が有るようなのです。気になります。
もし上手く傾向を分析できれば...
ファンが他のキャラに興味を持ったり、次に攻略するキャラを選び易くなったりといった効果が出たら一石二鳥ですし、もちろん逆に自分が唯一無二のセンスの持ち主であることに気付けたりすることも、楽しんで頂けるのではないかと考えました。
もちろん一人一票の単純な投票システムと比べて難易度が上がりますが、挑戦し甲斐があります。
完成系のイメージ固め
今回はやりたいことが比較的ハッキリしていましたので、次の様なリストアップはすぐに出来ました。
先ずは全体的な構成について...
- ブラウザさえあれば広くアクセス可能な Web アプリケーションにします
- Next.js を使って自分の技術向上を狙います
- 投票結果を保持する必要があるのでデータベースを使います、Drizzle ORM + MySQL は経験があります
- 本番環境は Docker (Compose) 管理なので、Next.js アプリケーションはコンテナ化します
(メモリを喰うので、MySQLコンテナは他のアプリケーションと共有にします)- オーバーヘッドの小さい distroless コンテナを使用します
次は使い方について...
- 同一ユーザか判断できるよう、X(旧Twitter)アカウントを用いた OAuth 認証を行います
(X Developerが無料で構築できる OAuth 設定は1つのみですが、使い時だと判断しました) - あるユーザの推しの組み合わせの変化を追えるよう、過去の投票結果も履歴としてデータベースに保存します
最後は見せ方について...
- ある特定のキャラ毎に、他にどのキャラと推されているかの頻度を棒グラフで表示します
- 時系列での変化も気になるので、上記の履歴データを使って過去時点までの集計結果を得て、投票状況の変化を折れ線グラフで表示します
- 全体的なデザインは、ゲーム中のUIを真似てみます
認証データ使用方法
ユーザデータの使用方法は次の図のようになります。
今回のアプリケーションでは、本人以外は具体的に誰がどんな投票をしたのか明らかにする必要はありません。
X(旧Twitter)OAuth 認証ではユーザアカウント毎にユニークなIDを取得することが出来ますから、これを使えば、例えば @ から始まるユーザ名等の 不必要な(詳しすぎる)ユーザ情報をデータベースに含まずに済みます。
データベース設計・管理
Drizzle ORM によって、TypeScript によるデータベース定義とマイグレーションを行えます。
主なテーブルをER図にするとこんな感じです。
投票結果の分析を行う SQL 関連部分も、Drizzle ORM でできます。
コアになる投票内容の分析SQLは次の様な感じです。
同じ投票時間を持った投票データの組の中から柊夜ノ介というキャラが含まれるもののうちで、
同じ twitter_id
を持つ投票の中で最も新しいもののうちで、
柊夜ノ介以外のキャラの名前と票数を集計して出力します。
select
character_name,
count(*) as count
from
Votes as t1
where
exists (
select
character_name
from
Votes as t2
where
t1.twitter_id = t2.twitter_id
and
t1.voted_time = t2.voted_time
and
t2.character_name = '柊夜ノ介'
)
and
voted_time = (
select
max(voted_time)
from
Votes as t3
where
t1.twitter_id = t3.twitter_id
)
and
character_name <> '柊夜ノ介'
group by
character_name
;
運用時に気付いたこと
Next.js コンテナがピーク時で 200 MB 弱のメモリを消費していました。想定より多かったのでサーバのスワップ領域を拡大しました。
非ピーク時には 100 MB 前後まで下がりましたので、アクセス状況によって変動する様です。
Static Rendering のページをただ返すだけでも多少のリソース消費があるのが分かりましたので、Nginxのキャッシュ設定を revalidate 時間に合わせて設定しました。ある程度の負荷削減になったようです。
終わりに
リソース消費もそれなりですが、Next.js は React だけでは実現できない様々なニーズに応えてくれます。
もっと上手く機能を使ったり、調整したりすれば、もっと高度な機能を効率よく実装できそうです。
引き続きリサーチを続けて参ります。
Discussion