빌드노트
날짜만 넣은 Date가 미국 시간대에서 하루 전으로 보이는 이유
YYYY-MM-DD를 Date로 만들면 UTC 자정이 되고, 로스앤젤레스에서는 전날로 표시될 수 있다. 세 브라우저 시간대의 ISO 값과 로컬 날짜를 비교하고, 달력 날짜 문자열 보존과 시점의 시간대 표시를 구분한다.
new Date("2026-10-20")를 화면에 표시했는데 로스앤젤레스에서는 10월 19일로 나온다. 날짜만 있는 문자열을 Date로 만들면 10월 20일 UTC 자정이라는 시점이 된다. 그 시점은 로스앤젤레스에서 아직 19일이다. 생일처럼 달력 날짜 자체를 보존해야 한다면 YYYY-MM-DD 문자열을 유지하고, 실제 발생 시각이라면 표시할 시간대를 정해야 한다.
파싱한 시점과 화면의 날짜를 나눠 본다
ECMAScript의 날짜 문자열 규칙에서 YYYY-MM-DD는 날짜 전용 형식이다. 오프셋이 없을 때 이 형식은 UTC로 해석한다. 반면 2026-10-20T00:00:00처럼 시간까지 있는데 오프셋이 없으면 로컬 시간으로 해석한다. 문자열 하나를 받는 Date 생성자도 Date.parse와 같은 규칙을 사용한다. 이 차이는 ECMAScript의 Date.parse 규정 (새 탭에서 열림)과 MDN의 날짜 전용 입력 예제 (새 탭에서 열림)에서 확인할 수 있다.
브라우저 개발자 도구의 Console에서 다음 코드를 실행하면 된다. 별도 라이브러리는 필요 없다. 고정 입력을 쓰므로 현재 날짜와 관계없이 비교할 수 있다. getMonth()는 0부터 시작하므로 읽기 쉬운 월을 만들 때 1을 더했다.
(() => {
const input = "2026-10-20";
const date = new Date(input);
console.log(JSON.stringify({
input,
zone: Intl.DateTimeFormat().resolvedOptions().timeZone,
iso: date.toISOString(),
localYmd: [date.getFullYear(), date.getMonth() + 1, date.getDate()],
local: date.toLocaleDateString("ko-KR"),
utc: date.toLocaleDateString("ko-KR", { timeZone: "UTC" }),
seoul: date.toLocaleDateString("ko-KR", { timeZone: "Asia/Seoul" }),
la: date.toLocaleDateString("ko-KR", { timeZone: "America/Los_Angeles" })
}));
})();
Windows 11, Chromium 147.0.7727.15, Playwright 1.59.0에서 시간대를 지정한 새 브라우저 컨텍스트를 각각 만들고 이 코드를 실행했다. 세 컨텍스트 모두 iso는 2026-10-20T00:00:00.000Z였다. Z는 UTC를 뜻한다. 달라진 것은 로컬 년월일과 표시 결과였다.
UTC:localYmd는[2026,10,20],local은2026. 10. 20.이었다.Asia/Seoul:localYmd는[2026,10,20],local은2026. 10. 20.이었다.America/Los_Angeles:localYmd는[2026,10,19],local은2026. 10. 19.이었다.
이 고정 시점에서 서울의 로컬 시각은 20일 09시, 로스앤젤레스는 19일 17시로 관찰됐다. 같은 시점의 달력 날짜가 시간대별로 다른 것이다. 로스앤젤레스의 오프셋은 계절에 따라 달라지므로 이 결과의 7시간 차이를 연중 고정값으로 쓰면 안 된다.
utc와 seoul은 세 컨텍스트에서 모두 2026. 10. 20., la는 모두 2026. 10. 19.이었다. 한국 시간대의 브라우저에서도 la를 보면 이전 날짜가 표시되는 조건을 확인할 수 있다. "ko-KR"은 한국어 표시 형식이며 한국 시간대를 지정하는 옵션이 아니다. 시간대를 생략한 toLocaleDateString은 실행 환경의 시간대를 사용하고, 명시하려면 timeZone을 전달한다. MDN의 timeZone 옵션 (새 탭에서 열림)
자기 코드에서도 ISO 값은 같은데 로컬 날짜만 다르다면 먼저 표시 기준을 확인한다. ISO 값부터 다르다면 화면 포맷을 바꾸기 전에 API의 원문 문자열과 Date 생성 직후 값을 대조한다. 시간이나 오프셋이 붙었는지, 중간에 날짜를 수정했는지가 다음 확인 대상이다. toLocaleDateString의 공백·구분자는 구현에 따라 달라질 수 있으므로 위 표시 문자열 전체를 고정 정답으로 비교하지는 않는다. 표시 문자열의 구현 차이 (새 탭에서 열림)
생일을 보관하는지, 발생 시각을 보관하는지 정한다
생일이나 기념일처럼 어디서 읽든 10월 20일이어야 하는 값에는 임의의 자정 시점을 붙일 필요가 없다. API와 화면 상태에서 유효한 YYYY-MM-DD 문자열로 보관하고, 화면에 그 날짜를 그대로 쓰거나 문자열의 구성 요소만 조합한다.
const birthday = "2026-10-20";
console.log(JSON.parse(JSON.stringify({ birthday })).birthday);
console.log(birthday.split("-").join("."));
실제 출력은 세 시간대에서 모두 차례로 2026-10-20, 2026.10.20이었다. 이 코드는 이미 형식과 실재 날짜가 검증된 값을 표시하는 예다. split 자체가 잘못된 날짜를 검증해 주지는 않는다. 핵심은 날짜를 저장하고 다시 읽는 경로에서 시점 변환을 끼워 넣지 않는 데 있다.
주문 생성 시각처럼 하나의 순간을 뜻한다면 API에서 2026-10-20T00:00:00Z처럼 Z나 명시적 오프셋을 포함해 전달한다. 그런 다음 사용자 현지 날짜로 보여 줄지, timeZone: "Asia/Seoul"로 업무 기준 날짜를 보여 줄지 정한다. 현지 표시가 요구사항이라면 로스앤젤레스의 19일 표시는 올바르다. 모든 날짜에 UTC 표시를 강제하는 수정은 이 요구와 맞지 않는다.
날짜 전용 문자열을 어쩔 수 없이 Date 기반 포맷터에 넣는 경우에는, 이 문자열을 UTC 자정으로 해석하고 UTC로 출력한다는 규칙을 해당 경로에 함께 둘 수 있다. 위 실험의 utc가 그 조합이다. 다만 이렇게 만든 값을 다른 코드가 로컬 getter로 읽으면 다시 19일이 된다. 한 화면의 timeZone 옵션이 데이터 전체를 날짜 전용 타입으로 바꾸지는 않는다.
T00:00:00을 붙이면 다른 문제가 생길 수 있다
날짜 뒤에 T00:00:00을 붙이면 이번 세 컨텍스트의 로컬 년월일은 모두 20일이 됐다. 하지만 ISO로 출력한 시점은 달라졌다. UTC 컨텍스트에서는 2026-10-20T00:00:00.000Z, 서울에서는 2026-10-19T15:00:00.000Z, 로스앤젤레스에서는 2026-10-20T07:00:00.000Z였다.
서울 시간대의 브라우저 Console에서 다음을 실행하면 이 차이를 볼 수 있다. 다른 시간대에서는 첫 줄의 출력이 달라지는 것이 정상이다.
console.log(new Date("2026-10-20T00:00:00").toISOString());
console.log(new Date("2026-10-20T00:00:00").toISOString().slice(0, 10));
서울 컨텍스트의 실제 출력은 차례로 2026-10-19T15:00:00.000Z, 2026-10-19였다. 로컬 자정으로 바꾼 뒤 UTC 문자열 앞의 열 글자를 잘라 저장하면 날짜가 다시 전날로 간다. 시간을 덧붙여 화면 한 곳을 고치는 것보다, 그 값이 달력 날짜인지 시점인지 먼저 정해야 하는 이유다.
이번 관찰은 유효한 고정 날짜 하나와 Chromium의 세 시간대 컨텍스트에 한정한다. 시스템 시계와 실제 예약 데이터는 변경하지 않았다. Safari·Firefox, 잘못된 날짜 입력, 서머타임 전환 시각은 실행하지 않았다. 이 결과로 확인한 범위는 날짜 전용 입력의 UTC 해석, 시간대별 표시, 날짜 문자열 보존의 차이다.