본문으로 건너뛰기
yutils

JavaScript 타임존 함정 — UTC·IANA·DST·날짜 저장의 정답

JS Date 의 위험, 항상 UTC 로 저장해야 하는 이유, offset 과 IANA 이름의 차이, DST 함정, 사용자에게 올바르게 표시하는 방법.

약 9분 읽기

날짜·시간 버그는 끝이 없다. "사용자에게 1 시간 일찍 알림이 갔다", "DST 넘어갈 때 cron 이 두 번 돌았다", "DB 의 일별 집계가 사용자마다 다르다". 이 가이드는 JavaScript 의 Date 의 함정, 항상 UTC 로 저장 해야 하는 이유, IANA 타임존 이름과 offset 의 차이, DST 가 부르는 사고를 정리한다.

가장 큰 거짓말 — Date 가 timezone 을 안다

흔한 오해: JS Date 객체에 timezone 정보가 박혀 있다 — 사실 아님.

  • Date 의 내부 값은 단 하나 — UTC 의 epoch milliseconds (1970-01-01 00:00 UTC 부터 경과 ms).
  • toString() · getHours() · getDay() 등 표시 함수는 실행 환경의 OS timezone 으로 변환해 보여줌.
  • 서버에서 new Date() 와 브라우저에서 new Date() 의 epoch 값은 같지만 getHours() 결과는 다를 수 있다.

결론: Date 객체 자체에는 "어느 지역의 시각" 정보가 없다. 오직 절대 시점만. 표시할 때 비로소 timezone 을 골라 변환.

저장은 항상 UTC

DB 컬럼 타입 권장:

  • Postgres: TIMESTAMPTZ (timezone 정보 포함). 입력값을 UTC 로 정규화해 저장. TIMESTAMP WITHOUT TIME ZONE 은 피한다 — 어떤 timezone 인지 모름.
  • MySQL: DATETIME 으로 UTC 명시 저장. 또는 TIMESTAMP (자동 UTC 변환, 단 2038 년 한도 주의).
  • MongoDB: ISODate 가 자동 UTC.
  • 로그·메시지: ISO 8601 with Z 또는 +00:00. 예: 2026-05-16T14:30:00Z.

Unix epoch (초/ms) 도 자체로 UTC. 가장 이식성 좋은 표현. Unix 타임스탬프 변환 로 양방향 변환을 즉시 볼 수 있다.

왜 사용자 timezone 으로 저장하지 않나

사용자가 출장 가서 timezone 이 바뀌면? 이전 데이터가 1 시간 어긋남. DST 가 적용되는 지역이면 같은 시각이 여름·겨울 다른 절대 시점이 됨. UTC 만이 절대값이라 서로 비교·정렬·집계가 안전하다.

IANA 이름 vs UTC offset

타임존을 표현하는 두 가지 방식.

IANA 이름 (권장)

Asia/Seoul, America/New_York, Europe/London, Pacific/Auckland. 지역의 모든 과거·미래 규칙 (DST 시작/종료, offset 변경 이력) 을 포함.

  • 장점: DST 자동 처리. 정치적 변경에 따라 OS/브라우저 업데이트로 반영.
  • 예: America/New_York 은 EST (-5) ↔ EDT (-4) 자동 전환.

UTC offset (제한적)

+09:00, -05:00 등 고정 offset. DST 정보 없음.

  • 한국 (KST = +09:00) 처럼 DST 없는 지역은 IANA 와 거의 등가.
  • 미국·유럽 등 DST 가 있는 지역에서는 -05:00 만 써서는 여름·겨울 정보가 사라짐. 사용 X.
  • 로그 timestamp 에 절대값으로 박을 때만 적합.

브라우저 사용자 timezone 감지:

Intl.DateTimeFormat().resolvedOptions().timeZone
// "Asia/Seoul"

지역 간 시각 변환은 타임존 변환기 로 즉시 확인 가능 — IANA 이름 datalist 400+ 종 지원.

DST 가 부르는 사고

1. 같은 시각이 두 번 또는 0 번 존재

가을 DST 종료 (예: 미국 11 월 첫 일요일 02:00 → 01:00) 시 01:30 가 두 번 발생. 봄 DST 시작 (3 월) 시 02:30 는 존재하지 않음.

영향:

  • cron 이 02:30 에 돌면 봄에는 안 돌고 가을에는 두 번 돌 수 있음.
  • 알림 예약을 "사용자 local 02:30" 으로 저장하면 DST 경계에서 누락/중복.

해결: cron 은 UTC 로 설정. 표시만 사용자 timezone.

2. 60 분 ≠ 1 일/24 시간

