📋 概要
このドキュメントでは、アプリにおけるタイムゾーン処理のベストプラクティスを説明します。将来的な海外展開を見据えた実装方針です。
🌍 基本的な方針
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-%E5%AE%B6%E8%A8%88%E7%B0%BF%E3%81%84%E3%82%89%E3%81%9A%E3%81%AE%E8%B3%87%E7%94%A3%E3%83%88%E3%83%A9%E3%83%83%E3%82%AB%E3%83%BC/id6753191685