
グローバル・アプリに不可欠な時間とタイムゾーン処理
アプリケーションを限られた地域向けに作成するのは簡単ですが、世界中にサービスを提供することを目指すと、複雑さは大幅に増加します。例を挙げて説明しましょう。世界中で使用できるアプリを作成する計画があり、そのアプリにはユーザーのプロフィールページにアカウント作成時間を表示するという単純な機能があるとします。これは簡単な要件に思えますよね?しかし、ここに落とし穴があります。ユーザーは世界中のどこからでもアクセスする可能性があり、彼らのタイムゾーンに合わせてアカウント作成時間を表示するのが理想的ではないでしょうか?しかし、タイムゾーンに応じて時間を表示するには、ユーザーのタイムゾーンを特定し、保存し、それに応じて表示するというオーバーヘッドが必要になります。心配する必要はありません。この記事では、適切な計画とツールの選択によって、このような複雑なシナリオをどのように扱うことができるかを探ります。
背景
問題解決に入る前に、アプリ開発におけるタイムゾーンの複雑さについて話しましょう。私たちが住む世界には異なるタイムゾーンがあり、それぞれのタイムゾーンに応じて行動しています。これは事実です。タイムゾーンを理解していない方のために説明すると、タイムゾーンは国の境界に応じて時間を評価し保存するための指標です。簡単に言えば、ニューヨークで午前12時25分のとき、東京では午後1時25分になります。これは二つの国の距離と地理的位置の違いによるもので、それぞれの国に異なるタイムゾーンを生み出しています。では、異なるタイムゾーンをサポートするアプリを開発する際に、これが何を意味するのでしょうか?
ソフトウェアエンジニアの混沌それとも創意工夫?

