React + Vite + Tailwind CSSで「没入型タイマー」を自作する技術スタックと実装の全貌

Uncategorized

はじめに:生存戦略の裏側へ

先日、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 のユーティリティクラスを組み合わせることで実現しています。 重要なのは「背景の透過」と「ぼかし効果」のレイヤー構造です。

実装のポイント

  1. bg-white/80 (または dark:bg-gray-900/80): 背景色に不透明度を設定し、背後の要素(ブラーなど)を透けさせます。
  2. backdrop-blur-md: すりガラスのような「ぼかし」効果を背景に適用します。
  3. 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

この問題を解決するために、「経過時間を積算する」のではなく、「基準時刻(開始時刻)と現在時刻の差分を計算する」ロジックを採用しました。

  1. タイマー開始時 (start) に、現在のタイムスタンプ (Date.now()) から、すでに経過した時間 (prev) を引いた値を「基準開始時刻 (baseTime)」として記録します。
  2. 表示更新時 (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

コメント

タイトルとURLをコピーしました