家計簿アプリを作る #70:Cloudflare Pages への再デプロイ

家計簿アプリ作成シリーズの第70回です。よく使う取引のクイック登録機能をCloudflare Pagesへ再デプロイします。

デプロイ手順

transaction_templates テーブルのマイグレーションを本番のD1に適用してから、ビルド・デプロイを実行しました。

ハマりポイント:本番環境だけ日付が1日ずれる

テンプレートからのクイック登録は「今日の日付」で取引を作成する仕様でしたが、ローカルでは正しく動くのに、デプロイ後の本番環境だけ日付が1日前になってしまう不具合が発覚しました。

原因は、Cloudflare Workers(Cloudflare Pagesの実行基盤)がデフォルトでUTCタイムゾーンで動作することでした。ローカルのMacはJST(UTC+9)なので new Date() の年月日取得(getFullYear()など)はJST基準になりますが、本番のUTC環境では同じコードが日本時間の午前9時より前(UTCではまだ前日)にUTC基準の日付を返してしまい、1日前として保存されていました。

// ❌ 実行環境のローカルタイムゾーンに依存する
function formatLocalDate(date: Date): string {
  const year = date.getFullYear();
  const month = String(date.getMonth() + 1).padStart(2, '0');
  const day = String(date.getDate()).padStart(2, '0');
  return `${year}-${month}-${day}`;
}

Intl.DateTimeFormattimeZone: 'Asia/Tokyo' を明示することで、実行環境のタイムゾーンに関係なく常に日本時間で計算するように修正しました。

// ✅ 明示的にAsia/Tokyoを指定し、実行環境に依存しない
export function formatLocalDate(date: Date, format: 'day' | 'month' = 'day'): string {
  const options: Intl.DateTimeFormatOptions = format === 'month'
    ? { timeZone: 'Asia/Tokyo', year: 'numeric', month: '2-digit' }
    : { timeZone: 'Asia/Tokyo', year: 'numeric', month: '2-digit', day: '2-digit' };
  const formatter = new Intl.DateTimeFormat('sv-SE', options);
  return formatter.format(date);
}

'day'/'month' のフォーマット切り替えを引数で持たせることで、「今日の日付(YYYY-MM-DD)」だけでなく、定期収支の自動登録で使っている「今月(YYYY-MM)」の計算にも同じ関数を使い回せるようにしています。

「瞬間」を扱う値と「カレンダー上の日付」を扱う値の違い

この修正の過程で、「DBに保存されている updatedAt などのタイムスタンプはUTCのままでいいのか」という疑問も出てきました。結論は、UTCのままで問題ありません。

  • 「いつか(瞬間)」を記録する値(updatedAt など):タイムスタンプとしてUTCのまま保存し、表示する側でタイムゾーン変換すればよい
  • 「何月何日か(カレンダー上の日付)」を扱う値(取引のdateなど):タイムゾーンを明示して文字列に変換する必要がある

今回のバグは後者(カレンダー上の日付)を、実行環境依存の方法で計算してしまったことが原因でした。DBに保存する瞬時点のタイムスタンプと、人間が意識する「暦の上の日付」は性質が異なる、という整理が今回の学びでした。

まとめ

ポイント 内容
ローカルと本番のタイムゾーン差 Cloudflare Workersはデフォルト UTC。開発機(JST)と本番で結果が変わりうる
対処法 Intl.DateTimeFormattimeZoneを明示し、実行環境に依存しない計算にする
タイムスタンプとカレンダー日付 前者はUTC保存のままでよく、後者だけタイムゾーンを意識した変換が必要
← トップページに戻る