본문으로 건너뛰기
기술스택 · stack/javascript/날짜와 시간: Date의 함정, 표시, 시간대

JavaScript · 5장 자료구조와 자료형

날짜와 시간: Date의 함정, 표시, 시간대

예약 날짜, 마감까지 남은 날, "3시간 전" 같은 표시는 어느 서비스에나 있어요. 그런데 자바스크립트의 Date는 1995년에 급하게 만들어져 함정이 많고, 같은 순간도 보는 사람의 시간대에 따라 다른 날짜가 돼요. 실무에서 꼭 지켜야 할 규칙만 모아 정리해요.

#5.10

이 글의 실행 블록은 어느 시간대에서 실행해도 같은 결과가 나오게 만들었어요. 시간대에 따라 달라지는 예는 실행 블록 대신 일반 코드로 보여 줘요.

01. Date 만들기와 세 가지 함정

Date는 속으로는 1970년 1월 1일 0시(UTC)부터 흐른 밀리초 숫자 하나를 들고 있어요. 그 숫자를 연 · 월 · 일로 보여 주고 다루는 방식에 함정이 세 가지 있어요.

첫째, 월은 0부터 세요. 1월이 0, 12월이 11이에요. 일(getDate)은 1부터 세고 요일(getDay)은 일요일이 0이라 더 헷갈려요. 넘치는 값을 넣으면 오류 대신 다음 달로 넘어가는데, 이 성질은 "그 달의 마지막 날" 구하기에 쓰기도 해요.

const d = new Date(2026, 10, 7);    // 2026년 11월 7일 (10 = 11월!)
console.log(d.getFullYear(), d.getMonth(), d.getDate(), d.getDay()); // 2026 10 7 6 (토요일)

const over = new Date(2026, 0, 32); // 1월 32일 → 2월 1일
console.log(over.getMonth(), over.getDate());   // 1 1

console.log(new Date(2026, 2, 0).getDate());    // 28 — 3월의 0일 = 2월의 마지막 날
console.log(new Date(2028, 2, 0).getDate());    // 29 — 윤년

둘째, 문자열 해석이 모양마다 달라요. 날짜만 있는 '2026-11-07'은 UTC 자정으로, 시각이 붙은 '2026-11-07T09:00'은 실행하는 컴퓨터의 현지 시각으로 읽어요. 그래서 날짜만 있는 문자열을 현지 날짜로 다시 읽으면, UTC보다 느린 시간대(미국 등)에서는 하루 전날이 돼요. 그 밖의 모양('11/07/2026' 같은 것)은 브라우저마다 다르게 읽을 수 있으니 쓰지 않아요.

new Date('2026-11-07').toISOString();     // '2026-11-07T00:00:00.000Z' — 어디서나 UTC 자정
new Date('2026-11-07').getDate();         // 한국에서는 7, 미국 뉴욕에서는 6
new Date('2026-11-07T00:00').getDate();   // 어디서나 7 — 현지 시각으로 읽어요

셋째, Date는 바뀌는 값이에요. setDate 같은 메서드는 그 Date 자체를 고쳐요. 객체라서 다른 변수에 담아도 같은 Date를 가리켜요(참조). 계산 전에 new Date(원래)로 복사해요.

const start = new Date(2026, 10, 7);
const due = start;                       // 복사가 아니에요
due.setDate(due.getDate() + 7);
console.log(start.getDate(), due.getDate()); // 14 14 — 시작일까지 바뀌었어요

const start2 = new Date(2026, 10, 7);
const due2 = new Date(start2);           // 복사해서 계산
due2.setDate(due2.getDate() + 7);
console.log(start2.getDate(), due2.getDate()); // 7 14

02. 보여 줄 때는 Intl.DateTimeFormat

getFullYear() · getMonth() + 1 · padStart로 직접 조립하는 대신 Intl.DateTimeFormat을 써요. 언어에 맞는 순서 · 요일 · 오전/오후를 알아서 정해 주고, timeZone을 정하면 어느 시간대 기준으로 보여 줄지도 고를 수 있어요. 아래는 UTC 9시 30분이라는 한 순간을 여러 방식으로 보여 줘요.

const at = new Date('2026-11-07T09:30:00Z'); // 끝의 Z = UTC

