2022 ナンプレアプリ開発まとめ
これはドワンゴ Advent Calendar 2022 の13日目の記事です。 qiita.com
自分は普段、ドワンゴの教育事業部にてN予備校のWebフロントエンドを開発しています。 この記事では趣味で個人開発しているナンプレアプリについて、今年の活動を報告します。
ナンプレアプリ numberp とは
ブラウザで動作するナンプレ(数独)のアプリです。 ゲーム画面はもちろん、問題の生成処理も自作しています。 全てフロントエンドで完結していて、バックエンド処理はありません。
- GitHub URL
numberp は「ナンバーピー」と呼んでいます。ナンプレっぽいドメインを探していたときに見つけたのがこれでした。
Vue から React へ移行した
モチベーション
2021/12 にドワンゴ教育事業部のWebフロントエンドエンジニアとして N予備校の開発に携わることとなりました。N予備校では React を使用してフロントエンドを構築しています。
自分は React の経験がなかったため、キャッチアップを目的としてナンプレアプリを Vue から React へ移行することとしました。
移行にあたって発生した作業
主に以下の作業を行いました。
- コアロジックのパッケージ化
- React でのアプリケーション構築
コアロジックのパッケージ化
ナンプレの問題を解く・生成するコアロジックをパッケージ化し、それ以外の部分を React で再構築することで React への移行を実現しました。
コアロジックは以下のリポジトリで管理しています。 https://github.com/ysk8hori/numberplace-generator
パッケージは GitHub Packages で公開しています。 https://github.com/ysk8hori/numberplace-generator/pkgs/npm/numberplace-generator
パッケージ化前から境界を設けていた
もともと、Vue のプロジェクトの中で以下のように境界を設け、依存が双方向にならないよう整理していました。
当時は、境界を設けることで「認知負荷の抑制」や「テスト容易性向上」などの効果を感じていました。そして、当時は実感を得られていませんでしたが、境界には区切られた部分のパッケージ化が容易となる効果もあります。
モノリスとして作っている段階では、境界で区切られたコンポーネントをパッケージ化する必要性も、フレームワークを乗り換える必要性も感じることはなかなかできません。しかしながら時間が経つと、技術の変遷や自身の成長がきっかけとなり、アーキテクチャやフレームワークを変更したくなるものです。
今回、Vue から React へ乗り換えることとなり、この効果を体感することができました。
パッケージを分けるだけでは不十分
境界を設けていたことによって、コアロジックのパッケージ化は簡単に行えましたが、「使用方法が難しい」という問題が残ってしまいました。というのも、問題の生成を行うために DI コンテナに何らかのインスタンスを登録したり、問題生成のためにいくつかのクラスを生成して使用する必要があったりと、凝った作りになっていました。生成した問題の構造も複雑でした。
このままでは、作った自分でも利用するのが難しい状態です。
そこで、関数を一つ呼び出すだけで問題が生成されるように変更しました。
import { generateGame } from "@ysk8hori/numberplace-generator"; // Generate standard 9x9 size number place game. const [puzzle, solved] = generateGame({ width: 3, height: 3 }); console.log(pazzle.toString()); /* 9, , , , , ,4, , 6, , ,4,9,2,3,5, 2, ,4, ,6,3, ,1,9 , , , , , ,5,6,2 5, , , , , , ,3, ,3, ,9, , ,7, ,1 7, , , ,5,6, ,9, ,2, ,3, , ,6,7, 3, , ,7, ,1,2,4, */ console.log(solved.toString()); /* 9,5,3,8,1,7,4,2,6 6,8,1,4,9,2,3,5,7 2,7,4,5,6,3,8,1,9 8,9,7,1,3,4,5,6,2 5,1,2,6,7,8,9,3,4 4,3,6,9,2,5,7,8,1 7,4,8,2,5,6,1,9,3 1,2,5,3,4,9,6,7,8 3,6,9,7,8,1,2,4,5 */
ちなみに返却される puzzle
や solved
の型は以下の Game
です。かなりシンプルかと思います。
/** Numberplace game. */ export type Game = { cells: Cell[]; toString: () => string; }; /** One cell. */ export type Cell = { /** Position of cell. */ pos: Position; /** Answer filled in cell. If not filled in, undefined. */ answer: undefined | string; }; /** Position of x and y. */ export type Position = Readonly<[number, number]>;
内部構造に詳しくなくても利用可能な IF とすべき
ですよね。
React でのアプリケーション構築
コアロジックはパッケージ化し流用することができましたが、UI やゲームの状態管理機能などは全て作り直しています 🤣
特筆すべきことはそんなにないのですが、あえて挙げれば...
Vue で作成した際には、ユーザーの操作やゲームの管理については Application という領域を設けて Vue と切り離して実装していたのですが、今回はそれをしませんでした。 というのも、今回は React のキャッチアップが目的だったので、できるだけ React の機能を使って作ろうと考えたためです。
機能追加
盤面の種類を増やした
もともと Vue で作っていた頃には、好きなサイズを指定して問題を生成&プレイ可能にしていました。 今回は複数のサイズに加え、盤面の対角線上のマスでも一意にする必要がある問題や、背景色の異なる四角いエリアでも一意にする必要がある問題を生成&プレイ可能としました。
マテリアルデザインのアイコンで遊べるようにした
同じドワンゴアドベントカレンダーで、ニコ生フロントエンドのミスケンさんが以下の記事を書かれていました。
そこで紹介されていたのが smart-svg というSass製SVG爆速表示ライブラリ。
自分が作っているナンプレアプリは各マスに表示する数字などにSVG画像を使用していましたので、早速使ってみました。 性能は測っていないのですが...使いやすかったので、ついでに数字の代わりにマテリアルデザインアイコンで遊べるようにしてみました。
改善点
改善した点をあげていきます。
UI をロックしない! そう、 WebWorker ならね!
問題生成処理は 1 スレッドで CPU パワーを使って行いますが、大きな問題だと問題が生成されるまでに数秒から十数秒、時には数十秒かかります。そして、何も考えずにブラウザで JavaScript を実行する場合は基本的にメインスレッド(UI スレッド)で動くので、問題生成を行うとその間 UI をロックしてしまいます。
そこで、今回は WebWorker を使用して別のスレッドで問題生成処理を行い、UI をロックしないよう改良しました。
vite を使用しているのですが、 Web Worker が非常に簡単に使えて体験が良かったです。
オフラインでも遊べる!
スマホにアプリとしてインストールして使いたかったのと、オフラインでも使用可能とするために PWA にしています。 ServiceWorker は何にも使用していませんが。
性能向上した
問題生成処理の性能を上げました。これにより、16×16 などの大きな盤面の問題の生成が可能となりました。良かったね(白目) とはいえ、生成にかなり時間を食うことがあります。また、その間CPUパワー使いまくるので、気になる方は諦めてください...。 まだまだ改善の余地があります...。
運用面の改善
リリース、テスト、ライブラリのアップデートなど、今後長くアプリケーションを運用していけるよう整えました。
CI 充実化
CI を充実させ、unit test の他、 Chromatic での VRT(Visual Regression Testing) も行うようにしています。また、タグを作成した際には自動で本番環境へデプロイします。開発体験がとても良いです。
タイミング | CI で行うこと |
---|---|
PR 作成 | 単体テスト storybook のビルド |
Develop → Main への PR 作成 | 単体テスト Chromatic での VRT |
リリースタグ作成 | 本番環境へのデプロイ |
なお、PR 作成時に storybook のビルドだけを行っていますが、どこかに公開したりはしていません。これは、 renovate によって storybook 関連ライブラリのアップデートが行われた際に storybook のビルドが失敗するようになった場合に検知できるようにしています。
Chromatic で VRT
Chromatic は割とすぐに無料枠を食いつぶしてしまうので、default ブランチからリリース専用のブランチへマージする PR でのみ VRT を行うようにしています。見た目が絶対に変わらないであろう PR や、コミットコメントの修正をコミットした際などに高価な VRT を行うのはもったいないですね。 Chromatic は最高です。
renovate によるライブラリの自動アップデート
Renovate を導入しライブラリの最新化が自動で行われるようにしています。CI が通ればデフォルトブランチへのマージを自動で行う設定としているので、ほとんど手間が必要ありません。Renovate 導入には CI でのテストを充実させる必要があります。 Renovate は最高です。
Sentry でエラーの監視を可能とした
Sentry を入れてみました。 自分と、自分の親戚のおじさんくらいしか使用していないのでそんなに意味はないですが・・・。
失敗談
Rust の WebAssembly にしようとして挫折した
性能向上のため Rust でも問題生成処理を作成したのですが、以下の理由により日の目を見ることはありませんでした。
WebAssembly で使用不可能な crate に依存していた
Rust で作ればなんでも WebAssembly として動かせるものと思い込んでいました・・・。 そうではなさそう。未だによくわかっていないので、また今度チャレンジするつもりです。
そんなに性能上がらなかった
なんなら若干遅いくらいでした。どうして・・・。 問題生成処理は割と単純な繰り返し処理が多いので JavaScript でも十分に性能が出るのかもしれません。
まとめ
Vue から React への移行においては、境界をうまく設けて、フレームワークやライブラリへの依存箇所を限定し、依存の方向を整理し疎結合な作りとしておくことで、アプリケーションの寿命が伸びるのだということを実感することができました。
また、業務で使う技術のキャッチアップや、逆に今は業務で使っていないが使えそうな技術のキャッチアップもできて、良い個人開発ができました。
来年はもう一度 Rust にチャレンジしたいな〜。