🌍

タイムゾーン処理のベストプラクティス

に公開

📋 概要

このドキュメントでは、アプリにおけるタイムゾーン処理のベストプラクティスを説明します。将来的な海外展開を見据えた実装方針です。

🌍 基本的な方針

1. データベースには UTC 時間で保存

原則:すべてのタイムスタンプは UTC 時間で保存する

  • データベースのカラム型はTIMESTAMPTZ(タイムゾーン付きタイムスタンプ)を使用
  • PostgreSQL のNOW()関数は自動的に UTC 時間を返す
  • タイムゾーン情報はデータベースに保存しない
-- ✅ 正しい実装
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
// ✅ 正しい実装(UTC時間で保存)
updateData.completed_at = new Date().toISOString();

2. フロントエンドでローカル時間に変換して表示

原則:表示時のみユーザーのローカルタイムゾーンに変換する

  • データベースから取得した UTC 時間の文字列を、表示時にローカル時間に変換
  • toLocaleString()やtoLocaleDateString()を使用してローカル時間で表示
  • ユーザーのデバイスのタイムゾーン設定を自動的に使用
// ✅ 正しい実装
const date = new Date("2024-01-01T00:00:00Z"); // UTC時間
date.toLocaleString("ja-JP"); // ローカル時間で表示(日本時間の場合:2024/1/1 9:00:00)

3. 日付のみを扱う場合は注意が必要

原則:日付のみを扱う場合は、ユーザーのローカルタイムゾーンの日付として扱う

  • due_dateなどの日付のみのフィールドは、YYYY-MM-DD形式で保存
  • 時刻を含めないことで、タイムゾーンの影響を回避
  • 表示時は、ローカル時間の日付として解釈
// ✅ 正しい実装(日付のみ)
const dueDate = "2024-01-01"; // YYYY-MM-DD形式
const [year, month, day] = dueDate.split("-").map(Number);
const date = new Date(year, month - 1, day); // ローカル時間として解釈

4. タイムゾーン情報はデバイスから取得

原則:ユーザーのタイムゾーン情報は保存せず、デバイスの設定を使用

  • データベースにユーザーのタイムゾーン情報を保存しない
  • JavaScript のDateオブジェクトは自動的にデバイスのタイムゾーンを使用
  • 海外展開時も、ユーザーのデバイス設定に従って表示される

🔧 実装パターン

パターン 1: タイムスタンプの表示(created_at、updated_at など)

import { formatTimestamp } from "../utils/dateUtils";

// UTC時間のタイムスタンプをローカル時間で表示
formatTimestamp(profile.created_at);
// → "2024年1月1日 9:00"

// 日付のみ表示
formatTimestampDateOnly(profile.created_at);
// → "2024年1月1日"

パターン 2: 日付のみの表示(due_date など)

import { formatDateOnly } from "../utils/dateUtils";

// YYYY-MM-DD形式の文字列をローカル時間の日付として表示
formatDateOnly(task.due_date);
// → "2024年1月1日"

パターン 3: 日付の比較(グラフなど)

// 日付キー(YYYY-MM-DD)で比較することで、タイムゾーンの影響を回避
const formatDateKey = (date: Date) => {
  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}`;
};

const dateKey = formatDateKey(new Date(insight.created_at));

🚫 避けるべき実装

❌ タイムゾーンをハードコードする

// ❌ 悪い実装
const date = new Date(profile.created_at);
date.setHours(date.getHours() + 9); // 日本時間(UTC+9)をハードコード

❌ toISOString()でローカル時間を保存する

// ❌ 悪い実装(タイムゾーンのずれが発生)
const localDate = new Date();
dueDate = localDate.toISOString(); // UTC時間に変換されてしまう

❌ タイムゾーン情報をデータベースに保存する

-- ❌ 悪い実装
ALTER TABLE profiles ADD COLUMN timezone TEXT; -- 不要

📊 データフロー

ユーザーがアクションを実行
    ↓
フロントエンド: new Date()でローカル時間のDateオブジェクトを作成
    ↓
データベース: toISOString()でUTC時間の文字列に変換して保存
    ↓
データベース: TIMESTAMPTZ型でUTC時間として保存
    ↓
フロントエンド: データベースからUTC時間の文字列を取得
    ↓
フロントエンド: new Date(dateString)でDateオブジェクトに変換(UTC時間として解釈)
    ↓
フロントエンド: toLocaleString()でローカル時間に変換して表示

🌐 海外展開時の考慮事項

1. 言語設定

  • toLocaleString("ja-JP")のように言語コードを指定している箇所は、将来的にユーザーの言語設定に応じて動的に変更する必要がある
// 将来的な実装例
const locale = getUserLocale(); // "ja-JP", "en-US", etc.
date.toLocaleString(locale);

2. 日付フォーマット

  • 日本の「2024 年 1 月 1 日」形式と、アメリカの「January 1, 2024」形式など、地域によって異なる
  • Intl.DateTimeFormatを使用することで、自動的に適切な形式で表示される

3. 週の始まり

  • 日本では週の始まりは月曜日、アメリカでは日曜日
  • カレンダー表示などで週の始まりを扱う場合は、Intl.DateTimeFormatのオプションを使用

4. 夏時間(DST)

  • アメリカなどでは夏時間が存在するが、JavaScript のDateオブジェクトが自動的に処理するため、特別な対応は不要
  • データベースのTIMESTAMPTZ型も自動的に処理する

🔗 参考資料

📲 僕の作ったアプリ(宣伝)

Tanao – 家計簿いらずの資産トラッカーアプリ - App Store
https://apps.apple.com/jp/app/tanao-家計簿いらずの資産トラッカー/id6753191685

Discussion