Skip to main content

클라이언트 앱 타임존 처리 가이드

작성일: 2026-04-06 Last Updated: 2026-04-06 대상: Browser Extension, Tauri Desktop App, Flutter Mobile App


1. 핵심 원칙: UTC 저장, 로컬 표시

레이어포맷예시
서버 저장/APIUTC (ISO 8601)2026-04-06T03:00:00Z
클라이언트 전송UTC (ISO 8601)2026-04-06T03:00:00Z
UI 표시사용자 로컬 타임존2026년 4월 6일 오후 12:00 (KST)
로컬 저장소UTC2026-04-06T03:00:00Z

절대 하지 말 것

금지 사항이유
서버에 로컬 시간 저장타임존 정보 손실, DST 변경 시 오류
타임존 오프셋만 저장 (+09:00)DST 적용 불가, IANA 타임존명 필요
클라이언트에서 UTC 변환 없이 API 전송서버 측 일관성 깨짐
Date.now() 결과를 문자열로 저장밀리초 타임스탬프는 디버깅 어려움
타임존 직접 계산 (오프셋 수동 계산)DST, 역사적 변경 미반영

2. 타임존 감지

자동 감지 우선순위

순위소스설명
1사용자 명시 설정앱 설정에서 직접 선택한 타임존
2서버 사용자 프로필백엔드에 저장된 타임존
3OS/브라우저 타임존시스템 자동 감지
4기본값UTC (절대 특정 지역 타임존을 기본값으로 사용하지 않음)

플랫폼별 감지 API

플랫폼감지 방법반환값 예시
Browser ExtensionIntl.DateTimeFormat().resolvedOptions().timeZone"Asia/Seoul"
Tauri (프론트엔드)Intl.DateTimeFormat().resolvedOptions().timeZone"Asia/Seoul"
Tauri (Rust)iana_time_zone::get_timezone()"Asia/Seoul"
FlutterDateTime.now().timeZoneName"KST"
Flutter (권장)timezone 패키지 + tz.local.name"Asia/Seoul"

구현 예시

// TypeScript (Browser Ext, Tauri 프론트엔드)
function detectTimezone(): string {
// 1. 사용자 설정
const saved = localStorage.getItem('user_timezone');
if (saved && isValidTimezone(saved)) return saved;

// 2. 서버 프로필 (캐시)
const profile = getCachedUserProfile();
if (profile?.timezone && isValidTimezone(profile.timezone)) {
return profile.timezone;
}

// 3. OS/브라우저
try {
return Intl.DateTimeFormat().resolvedOptions().timeZone;
} catch {
// 4. 기본값
return 'UTC';
}
}

function isValidTimezone(tz: string): boolean {
try {
Intl.DateTimeFormat(undefined, { timeZone: tz });
return true;
} catch {
return false;
}
}
// Flutter
import 'package:timezone/timezone.dart' as tz;

String detectTimezone() {
// 1. 사용자 설정
final saved = prefs.getString('user_timezone');
if (saved != null && _isValidTimezone(saved)) return saved;

// 2. 서버 프로필
final profile = getCachedUserProfile();
if (profile?.timezone != null) return profile!.timezone!;

// 3. OS 타임존
return tz.local.name; // "Asia/Seoul"
}

3. 로컬 표시 변환 패턴

TypeScript (Browser Ext, Tauri)

// UTC -> 로컬 표시
function formatDateTime(
utcString: string,
locale: string = 'ko-KR',
timezone?: string
): string {
const date = new Date(utcString);
return new Intl.DateTimeFormat(locale, {
timeZone: timezone || detectTimezone(),
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
}).format(date);
}

// 로컬 입력 -> UTC 변환 (API 전송용)
function toUTC(localDate: Date): string {
return localDate.toISOString(); // 항상 UTC ISO 8601
}

// 사용 예시
formatDateTime('2026-04-06T03:00:00Z');
// "2026년 4월 6일 오후 12:00" (KST 기준)

Flutter (Dart)

import 'package:intl/intl.dart';
import 'package:timezone/timezone.dart' as tz;

// UTC -> 로컬 표시
String formatDateTime(String utcString, {String? timezone}) {
final utcDate = DateTime.parse(utcString);
final location = tz.getLocation(timezone ?? detectTimezone());
final localDate = tz.TZDateTime.from(utcDate, location);

return DateFormat.yMMMMd('ko').add_Hm().format(localDate);
// "2026년 4월 6일 12:00"
}

// 로컬 입력 -> UTC 변환
String toUTC(DateTime localDate) {
return localDate.toUtc().toIso8601String();
}