const seoul = new Intl.DateTimeFormat('ko-KR', { dateStyle: 'full', timeStyle: 'short', timeZone: 'Asia/Seoul' });
const ny = new Intl.DateTimeFormat('ko-KR', { dateStyle: 'full', timeStyle: 'short', timeZone: 'America/New_York' });
const short = new Intl.DateTimeFormat('ko-KR', { dateStyle: 'long', timeZone: 'Asia/Seoul' });

console.log(seoul.format(at)); // 2026년 11월 7일 토요일 오후 6:30
console.log(ny.format(at));    // 2026년 11월 7일 토요일 오전 4:30
console.log(short.format(at)); // 2026년 11월 7일

"어제", "3시간 후" 같은 상대 시간은 Intl.RelativeTimeFormat이 만들어요. 몇 시간 · 며칠 차이인지는 직접 계산해서 넘겨요.

const rtf = new Intl.RelativeTimeFormat('ko', { numeric: 'auto' });
console.log(rtf.format(-1, 'day'));   // 어제
console.log(rtf.format(0, 'day'));    // 오늘
console.log(rtf.format(3, 'hour'));   // 3시간 후
console.log(rtf.format(-2, 'week'));  // 2주 전

03. 시간대: 저장은 UTC로, 보여 줄 때만 현지 시간으로

서울에서 오후 6시 30분에 쓴 글은 뉴욕에서는 같은 날 오전 4시 30분이에요. 같은 순간이지만 보이는 날짜와 시각이 달라요. 그래서 원칙은 하나예요. 서버 · 데이터베이스 · API에는 시간대 정보가 붙은 UTC 문자열(toISOString(), 끝에 Z)이나 밀리초 숫자(getTime())로 저장하고, 화면에 보여 줄 때만 사용자의 시간대로 바꿔요.

①세 줄은 각각 UTC · 서울 · 뉴욕의 시계예요. 세로선 하나가 2026-11-07T09:30:00Z라는 같은 순간이에요. UTC로는 오전 9시 30분, 서울(UTC+9)로는 오후 6시 30분, 뉴욕(UTC−5)으로는 오전 4시 30분이에요. 저장은 이 한 순간으로 하고, 보여 줄 때만 줄을 골라요.
const at = new Date('2026-11-07T09:30:00Z');
console.log(at.toISOString());          // 2026-11-07T09:30:00.000Z — 서버로 보낼 모양
console.log(at.getTime());              // 1794043800000 — 같은 순간의 밀리초
console.log(JSON.stringify({ at }));    // {"at":"2026-11-07T09:30:00.000Z"} — JSON도 ISO로 바뀌어요

// 날짜끼리 며칠 차이인지: UTC 기준 날짜로 빼면 일광 절약 시간의 영향을 받지 않아요
const days = (Date.UTC(2026, 11, 25) - Date.UTC(2026, 10, 7)) / 86_400_000;
console.log(days);                      // 48

"생일", "예약일"처럼 시각 없이 날짜만 의미가 있는 값은 오히려 UTC 순간으로 바꾸면 안 돼요. 시간대에 따라 하루가 밀리니까요. 이런 값은 '2026-11-07' 문자열 그대로 저장하고, 표시할 때만 쪼개서 써요.

04. 새 날짜 API, Temporal 현황

이런 함정을 고치려고 만든 새 표준이 Temporal이에요. 2026년 3월 표준(TC39 Stage 4)이 되었고, Chrome · Edge 144와 Firefox 139부터 쓸 수 있고 Node.js 26에도 들어갔어요. 다만 2026년 8월 기준 Safari 정식 버전에는 아직 없어서, 아이폰 사용자가 있는 서비스는 폴리필(폴리필)을 함께 쓰거나 지원 여부를 확인하고 써요.

Temporal은 값이 바뀌지 않고(계산하면 새 값을 돌려줘요), 월을 1부터 세고, "시각 없는 날짜(PlainDate)"와 "시간대가 붙은 순간(ZonedDateTime)"을 다른 종류로 나눠요. 아래 블록은 Temporal이 없는 브라우저에서는 안내만 찍어요.

if (typeof Temporal === 'undefined') {
  console.log('이 브라우저에는 아직 Temporal이 없어요');
} else {
  const today = Temporal.PlainDate.from('2026-11-07');
  console.log(today.add({ months: 1 }).toString());                  // 2026-12-07
  console.log(Temporal.PlainDate.from('2026-01-31').add({ months: 1 }).toString()); // 2026-02-28
  console.log(today.until('2026-12-25').days);                       // 48
  console.log(today.toString());                                     // 2026-11-07 — 원래 값은 그대로
}

