Next.jsですごろくゲーム作ってみた
はじめまして。現在、株式会社ヌーラボでインターンをしています、ぽるしぇと申します。インターン先では現在フロントエンドを担当させていただいています。
インターンを始める前は、フロントエンドは個人や友人とのチーム開発程度のものしか実装したことがなく、ガチの開発を経験したことが全くありませんでした。
今回は、このインターン先での研修を通して、私が経験したことや学んだこと、感じたことを話せたらと思います。最後まで読んでいただけますと幸いです🙇♂️
すごろくトーク
私が作ったものは、すごろくのゲームで、名を「すごろくトーク」と言います。
1人がサイコロを振り、出た数分だけマスを進み、そのマスに書かれたテーマについて本人が話し、これを繰り返すというシンプルなゲームです。
インターン先ではこのゲームが広く浸透していたのにも関わらず、従来ではこのゲームを描画ツール上で再現し、ホストが皆のアイコンとして描画された図形を動かすという、原始的な方法でこのゲームをプレイしていました。
今回の研修では、このゲームをよりクールに作ることに挑戦しました。
完成したゲームは以下となります。
アカウントの問題上、撮影時はローカル環境で動かす必要があり、サイコロが暴れてしまっていますがご了承ください😢
この開発は、私ともう一人のインターン生の2人で行い、私はフロントエンドの方を担当しました。今回は、この開発を通して学んだことや感じたことを書いていこうと思います。
*以下の記事ではより広い視点で開発体験記を書いていますので、気が向きましたらぜひご覧ください。
Next.js
今回は Next.js を用いてアプリを構築しました。
このアプリではWebSocketを使ったリアルタイム通信を実装する必要があり、受信したデータに基づいてUIの必要な部分のみを効率的に更新できるReactの特性が非常に適していると考えました。
また、このゲームではアニメーションを一定程度実装する必要があり、処理が重くなることが予想されました。Next.js にはこれを軽減する機能が備わっており、非常に魅力的でした。
例えば、next/Image によって画像やメディアを自動で最適化でき、表示負荷を軽減できます。また、重い物理演算ライブラリ(Three.js や Cannon.js)を 遅延読み込み することで、初期ロードを軽くできます。
これらの利点により、アプリのパフォーマンスを維持しつつ、快適なユーザー体験を提供できる構成になると判断しました。
また、今回はSSR/Server Actionsは使用しない設計にし、ビルド時にHTML生成し(SSG)、配信後はクライアントサイドでUI更新(CSR)をするようにしました。そのため、デプロイは静的エクスポートで実現しています。
next/Imageの静的エクスポート対応には追加設定が必要ですが、next.config.js で簡単に設定できるためとても便利でした。
ディレクトリ設計
今回はfeaturesディレクトリを採用しました。
featuresディレクトリがどういうものかは、以下のリンクがわかりやすかったため、参考に貼っておきます。
この設計の強みは、コードの整理のしやすさと、スケールのしやすさだと思います。
このアプリは、まず最低限の機能から備え、ユーザのフィードバックをもとにどんどん機能を追加していきたいと考えていましたが、このディレクトリ設計を採用することで、とても開発がしやすかったです。
まず、feature(機能)ごとにロジックを分けることで、各ロジックの責務範囲を明確にし、機能改善や削除を楽にできます。
そして、feature内では、役割(例えばステートやカスタムフックやコンポーネント)ごとに分けることで、依存方向を制御しやすくできます。
こうすることで、機能ごとの責務を明確に、かつ依存関係を制御しやすくすることで、コードを整理しやすく、また、新しい機能の追加も行いやすかったです。
今回分けたfeatureは以下になります
auth // 認証
user // ユーザ
coin // コイン
room // ゲームルーム
matching // マッチング
introduction // ゲーム開始時の自己紹介
game // ゲーム
webSocket // WebSocket通信
translation // 翻訳
役割は以下のように分けました
const // 静的変数
state // グローバルステート
types // 型定義
utils // 関数ロジック
hooks // カスタムフック
components // コンポーネント
認証
今回は認証方法の1つとして、JWT認証を採用しました。
JWTトークンがどういうものかは、以下の記事がわかりやすかったため、参考に貼っておきます。
今回JWT認証を理由は、ステートレス性を実現したかったからです。
バックエンドでセッション情報を管理する方法だと、ユーザーの増加に伴い負荷が大きくなります。特に今回はゲームの仕様上、バックエンドの負担が大きくなりそうな設計にしたので、これは避けたかったです。
JWT認証というのは、トークンが変えられていないかをバックエンドで検証できれば良いので、追加でセッション情報を管理する必要がなくなります。
ただ、そのためにはJWTをユーザ側で保持する必要があり、いくつかのセキュリティ対策が必要でした。
例えば、JWTには重要なユーザ情報を含めず、クッキーに保存するようにし、またクッキーにはHttpOnly属性とSecure属性をつけることで、JavaScriptからアクセスできないようにし、XSS攻撃のリスクを減らしました。
今回、JWTにはユーザIDとセッションの有効期限を含め、これをバックエンド側で検証してもらいました。トークンが改変されていない、かつ有効期限が切れていなければ、フロントからのリクエストに返答するようにしました。
こうすることで、ユーザ(フロントエンド)は保持しているJWTトークンを使ってユーザ情報を取得したり、ソケット通信のハンドシェイク時に認証を通すことができるようになりました。
このように、フロント側での検証は不要で、バックエンドでもセッション情報を管理する必要がない、効率的な認証設計になったと思いますが、安全面ではまだまだ課題があると思います。今後新しい知識を取り入れるたびに改善していければと思っています。
状態管理
今回のアプリ開発で特に困ったのは状態管理でした。
Reactコンポーネントの開発において、なるべくコンポーネント自体は何も情報を知らなくても良いようにしたかったのですが、そうすると親コンポーネントから子コンポーネントに渡す必要がありました。しかし、今回のゲームにはマップ、ユーザリスト、サイコロ、メニューバーなど複数のコンポーネントがあり、さらにこれらにもそれぞれ子・孫コンポーネント、別のfeatureのコンポーネントも複数あるため、親子関係が深く、ステートのバケツリレー状態となってしまい、開発が非常に困難でした。
そこで、グローバルステートを用いようと思い、Jotaiを採用しました。
Jotaiを使うことで、atom単位で状態を管理でき、必要なコンポーネントだけが状態を参照して再レンダリングされるため、親子関係に関わらず状態を簡単に共有できるようになりました。
特に恩恵を受けたのはWebSocket通信の部分です。
今回はリアルタイム通信を実現するためにWebSocketを採用しましたが、マッチングやゲーム時のいくつもある処理ごとにソケット通信を開閉し、毎回認証を行うのは非効率でした。
そこで、ハンドシェイク後に確立されたソケット接続をatomとして管理し、各コンポーネントで共有することで、ゲーム開始から終了まで共通のソケット通信を使うことができました。
このように、jotaiのようなグローバルステートを用いたことで、各コンポーネントは自分が必要とする状態だけを受け取ればよくなり、依存関係を小さくでき、独立性と再利用性を高めることができました。
アニメーション
今回のアプリで、UXを上げる工夫として特にこだわったのはサイコロのアニメーションです。
なるべくサイコロはリアリティのあるものにしたいというエゴがありましたが、最初はCSSのみではできないし、かといって動画は面白みがないと思っていましたが、物理演算ライブラリで実現できるということを知りました。
今回は、1から作るのが面倒だったため、CodePenで公開されていたサイコロのアニメーション(MITライセンス)を基として作らせていただきました。
React用にframer-motion-3dというのが用意されていましたが、これはReact19にはまだ対応できていなかったようです(2025年5月時点)。
ゲームエンジンがなくとも、ある程度の良いものは作れるということを知れました。
まとめ
今回の開発では、非常に多くのことを学び、経験することができました。
React、Next、TypeScriptの型づけに関する知識やテクニックだけでなく、これらの表現の可能性を実感し、実際に手を動かして形にする体験もできました。
また、フロントはもちろんのこと、セキュリティやインフラに関する知識もほんの少しだけではありますが学ぶことができました。
そして、今回は採用していなくとも、まだまだフロントには多くの技術があるということを知りました。このアプリに必要な部分のみを調べてこれなので、きっともっと広い世界が広がっているのだろうと感じました。
これからもこの経験を活かし、新しいことをどんどん吸収していきながら、自分のどんなアイデアも形にできるようなエンジニアになっていきたいです。
Discussion