포맷 프리셋

프리셋용도한국어 예시영어 예시
dateShort목록, 테이블2026.04.0604/06/2026
dateLong상세 보기2026년 4월 6일April 6, 2026
dateTime전체 날짜시간2026년 4월 6일 오후 12:00Apr 6, 2026, 12:00 PM
timeOnly시간만오후 12:0012:00 PM
relative상대 시간3시간 전3 hours ago
range기간4월 1일 ~ 4월 6일Apr 1 - Apr 6

4. 타임존 선택 UI

설계 원칙

원칙설명
검색 가능IANA 타임존명 및 도시명으로 검색
오프셋 표시(UTC+09:00) Asia/Seoul 형태
자동 감지 옵션"시스템 설정 따르기" 기본 선택
인기 타임존 상단사용자 지역 기반 추천
변경 즉시 적용미리보기로 현재 시간 표시

권장 타임존 목록 (상위 표시)

const POPULAR_TIMEZONES = [
{ value: 'auto', label: '시스템 설정 따르기' },
{ value: 'Asia/Seoul', label: '(UTC+09:00) 서울' },
{ value: 'Asia/Tokyo', label: '(UTC+09:00) 도쿄' },
{ value: 'Asia/Shanghai', label: '(UTC+08:00) 베이징/상하이' },
{ value: 'America/New_York', label: '(UTC-05:00) 뉴욕 (동부)' },
{ value: 'America/Los_Angeles', label: '(UTC-08:00) 로스앤젤레스 (태평양)' },
{ value: 'America/Chicago', label: '(UTC-06:00) 시카고 (중부)' },
{ value: 'Europe/London', label: '(UTC+00:00) 런던' },
{ value: 'Europe/Paris', label: '(UTC+01:00) 파리/베를린' },
{ value: 'Australia/Sydney', label: '(UTC+11:00) 시드니' },
{ value: 'Pacific/Auckland', label: '(UTC+13:00) 오클랜드' },
{ value: 'UTC', label: '(UTC+00:00) UTC' },
];

오프셋 값은 DST에 따라 변동되므로, 표시용으로만 사용하고 저장은 IANA 이름으로 한다.


5. 상대 시간 표시

규칙

경과 시간표시 형식예시
< 1분"방금 전"방금 전
< 1시간"N분 전"5분 전
< 24시간"N시간 전"3시간 전
< 7일"N일 전"2일 전
< 1년절대 날짜 (월/일)3월 15일
>= 1년절대 날짜 (년/월/일)2025년 3월 15일

구현

// Intl.RelativeTimeFormat (모던 브라우저)
function relativeTime(utcString: string, locale = 'ko'): string {
const now = Date.now();
const target = new Date(utcString).getTime();
const diffMs = target - now;
const diffSec = Math.round(diffMs / 1000);
const diffMin = Math.round(diffSec / 60);
const diffHour = Math.round(diffMin / 60);
const diffDay = Math.round(diffHour / 24);

const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });

if (Math.abs(diffSec) < 60) return rtf.format(0, 'second'); // "방금 전"
if (Math.abs(diffMin) < 60) return rtf.format(diffMin, 'minute');
if (Math.abs(diffHour) < 24) return rtf.format(diffHour, 'hour');
if (Math.abs(diffDay) < 7) return rtf.format(diffDay, 'day');

// 7일 이상: 절대 날짜
return formatDateTime(utcString, locale);
}
// Flutter - timeago 패키지 또는 직접 구현
import 'package:timeago/timeago.dart' as timeago;

String relativeTime(String utcString) {
final date = DateTime.parse(utcString);
final diff = DateTime.now().toUtc().difference(date);

if (diff.inDays > 7) {
return DateFormat.MMMd('ko').format(date.toLocal());
}

return timeago.format(date, locale: 'ko');
}

실시간 갱신

경과 시간갱신 주기
< 1분10초
< 1시간1분
< 24시간1시간 (또는 표시 시 1회)
>= 24시간갱신 불필요 (절대 날짜)

6. DST(서머타임) 처리

DST가 적용되는 주요 타임존

타임존표준DST전환 시기
America/New_YorkEST (UTC-5)EDT (UTC-4)3월 둘째 일요일 ~ 11월 첫째 일요일
America/Los_AngelesPST (UTC-8)PDT (UTC-7)동일
Europe/LondonGMT (UTC+0)BST (UTC+1)3월 마지막 일요일 ~ 10월 마지막 일요일
Europe/ParisCET (UTC+1)CEST (UTC+2)동일
Australia/SydneyAEST (UTC+10)AEDT (UTC+11)10월 첫째 일요일 ~ 4월 첫째 일요일