DST 가 있는 지역의 1 일은 23 또는 25 시간일 수 있다. date + 24h 가 다음 날 같은 시각이 아닐 수 있음. 일수 차이를 계산할 때는 UTC 변환 후 차이. 날짜 계산기 같은 도구가 internally 이를 처리한다.

3. midnight 정의

"오늘 자정" 은 사용자에게 의미 있는 단위지만 UTC 기준 자정은 사용자에게 오전 9 시 (서울) 일 수 있다. 일별 집계를 UTC 로만 하면 사용자 입장에서 하루가 어긋난다. 사용자 timezone 의 자정을 UTC 로 변환해 쿼리 범위를 만든다.

표시 — 사용자에게 친숙한 포맷

Intl.DateTimeFormat 이 표준이고 빠르고 풍부.

new Intl.DateTimeFormat("ko-KR", {
  timeZone: "Asia/Seoul",
  dateStyle: "long",
  timeStyle: "short",
}).format(new Date()) // "2026년 5월 16일 오후 11:30"

커스텀 패턴 (YYYY-MM-DD HH:mm 등) 은 날짜 포매터 로 즉시 결과 확인. moment.js 시대의 패턴 토큰을 그대로 입력 가능.

"방금 전", "3 시간 전" 같은 상대 시각은 Intl.RelativeTimeFormat:

const rtf = new Intl.RelativeTimeFormat("ko", {numeric: "auto"});
rtf.format(-3, "hour"); // "3시간 전"

API 통신 — ISO 8601 + Z

클라이언트 ↔ 서버 통신은 ISO 8601 UTC 권장.

  • 응답: "created_at": "2026-05-16T14:30:00Z"
  • 요청: 마찬가지로 UTC ISO. 서버는 무조건 UTC 로 해석.
  • +09:00 같은 offset 도 ISO 표준이지만, IANA 이름이 필요한 경우 별도 필드로 보내는 게 명확. 예: {"due_at": "2026-05-16T14:30:00Z", "due_tz": "Asia/Seoul"}.

cron 과 스케줄러

  • cron 은 UTC 로. 시스템 timezone 변경에 영향 없음.
  • 사용자에게 보여주는 cron 표시는 별도 — 0 9 * * * 가 UTC 09:00 이지만 사용자에게는 "Asia/Seoul 18:00" 으로 표시.
  • GitHub Actions cron 은 UTC 고정 (별도 설정 불가). 사용자 local 에 맞추 려면 매일 다른 시각이 될 수 있음에 주의.

2038 년 문제

Unix timestamp 가 32-bit signed int 인 시스템은 2038-01-19 03:14:07 UTC 에 overflow. 2026 년 시점에서도 일부 임베디드/구형 시스템은 영향. 새 시스템은 64-bit ms (JS Date 와 동일) 사용. MySQL TIMESTAMP 쓸 때 주의.

Temporal API — 미래의 정답

TC39 Stage 3 의 Temporal API 는 Date 의 모든 함정을 해결. Temporal.ZonedDateTime 은 절대 시점 + IANA 이름을 함께 보유. 2026 년 현재 일부 브라우저에서 flag 뒤. polyfill 로 새 코드는 사용 가능. Date 후속 표준이므로 익숙해질 가치.

흔한 함정

1. new Date("2026-05-16")

UTC 자정으로 해석. new Date("2026-05-16T00:00") local 자정. 같은 문자열인데 1 차이가 9 시간 (KST) 나기 쉬움. 명시적으로 Z 또는 offset 박는 습관.

2. toISOString() 가 항상 UTC

편한 점이지만 사용자 timezone 으로 저장하려는 의도와 충돌. 서버에 보낼 때만 사용.

3. 타임존 변환을 직접 시도

offset 을 더하기/빼기로 처리하면 DST 경계에서 실패. Intl.DateTimeFormat 또는 date-fns-tz, luxon, Temporal 같은 라이브러리에 맡길 것.

4. moment.js 신규 도입

moment.js 는 2020 년 이후 legacy. 새 프로젝트는 dayjs (동일 API), date-fns, Luxon, Temporal polyfill 권장. 번들 크기·tree-shake 모두 우수.

5. 브라우저 timezone 신뢰

VPN·OS 설정 변경으로 클라이언트 timezone 이 부정확할 수 있음. 중요한 결정 (예: 자동 알림 시각) 은 사용자에게 명시 입력 받기.

요약

  • Date 의 내부값은 UTC ms. timezone 은 표시 단계만.
  • 저장·전송 모두 UTC (ISO 8601 + Z 또는 epoch). 사용자 local 절대 X.
  • IANA 이름이 정답. 고정 offset 은 표시·로그 용도만.
  • DST 경계는 시각이 두 번 또는 0 번. cron 은 무조건 UTC.
  • 포맷은 Intl.DateTimeFormat. 새 프로젝트는 Temporal 검토.
가이드 목록으로