JavaScriptを使ったラジオ体操選択アプリの作成記録
開発記:ラジオ体操継続支援アプリ
~LocalStorageからIndexedDBへ、そして「空白期間スキャン」による精密な習慣化ロジックの構築~
JavaScript講義の最終課題として、実施するラジオ体操を選択し、その動画を再生するプロダクトを作成しました。本プロダクトがどのような変遷を経て現在の形に至ったのかまとめました。
1. プロダクトの作成遍歴
記録システムの構築と「公平性」へのこだわり
開発の第一歩は、「第1体操と第2体操を偏りなく、公平に行いたい」という個人的なニーズから始まりました。まず、実施回数を保存する記録システムが必要となり、手軽に導入できる localStorage を採用しました。この段階で、過去の履歴を参照して実施回数が少ない方の体操を優先的に選ぶ「公平な選択ロジック(selectExercise)」が完成しました。
視覚的フィードバックと機能追加
次に、継続のモチベーションを高めるため、カレンダー上で実施日がひと目で分かるよう以下の機能を追加し、日々の努力を数値化できるようにしました。
- アニメーション(✔マークのスタンプ効果)
- 継続日数(ストリーク)
- 総実施回数の表示
データの複雑化とIndexedDBへの転換
機能が増えるにつれ、保存すべきデータ構造が複雑化していきました。localStorage は文字列しか保存できず、容量制限(約5MB)や保存時の画面フリーズ(同期処理)といった課題に直面しました。そこで、大容量の構造化データを扱え、バックグラウンドで処理が可能な IndexedDB への移行を決断しました。
「除外日」設定と判定ロジックの苦闘
学校生活のサイクルに合わせるため、土日祝日や長期休暇など、運動が実施されない日を「除外日」として設定する機能が必要になりました。最初は、今日から「昨日」へ遡り、休みであればさらに遡るという**「遡り方式」**のロジックで継続日数を計算していました。しかし、この方式には致命的な問題がありました。「除外日(土日など)に自主的に実施した場合」や、後から「除外日の設定を変更した場合」に、継続記録が正しくカウントされない、あるいはリセットされてしまう不整合が発生したのです。
最終的な解決:全記録スキャン方式への刷新
この問題を解決するため、ロジックを根本から見直しました。新しいロジック(calculateStreak)では、「前回実施した日から今日まで」の空白期間を1日ずつスキャンし、その間に「実施すべき平日にサボった日」があるかを確認する方式に変更しました。また、休暇設定が変更されるたびに、IndexedDB内の全実施記録を一度 JavaScriptの Set オブジェクト に展開し、重複を排除した上で継続日数をゼロから再計算するようにしました。これにより、いかなる状況下でも「やるべき日に継続できているか」を正確に算出できるようになりました。
2. 開発で最も苦労した点:非同期処理のコントロール
本アプリの開発で最大の技術的難所は、IndexedDB特有の**「非同期処理の実行順序の制御」**でした。
「Data may be stale(データが古い)」問題との戦い
IndexedDBは「保存をお願いする」と、結果を待たずに次のプログラムが進んでしまいます。初期の開発では、**「記録の保存ロジックが完了する前に、ユーザーが再度体操ボタンを押したり画面を更新したりすると、直前の記録が反映されない(上書きされる)」**という問題が頻発しました。これは、ブラウザ側から見れば「古いデータに基づいた計算結果(Stale Data)」を保存しようとしている状態でした。
コールバックの連鎖による整合性の確保
この「ちぐはぐな状態」を防ぐため、コールバック関数を厳格に入れ子にする設計を徹底しました。具体的には、以下の順序を絶対に崩さないように実装しています。
- データの保存(
saveDailyExercise)を実行し、DBからの成功通知(onsuccess)を待つ - 保存完了の合図を受け取ってから、初めて継続日数の再計算(
calculateStreak)を開始する - 計算が終わった後に、最終的な画面の再描画(
updateUI)を行う
あえてモダンな構文(Promise / async / await)を使わず、ブラウザ標準の**イベント駆動型(コールバック方式)**を愚直に組み合わせることで、データの読み書きが完全に終わるまで次のステップへ進ませない堅牢なフローを構築しました。この工夫により、連続して操作を行ってもデータの整合性が崩れない、信頼性の高いシステムを実現できました。
使用した技術
| 技術 | 用途 |
|---|---|
| Vanilla JavaScript | ライブラリに頼らず、ロジックをゼロから構築 |
| IndexedDB | イベントモデルに基づいた非同期データ管理 |
| Set Object | 数千件の実施記録から特定の日付を高速に検索するために活用 |
| CSS Keyframes |
cubic-bezier を使用したスタンプ弾む演出 |
この遍歴を通じて、技術的な正しさだけでなく、ユーザーがいかに「挫折感」を抱かずに使い続けられるかというUX設計の重要性を学びました。
Discussion