// 같은 일을 Date로 하면: 1월 31일 + 1개월 = 2월 31일 → 3월 3일로 넘쳐요
const jan31 = new Date(2026, 0, 31);
jan31.setMonth(1);
console.log(jan31.getMonth() + 1, jan31.getDate()); // 3 3

크리스마스 날짜를 2026-12-25 모양으로 보여 주고, 11월 7일부터 며칠 남았는지 세려는 코드예요. 날짜가 엉뚱하게 나와요. 고쳐서 2026-12-25와 D-48이 나오게 해 보세요.

const today = new Date(2026, 11, 7);
const christmas = new Date(2026, 12, 25);

const label = `${christmas.getFullYear()}-${christmas.getMonth()}-${christmas.getDate()}`;
const left = Math.round((christmas - today) / 86_400_000);

console.log(label);
console.log(`D-${left}`);
해답 보기

월이 0부터라서 11월은 10, 12월은 11이에요. 12를 넣으면 다음 해 1월로 넘어가요. 보여 줄 때는 반대로 getMonth() + 1을 하고 두 자리로 맞춰요. 날짜 차이에 Math.round를 쓰는 건, 일광 절약 시간이 있는 시간대에서 하루가 23시간이나 25시간인 날이 끼어도 정확한 날 수가 나오게 하려는 거예요.

const today = new Date(2026, 10, 7);
const christmas = new Date(2026, 11, 25);

const pad = n => String(n).padStart(2, '0');
const label = `${christmas.getFullYear()}-${pad(christmas.getMonth() + 1)}-${pad(christmas.getDate())}`;
const left = Math.round((christmas - today) / 86_400_000);
// 2026-12-25
// D-48

05. 실전 포인트

  • 언제 쓰나요? 저장 · 전송은 toISOString()(UTC), 표시는 Intl.DateTimeFormat과 Intl.RelativeTimeFormat, 날짜 차이는 Date.UTC로 계산해요. 시각 없는 날짜는 'YYYY-MM-DD' 문자열로 둬요.
  • 대신 쓰는 것: 날짜 계산이 많다면 Temporal(Safari는 폴리필과 함께)이나 date-fns · Day.js 같은 라이브러리를 써요. 예전에 많이 쓰던 Moment.js는 더 이상 새 기능을 만들지 않아요.
  • 흔한 실수: 월에 1을 더하거나 빼는 걸 잊는 것, '2026-11-07'을 현지 날짜로 다시 읽어 하루가 밀리는 것, Date를 복사하지 않고 setDate로 고치는 것, 서버에 시간대 없는 시각 문자열을 보내는 것이에요.

정리

  • Date는 UTC 기준 밀리초 하나예요. 월은 0부터, 일은 1부터 세요.
  • 날짜만 있는 ISO 문자열은 UTC 자정으로 읽혀요. 그 밖의 문자열 모양은 쓰지 않아요.
  • Date는 바뀌는 값이라 계산 전에 new Date(원래)로 복사해요.
  • 저장은 UTC, 표시는 Intl.DateTimeFormat으로 사용자 시간대에 맞춰요.
  • Temporal은 이 문제를 고친 새 표준이에요. Safari 지원을 확인하고 써요.
new Date(2026, 1, 1)은 몇 월 며칠일까요?

2026년 2월 1일이에요. 월은 0부터 세서 1이 2월이에요.

미국 사용자의 화면에서 new Date('2026-11-07').getDate()가 6이 나오는 이유는?

날짜만 있는 ISO 문자열은 UTC 자정으로 읽혀요. UTC보다 느린 미국 시간대에서는 그 순간이 아직 11월 6일 저녁이라 현지 날짜가 6이에요.

글 작성 시각을 서버에 저장할 때 어떤 모양으로 보내야 할까요?

date.toISOString()처럼 끝에 Z가 붙은 UTC 문자열(또는 밀리초 숫자)로 보내요. 시간대가 분명해서, 보여 줄 때 사용자마다 현지 시간으로 정확히 바꿀 수 있어요.

SIMPLE · 기술스택 / JavaScript — 5장 자료구조와 자료형