はじめに:生存戦略の裏側へ
先日、noteにて「身体が動かなくなる恐怖に勝つため、人生のコックピットを自作した話」を公開しました。 あちらでは開発の背景にある「ストーリー」を綴りましたが、本記事ではその裏側にある「技術(エンジニアリング)」を解剖します。
感情は抜き。コードとロジックで語る、技術解説編です。
技術選定の理由(Architecture)
React (Vite) を選んだ理由
単純なタイマーアプリであれば Vanilla JS でも可能ですが、以下の理由から React + Vite を採用しました。
- コンポーネント指向のUI構築: 「タイマー画面」「履歴テーブル」「統計カード」など、独立した機能単位でUIを管理し、再利用性を高めるため。
- 動的な状態管理 (State Management): タイマーのカウント、履歴データの追加・削除、ダークモードの切り替えなど、ユーザー操作に応じてリアルタイムに変化するUIの状態を
useStateやuseEffectで宣言的に記述するため。Vanilla JSでの命令的なDOM操作は、機能追加に伴い複雑化しやすいため避けました。
PWA (Progressive Web App) を選んだ理由
当初はネイティブアプリ (Swift/Kotlin) も検討しましたが、PWA が最適解でした。
- 開発スピードとマルチプラットフォーム: Web技術だけで iOS/Android 両方に対応でき、個別のコードベースを維持する必要がありません。
- App Store審査の回避: 個人の健康管理ツールとして迅速に改善サイクルを回したかったため、審査待ちやリジェクトのリスクがないWebデプロイを選択しました。
- ホーム画面への追加: Webアプリでありながら、インストールすることでネイティブアプリ同様にホーム画面から起動し、全画面表示(スタンドアロンモード)で使用可能です。
Data Persistence: LocalStorage の採用
バックエンドやデータベース(Firebase等)を使わず、ブラウザの LocalStorage を採用しました。
- プライバシー・バイ・デザイン: 個人の行動記録というセンシティブなデータをサーバーに送信せず、ユーザー自身のデバイス内に完結させることで、絶対的なプライバシーを保証します。
- オフライン動作: 通信環境がない場所でも、アプリの全機能を利用可能です。
- 実装のシンプルさ: 認証機能等が不要になり、アプリを軽量かつ高速に保つことができます。
UI実装:Glassmorphism(すりガラス)の魔術
このアプリのデザイン言語である「Glassmorphism」は、Tailwind CSS のユーティリティクラスを組み合わせることで実現しています。 重要なのは「背景の透過」と「ぼかし効果」のレイヤー構造です。

実装のポイント
bg-white/80(またはdark:bg-gray-900/80): 背景色に不透明度を設定し、背後の要素(ブラーなど)を透けさせます。backdrop-blur-md: すりガラスのような「ぼかし」効果を背景に適用します。border-white/10: 非常に薄い境界線を追加し、ガラスの「厚み」や「エッジ」を表現します。
コードスニペット (Header Component)
<header className="
p-4
border-b border-gray-200 dark:border-white/10 /* ガラスのエッジ */
bg-white/80 dark:bg-gray-900/80 /* 半透明の背景 */
backdrop-blur-md /* すりガラス効果 */
sticky top-0 z-50 /* スクロール追従 */
transition-colors duration-300
text-gray-900 dark:text-white
">
{/* Header Content */}
</header>
ロジック:iPhoneのSafari対策 (Time Management)
課題: バックグラウンドでのタイマー停止
iOS Safariなどのモバイルブラウザでは、バッテリー節約のために、画面がオフになったりバックグラウンドに回ると JavaScript の setInterval の実行頻度が極端に落ちたり、完全に停止したりします。そのため、単純に「1秒ごとにカウンタを+1する」実装では時間が大幅にズレてしまいます。
解決策: Timestamp Delta Calculation
この問題を解決するために、「経過時間を積算する」のではなく、「基準時刻(開始時刻)と現在時刻の差分を計算する」ロジックを採用しました。
- タイマー開始時 (
start) に、現在のタイムスタンプ (Date.now()) から、すでに経過した時間 (prev) を引いた値を「基準開始時刻 (baseTime)」として記録します。 - 表示更新時 (
updateTime) には、その時点のDate.now()とbaseTimeの差分を計算します。
これにより、途中でスクリプトが停止しても、再開時にその時点の Date.now() を参照するため、正確な経過時間が算出されます。
コードスニペット (Timer Hook)
const start = useCallback(() => {
setTime(prev => {
const now = Date.now()
// 以前の経過時間を考慮して「本来の開始時刻」を逆算
const baseTime = now - prev
startTimeRef.current = baseTime
// LocalStorage等に永続化
return prev
})
setIsRunning(true)
}, [])
const updateTime = useCallback(() => {
if (startTimeRef.current) {
const now = Date.now()
// 現在時刻 - 基準開始時刻 = 正確な経過時間
const elapsed = now - startTimeRef.current
setTime(elapsed)
return elapsed
}
return 0
}, [])
デプロイ環境: Vercel
フロントエンドホスティングには Vercel を採用しています。 GitHubリポジトリと連携させることで、mainブランチへのプッシュをトリガーに自動的にビルドとデプロイ(CI/CD)が走ります。
- ゼロコンフィグ: Viteプロジェクトを自動検出し、最適な設定でビルドしてくれます。
- プレビューデプロイ: Pull Requestごとにプレビュー環境が作成されるため、UI修正の確認が容易です。
- エッジネットワーク: グローバルCDNにより、PWAとしての初期ロードも高速です。
まとめとリポジトリ
今回採用した React + Vite + Tailwind CSS の構成は、個人の生存戦略ツールとして非常に「コスパの良い」選択でした。
複雑なステート管理を React に任せつつ、PWA としてスマホのホーム画面に常駐させる。この「自分だけのコックピット」を作る体験は、エンジニアとして純粋に楽しいものです。
もしこのコードが参考になったら、GitHubでスター(⭐️)を押していただけると励みになります。
📂 GitHub Repository: Recording Effort

PyKataRyst(パイ・カタリスト)
元バイオ研究者(Ph.D.) → 現役Pythonエンジニア。
難病により筋肉は減り続けていますが、代わりに「コード(Python)」と「拡張脳(AI)」を鍛えています。
身体を使わずに稼ぎ、英語で世界を広げるための「人生の実験ログ」を公開中。 休憩時間は大谷翔平のニュースとハリポタで英語勉強を兼ねた推し活をしています。
📺 YouTube: @PyKataRyst/🐦 X(Twitter): @pykataryst/📂 GitHub: @PyKataRyst


コメント