날짜·시간 버그는 끝이 없다. "사용자에게 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 검토.