Asia/Seoul, Asia/Tokyo: DST 미적용 (현재)

DST 처리 규칙

규칙설명
IANA 타임존 사용America/New_York이지 EST가 아님
라이브러리에 위임절대 오프셋 직접 계산 금지
TZDB 최신 유지타임존 데이터베이스 정기 업데이트
전환 시점 테스트DST 전환일 전후 경계 테스트
반복 일정 주의"매일 오후 2시"가 DST 전환 시 UTC 기준 변경됨

DST 전환 시 주의 사항

// 문제: DST 전환일에 "존재하지 않는 시간" 또는 "중복 시간" 발생

// 예: 미국 동부, 3월 둘째 일요일 2:00 AM
// 1:59 AM EST -> 3:00 AM EDT (2:00~2:59는 존재하지 않음)

// 예: 미국 동부, 11월 첫째 일요일 2:00 AM
// 1:59 AM EDT -> 1:00 AM EST (1:00~1:59가 두 번 발생)

// 해결: Temporal API (미래) 또는 라이브러리가 자동 처리
// UTC 기준으로만 저장하면 이 문제 회피 가능

7. 플랫폼별 타임존 라이브러리

Browser Extension / Tauri (프론트엔드)

라이브러리크기특징권장도
Intl (내장)0KB브라우저 내장, IANA 지원필수
date-fns + date-fns-tz~7KB (트리셰이킹)함수형, 타입 안전, 트리셰이킹권장
luxon~23KBIntl 기반, 풍부한 API선택
dayjs + timezone~7KB경량, moment 호환 API선택
Temporal (Stage 3)0KB미래 표준, 폴리필 필요대기

권장 조합: Intl (기본) + date-fns / date-fns-tz (복잡한 연산)

Tauri (Rust)

라이브러리특징
chrono + chrono-tzRust 표준 날짜/시간 + IANA 타임존
iana-time-zoneOS 타임존 감지
time경량 대안 (chrono 대비)

Flutter (Dart)

라이브러리특징권장도
intl날짜 포맷, 숫자 포맷필수
timezoneIANA 타임존 변환권장
timeago상대 시간 표시권장
flutter_timezoneOS 타임존 감지선택

권장 조합: intl + timezone + timeago


8. Laravel 백엔드 연동

API 응답 형식

{
"data": {
"id": 1,
"title": "회의",
"starts_at": "2026-04-06T03:00:00Z",
"ends_at": "2026-04-06T04:30:00Z",
"created_at": "2026-04-01T00:00:00Z"
},
"meta": {
"server_time": "2026-04-06T06:30:00Z"
}
}

API 요청 시 타임존 전달

// 헤더에 타임존 포함 (서버에서 타임존 관련 로직에 활용)
const headers = {
'X-Timezone': detectTimezone(), // "Asia/Seoul"
'Content-Type': 'application/json',
};

// 날짜 범위 필터 요청 시 UTC로 변환하여 전송
const params = {
from: toUTC(localFromDate), // "2026-04-06T00:00:00Z" (KST 09:00 -> UTC 00:00)
to: toUTC(localToDate), // "2026-04-06T23:59:59Z"
};

Laravel 측 설정

// config/app.php
'timezone' => 'UTC', // 항상 UTC

// 미들웨어에서 사용자 타임존 사용
$userTimezone = $request->header('X-Timezone', 'UTC');

9. 구현 체크리스트

기본 설정

  • 모든 API 통신에 UTC ISO 8601 형식 사용
  • 타임존 감지 로직 구현 (4단계 우선순위)
  • 타임존 설정 UI 구현 ("시스템 설정 따르기" 포함)
  • API 요청 헤더에 X-Timezone 포함

표시 로직

  • 날짜/시간 포맷 프리셋 정의 (6종)
  • 상대 시간 표시 구현 (갱신 주기 포함)
  • UTC -> 로컬 변환 유틸 함수 작성
  • 로컬 -> UTC 변환 유틸 함수 작성

엣지 케이스

  • DST 전환일 경계 테스트
  • 타임존 변경 시 캐시된 데이터 재렌더링
  • 날짜 범위 필터의 타임존 올바른 변환
  • 반복 일정의 DST 처리
  • 서버 시간과 클라이언트 시간 차이 보정 (meta.server_time)

테스트

  • UTC, KST, EST, PST, CET 최소 5개 타임존 테스트
  • DST 전환일 (3월, 11월) 경계 테스트
  • 날짜 변경선 (UTC 0시) 전후 테스트
  • 로케일별 날짜 포맷 표시 확인