본문으로 건너뛰기

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

작성일: 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시) 전후 테스트
  • 로케일별 날짜 포맷 표시 확인