問題の提起
問題がすでに想像できたかもしれませんが、まだの方のために説明します。時間管理(時間の保存と使用)による情報提供には、多くの移動するコンポーネントがある場合、考慮すべき細かな詳細が多数あります。時間の保存、ユーザーの位置に応じた時間の表示、異なるタイムゾーンでホストおよび消費されるフロントエンドとバックエンド間の通信など、あらゆる操作に時間が必要であり、2つのエンティティ間でできるだけ正確にすることは混沌としているように聞こえるかもしれません。では、問題をより良く理解するために、異なるシナリオに分解してみましょう。
異なるタイムゾーンの時間関連データの保存と処理
ユーザーのタイムゾーンに応じたフロントエンドでの時間関連データの表示と異なるタイムゾーン間の時間変換
時間差の計算と表示、および読みやすい形式での表示
フロントエンドが異なるレンダリングパラダイムを使用する場合の時間の処理
フロントエンドとバックエンド間の時間関連データの通信
異なるタイムゾーンでの定期的なイベントの処理
これらは、異なるタイムゾーンで作業する際に頻繁に遭遇する一般的なシナリオです。不可能に思えませんか?そう感じるのは、時間管理のための人類の発明であるUTCについて知らないからです。
UTCとは何か?🤔
協定世界時(UTC)は、時間を保存および表示するための指標で、異なる国のタイムゾーンを特定のオフセットを加減するだけで導き出すことができます。簡単に言えば、すべての国に共通の24時間の単一のタイムラインがあるだけです。特定の国の時間を知りたい場合は、単に特定のオフセットを加減するだけで時間を得ることができます。例えば、UTC午前0時(UTC 12:00 AM)は、カトマンズでは(午前5:45)となり、オフセットは(+5:45)で UTC+5:45 となります。同様に、他の国や地域にも適用されます。中央の時間管理ソースがあれば、問題解決がはるかに容易になりますよね?確かに、中央の変換可能な時間管理単位があることで問題解決に大きな違いをもたらしますが、完全に問題を排除するわけではありません。では、個々の問題を解決するためにUTCをどのように活用できるか見てみましょう。
1. 異なるタイムゾーンの時間関連データの保存と処理
UTCの力を理解したら、異なるタイムゾーンの時間関連データを保存および処理する必要がある状況で、それを最大限に活用できます。時間関連データをUTCで保存するという原則を作りましょう。なぜかと疑問に思うかもしれませんが、単純なオフセット操作で任意のタイムゾーンに変換できるからです。ISO 8601やRFC3339など、UTCで時間関連データを保存するいくつかのフォーマットがあります。UTCで時間が保存されるとき、オフセットも保存されるため、タイムゾーンの識別とそれに応じた操作が容易になります。データベースに時間を保存する適切なフォーマットを選択することも考慮すべき重要な点です。以下は、時間関連データを保存する際に使用できるいくつかのフォーマットです。
Oracle: UTCのタイムゾーンを保存できるTIMESTAMP WITH TIME ZONEデータ型を使用します。
SQL Server: UTCのタイムゾーンを保存できるDATETIMEOFFSETデータ型を使用します。
MySQL: タイムゾーンコンポーネントを含むことができるTIMESTAMPデータ型を使用します。
PostgreSQL: UTCのタイムゾーンを保存できるTIMESTAMP WITH TIME ZONEデータ型を使用します。
2. ユーザーのタイムゾーンに応じたフロントエンドでの時間関連データの表示と異なるタイムゾーン間の時間変換
UTCで時間関連データを保存したら、それを消費し、ユーザーの位置に応じて表示する必要があります。これは、フロントエンドでUTC時間をユーザーの時間に変換することで達成できます。タイムゾーン関連のパッケージはいくつかありますが、Vanilla JavaScriptを使用してUTC時間をユーザーの時間に変換することもできます。
// Date in UTC but RFC3339
const dateFromBackend = new Date("2023-04-16T12:30:15-05:00")
// If Local is not passed then it refers to my current local
// and returns result as per my timezone
const toMyTime = date.toLocaleDateString(undefined, {
hour: "2-digit",
hour12: false,
minute: "2-digit",
second: "2-digit",
});
console.log(
"Original date: ",
dateFromBackend,
" to My PC Local: ",
toMyTime
);
// Results:
// Original date:2023-04-16T12:30:15-05:00 to My PC Local:04/16/2023, 23:15:15
ここでは、日付クラスの組み込み関数を使用しています。この関数は2つの引数を取ります。最初のundefined値はロケールを表し、位置を表します。2番目の引数は、達成したい設定のオブジェクトです。ローカルを渡さない場合、デフォルトで自分のロケールが使用されるため、undefinedが渡されています。
3. 時間差の計算と表示、および読みやすい形式での表示
時間差を計算する際、私たちはそれをできるだけ読みやすくしたいと考えます。しかし、UTCタイムと自分のタイムゾーンの差をどのように計算するのか疑問に思うかもしれません。これは、まずUTCタイムを自分の時間に変換してから比較することで解決できます。以下は、人間にとって読みやすい時間差を表示するためのVanilla JavaScriptの実装例です。
// Date in UTC but RFC3339
const dateFromBackend = new Date("2023-04-16T12:30:15-05:00")
const TIME_DATA = [
{ units: "seconds", value: 60 },
{ units: "minutes", value: 60 },
{ units: "hours", value: 24 },
{ units: "days", value: 7 },
{ units: "weeks", value: 4.34524 },
{ units: "months", value: 12 },
{ units: "years", value: Number.POSITIVE_INFINITY },
];
const fromNow = (
date: Date = new Date(),
// @ts-ignore
fmt: Intl.RelativeTimeFormat = new Intl.RelativeTimeFormat(undefined, {
numeric: "auto",
})
): string | undefined => {
// Current TimeStamp
const cDate = new Date() as any;
// Getting time differences in milliseconds
let inMs: number = ((date as any) - cDate) / 1e3;
for (const interval of TIME_DATA) {
if (Math.abs(inMs) < interval.value) {
return fmt.format(
Math.round(inMs),
// @ts-ignore
interval.units as Intl.RelativeTimeFormatUnit
);
}
inMs /= interval.value;
}
};
const time = fromNow(new Date(dateFromBackend));
console.log(
"Original date: ",
dateFromBackend,
" to human-readable format: ",
time
);
// Results:
// Original date:2023-04-16T12:30:15-05:00 to human-readable format: last month
ここでは、UTCをユーザーの時間に変換し、ミリ秒単位での差を評価して、人間にとって分かりやすい方法で差を算出するために、組み込みのIntl(国際化)関数を使用しています。しかし、単に差だけを求めたい場合は、以下のような実装になります。
// Date in UTC but RFC3339
const dateFromBackend = new Date("2023-04-16T12:30:15-05:00")
const tmpTime = dateFromBackend;
const currentTime = new Date() as any;
const msAgo = currentTime - tmpTime;
const secsAgo = Number(Math.round(msAgo / 1e3).toFixed(1));
const minAgo = Number(Math.round(msAgo / 6e4).toFixed(1));
const hrAgo = Number(Math.round(msAgo / 36e5).toFixed(1));
const dayAgo = Number(Math.round(msAgo / 864e5).toFixed(1));
const weeksAgo = Number(Math.round(dayAgo / 7).toFixed(2));
const yearAgo = new Date().getFullYear() - dateFromBackend.getFullYear();
const monthsAgo =
yearAgo <= 1
? new Date().getMonth() - inputTime.getMonth()
: Math.round(
yearAgo * 12 -
(new Date().getMonth() - inputTime.getMonth())
);
const differences = {
msAgo,
secsAgo,
minAgo,
hrAgo,
dayAgo,
weeksAgo,
monthsAgo,
yearAgo,
};
console.log(
"Original date: ",
dateFromBackend,
" to differences in time: ",
differences
);
// Results:
/*
Original date: 2023-04-16T12:30:15-05:00 to differences in time: {
msAgo: 3157242096,
secsAgo: 3157242,
minAgo: 52621,
hrAgo: 877,
dayAgo: 37,
weeksAgo: 5,
monthsAgo: 1,
yearAgo: 0
}
*/
JavaScriptでは、日付の値をDateクラスのインスタンスに渡すと、自動的にUTC時間に変換されます。ここでは、同じタイムゾーンの2つの時間を比較することで結果を得ています。
4. フロントエンドが異なるレンダリングパラダイムを使用する場合の時間の扱い
サーバーサイドレンダリング(SSR)を使用する場合、時間関連のデータに特別な注意を払う必要がある状況に遭遇することがあります。サーバーでの値がハイドレーション中に異なるため、ハイドレーションの問題が発生する可能性があります。この問題の一般的な解決策はクライアントサイドレンダリング(CSR)を使用することですが、それが使用できない場合は別の方法があります。サーバーでUTC時間でレンダリングし、CSSを使用して(display: none)で非表示にしつつレイアウトに存在させ、累積レイアウトシフト(CLS)を無視します。レンダリング後にユーザーのタイムゾーンに応じて時間の値を更新し、表示させます。この方法で、ハイドレーションの問題とCLSの両方を防ぐことができます。
5. フロントエンドとバックエンド間の時間関連データの通信
UTCの適切な概念を理解すれば、フロントエンドとバックエンド間で時間関連データを通信する標準としてUTCを使用するのが非常に簡単になります。JavaScriptではnew Date().toISOString()関数を使用して、フロントエンドで簡単にUTC日付を生成できます。これは、ほとんどのデータベースと言語と互換性のあるISO 8601形式でUTCデータを返します。しかし、バックエンドからデータを取得する際も、時間関連データをUTCで送信する必要があります。ほとんどのバックエンドはデフォルトでISO 8601形式をサポートしていませんが、ISO 8601形式のサブセットであるRFC3339形式をサポートしています。要するに、バックエンドがISO 8601形式またはRFC3339形式で日付を提供できれば問題ありません。
6. 異なるタイムゾーンでの定期的なイベントの処理
定期的なイベントを理解するために、例を挙げてみましょう。10回分の連続する月曜日の日時値を生成し、10個の異なる日付値を得る必要があるアプリに取り組んでいるとします。定期的なイベントを作成してフロントエンドに表示する必要がある状況や、さらに複数の地域をサポートする必要がある状況では、より複雑になります。すべてを一から作成するのは混沌としていますが、幸いにもRRuleというNPMパッケージがあり、定期的なイベントの作成を容易にします。UTCとRRuleパッケージを使用すれば、タイムゾーンの問題に対応しやすくなります。
補足
すべてのシナリオをバニラJavaScriptで実装するのは面倒ですが、問題をより深く理解するのに役立ちます。こちらのリポジトリでは、パッケージを使用する場合と使用しない場合の複数の異なるシナリオを実装しようと試みています。タイムゾーンとその使用例について、最小限のパッケージであるDayJSを使用しています。
GitHub - itSubeDibesh/Timezone-Usecase
タイムゾーンを扱う際に従うべき基準 ✅
タイムゾーンを深く理解した後、タイムゾーンを扱う際に役立つ一連の基準があります。
💻 バックエンドのアクション
日付と時間関連のデータをUTCで保存する
日付と時間関連のデータをISO 8601またはRFC3339形式で送信する
💻 フロントエンドのアクション
UTC時間に基づいてユーザーのローカル時間を表示する
バックエンドに時間関連のデータを送信する必要がある場合はnew Date().toISOString()を使用する
開発ツールのSensorsを使用して、異なるタイムゾーンでの時間データをテストする
SSRを使用している場合は、UTC時間でレンダリングし、CLSに注意しながら読み込み時に時間関連のデータを更新することを確認する
国際化サポートを備えた最小限の日時ライブラリを使用する
UTCの主要な使用例を理解すれば、これらの基準は単にアプリケーションが異なるタイムゾーンでスムーズに動作することを確認するための追加事項に過ぎません。タイムゾーンとその複雑さについて簡単に理解していただけたと思います。もし私が見落としたことがあれば、遠慮なくコメントしてください。
参考文献
この記事は、2023年5月に弊社のエンジニア Dibesh Raj Subedi が執筆した内容を日本語に翻訳したものです。
英語版はこちらをご覧ください。
https://articles.wesionary.team/timezone-and-its-complication-on-web-apps-fd465919e611
採用情報
私たちはプロダクト共創の仕組み化に取り組んでいます。プロダクト共創をリードするプロダクト・マネージャー、そして、私たちのビジョンを市場に届ける営業メンバーを募集しています!
開発パートナーをお探しの企業様へ
弊社は、グローバル開発のメリットを活かし、高い費用対効果と品質を両立しています。経験豊富で多様性のあるチームが、課題を正しく理解し、最適なシステムと優れた体験を実現します。業務システムの開発、新規事業の開発、業務効率化やDX化に関するお困りごと、ぜひ弊社にご相談ください。