반응형

바쁜 분은 이것만

  1. toISOString().slice(0, 10)은 UTC 날짜라서, 한국 시간 00:00~08:59에는 하루 전 날짜가 나옴
  2. 가장 확실한 방법은 getFullYear() + padStart(2, "0")로 직접 조립, 짧게는 toLocaleDateString("sv-SE")
  3. 서버가 UTC여도 한국 날짜가 필요하면 timeZone: "Asia/Seoul" 옵션을 같이 준다

자바스크립트에는 파이썬의 strftime처럼 "YYYY-MM-DD 모양으로 찍어 줘" 하는 함수가 따로 없습니다. 그래서 인터넷에서 많이 보이는 toISOString().slice(0, 10)을 쓰게 되는데, 이게 한국에서는 아침 9시 전까지 어제 날짜를 돌려줘요. 이번 글은 YYYY-MM-DD(와 시:분:초) 문자열을 만드는 방법 4가지를 비교하고, 서버 시간대가 달라도 한국 날짜를 얻는 법까지 정리했습니다.

출력이 매번 같도록 시간대는 TZ=Asia/Seoul, 날짜는 new Date(2026, 10, 13, 7, 30)(2026년 11월 13일 오전 7시 30분, 월은 0부터라 10 = 11월)으로 고정해서 Node.js v26으로 실행했어요.

1. toISOString()이 하루 전 날짜를 주는 이유

const d = new Date(2026, 10, 13, 7, 30); // 11월 13일 오전 7:30 (월은 0부터)
console.log("toString     :", d.toString());
console.log("toISOString  :", d.toISOString());
console.log("slice(0, 10) :", d.toISOString().slice(0, 10));

const night = new Date(2026, 10, 13, 9, 0);
console.log("09:00 →", night.toISOString().slice(0, 10));
$ TZ=Asia/Seoul node d1.mjs
toString     : Fri Nov 13 2026 07:30:00 GMT+0900 (Korean Standard Time)
toISOString  : 2026-11-12T22:30:00.000Z
slice(0, 10) : 2026-11-12
09:00 → 2026-11-13

11월 13일 아침인데 2026-11-12가 나왔습니다. toISOString()은 항상 UTC(세계 표준시)로 바꿔서 보여 주고, 끝의 Z가 그 표시예요. 한국은 UTC보다 9시간 빠르니 오전 7시 30분은 UTC로 전날 22시 30분입니다. 그래서 한국 시간 0시~8시 59분 사이에만 하루 전 날짜가 나오고, 9시부터는 멀쩡해요.

낮에 테스트하면 문제가 안 보이고, 새벽에 도는 예약 작업이나 자정 직후에만 틀린 날짜가 찍혀서 늦게 발견되는 경우가 많습니다. 같은 코드를 TZ=UTC에서 돌리면 아예 문제가 안 보이기도 해요.

$ TZ=UTC node d1.mjs
toString     : Fri Nov 13 2026 07:30:00 GMT+0000 (Coordinated Universal Time)
toISOString  : 2026-11-13T07:30:00.000Z
slice(0, 10) : 2026-11-13
09:00 → 2026-11-13

정리하면 toISOString()은 "서버끼리 주고받는 UTC 시각"을 만들 때 쓰는 함수이고, 화면에 보여 줄 한국 날짜를 만드는 용도가 아닙니다.

2. 방법 ①: padStart로 직접 만들기 (가장 확실)

const pad = (n) => String(n).padStart(2, "0");

function formatDate(d) {
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
}
function formatDateTime(d) {
  return `${formatDate(d)} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
}

const d = new Date(2026, 10, 13, 7, 30);
console.log(formatDate(d));
console.log(formatDateTime(d));
console.log(formatDate(d).replaceAll("-", ""));   // YYYYMMDD
console.log(formatDate(new Date(2026, 0, 5)));     // 한 자리 월·일

// padStart를 빼먹으면
const bad = (d) => `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()}`;
console.log("pad 없음:", bad(new Date(2026, 0, 5)));
2026-11-13
2026-11-13 07:30:00
20261113
2026-01-05
pad 없음: 2026-1-5

getFullYear()·getMonth()·getDate()는 모두 로컬 시간 기준이라 하루 밀릴 일이 없어요. 챙길 것은 두 가지입니다.

  • getMonth()는 0부터라서 +1을 해야 합니다.
  • 1월 5일이 2026-1-5가 되지 않게 padStart(2, "0")로 앞에 0을 채워요. 숫자에는 padStart가 없으니 String(n)으로 먼저 문자열로 바꿉니다.

코드가 조금 길지만 어떤 환경에서도 똑같이 동작하고, YYYYMMDD나 YYYY.MM.DD처럼 모양을 바꾸기도 쉬워서 함수 하나 만들어 두고 쓰는 걸 추천해요. 파이썬에서 같은 작업을 하는 법은 파이썬 날짜 문자열 변환 strptime·strftime 글에 정리해 뒀습니다.

3. 방법 ②: toLocaleDateString("sv-SE") 한 줄

const d = new Date(2026, 10, 13, 7, 30, 5);
console.log(d.toLocaleDateString("sv-SE"));
console.log(d.toLocaleString("sv-SE"));
console.log(d.toLocaleDateString("ko-KR"));
console.log(d.toLocaleDateString("en-CA"));
console.log(d.toLocaleDateString("en-US"));
console.log(d.toLocaleDateString());
2026-11-13
2026-11-13 07:30:05
2026. 11. 13.
2026-11-13
11/13/2026
11/13/2026

sv-SE는 스웨덴 표기 방식인데, 마침 YYYY-MM-DD와 YYYY-MM-DD HH:mm:ss 모양이라 한 줄로 쓰기 좋습니다. 로컬 시간 기준이라 하루 전 문제도 없어요.

  • ko-KR은 2026. 11. 13.처럼 점과 공백이 들어가서 그대로는 YYYY-MM-DD가 아닙니다.
  • en-CA(캐나다)도 지금은 같은 모양이 나오지만, 로케일 표기는 데이터(CLDR) 버전에 따라 바뀔 수 있다는 점은 알아 두세요. 출력 모양을 꼭 고정해야 하는 곳이면 1번 padStart 방식이 안전합니다.
  • 인자를 비우면 실행 환경의 언어 설정을 따르기 때문에 컴퓨터마다 다르게 나옵니다. 위 출력은 영어 설정이라 11/13/2026이에요.

4. 방법 ③: Intl.DateTimeFormat + formatToParts

모양을 직접 조립하고 싶은데 getMonth()+1 같은 계산은 피하고 싶을 때 쓰는 방법입니다. formatToParts()는 날짜를 조각(연·월·일·구분 기호)으로 나눠서 배열로 돌려줘요.

const d = new Date(2026, 10, 13, 7, 30, 5);
const fmt = new Intl.DateTimeFormat("ko-KR", {
  year: "numeric", month: "2-digit", day: "2-digit",
  hour: "2-digit", minute: "2-digit", second: "2-digit",
  hourCycle: "h23",
});
console.log(fmt.format(d));
console.log(fmt.formatToParts(d));

const p = Object.fromEntries(fmt.formatToParts(d).map(({ type, value }) => [type, value]));
console.log(`${p.year}-${p.month}-${p.day} ${p.hour}:${p.minute}:${p.second}`);
2026. 11. 13. 07:30:05
[
  { type: 'year', value: '2026' },
  { type: 'literal', value: '. ' },
  { type: 'month', value: '11' },
  { type: 'literal', value: '. ' },
  { type: 'day', value: '13' },
  { type: 'literal', value: '. ' },
  { type: 'hour', value: '07' },
  { type: 'literal', value: ':' },
  { type: 'minute', value: '30' },
  { type: 'literal', value: ':' },
  { type: 'second', value: '05' }
]
2026-11-13 07:30:05

literal은 구분 기호라 버리고, year·month·day만 골라서 원하는 모양으로 붙이면 됩니다. month: "2-digit"이라 0 채우기도 알아서 해 주고요. 로케일이 바뀌어도 조각 이름은 같으니 sv-SE 방식보다 모양이 흔들릴 걱정이 적습니다.

hourCycle: "h23"은 24시간제(0~23시)로 고정하는 옵션이에요. 이걸 빼면 ko-KR은 12시간제가 기본이라 자정이 이렇게 나옵니다.

const midnight = new Date(2026, 10, 13, 0, 5);
midnight.toLocaleTimeString("ko-KR", { hour: "2-digit", minute: "2-digit" })
// '오전 12:05'
midnight.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hourCycle: "h23" })
// '00:05'

Intl.DateTimeFormat 객체는 한 번 만들어 두고 여러 번 쓰는 게 좋습니다. 반복문 안에서 매번 toLocaleString(...)을 부르면 내부에서 포맷 객체를 계속 새로 만들기 때문이에요(MDN도 같은 이유로 많은 날짜를 처리할 땐 DateTimeFormat을 권합니다).

5. 방법 ④: 서버(UTC)에서도 한국 날짜 얻기 — timeZone 옵션

1~4번은 모두 "실행 중인 컴퓨터의 시간대"를 따릅니다. 내 맥은 한국 시간이라 괜찮은데, 클라우드 서버나 Docker 컨테이너는 보통 UTC로 돌아가요. 이때는 padStart 방식도 UTC 날짜를 만들어 버립니다. timeZone 옵션으로 시간대를 직접 지정하면 어디서 돌려도 한국 날짜가 나와요.

// 서버 시간대와 상관없이 "한국 날짜"를 얻기
const fmtKST = new Intl.DateTimeFormat("sv-SE", { timeZone: "Asia/Seoul" });
const d = new Date(Date.UTC(2026, 10, 12, 22, 30)); // = 한국 11월 13일 07:30
console.log("process TZ   :", Intl.DateTimeFormat().resolvedOptions().timeZone);
console.log("toISOString  :", d.toISOString().slice(0, 10));
console.log("local padStart:", `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`);
console.log("timeZone 지정 :", fmtKST.format(d));
console.log("toLocaleDateString:", d.toLocaleDateString("sv-SE", { timeZone: "Asia/Seoul" }));
$ TZ=Asia/Seoul node d5.mjs
process TZ   : Asia/Seoul
toISOString  : 2026-11-12
local padStart: 2026-11-13
timeZone 지정 : 2026-11-13
toLocaleDateString: 2026-11-13

$ TZ=UTC node d5.mjs
process TZ   : UTC
toISOString  : 2026-11-12
local padStart: 2026-11-12
timeZone 지정 : 2026-11-13
toLocaleDateString: 2026-11-13

같은 시각인데 UTC 환경에서는 padStart 방식도 2026-11-12가 됐습니다. timeZone: "Asia/Seoul"을 준 두 줄만 어느 환경에서나 2026-11-13이에요. 서버에서 "오늘 날짜로 파일 이름 만들기", "한국 날짜 기준 하루 통계"처럼 날짜가 중요한 코드라면 이 방식으로 고정해 두는 게 안전합니다. 지금 내 환경 시간대는 Intl.DateTimeFormat().resolvedOptions().timeZone으로 확인할 수 있어요.

정리

방법 출력(한국 11/13 07:30) 언제 쓰나
toISOString().slice(0, 10) 2026-11-12 (하루 전) UTC 날짜가 필요할 때만
getFullYear + padStart 2026-11-13 모양을 확실히 고정, YYYYMMDD 등 변형
toLocaleDateString("sv-SE") 2026-11-13 짧게 한 줄로
Intl.DateTimeFormat + formatToParts 2026-11-13 07:30:05 조각별 조립, 많은 날짜 반복 처리
위 방법 + timeZone: "Asia/Seoul" 어디서나 2026-11-13 서버·컨테이너(UTC)에서 한국 날짜

참고: MDN Web Docs — Date.prototype.toISOString, Date.prototype.toLocaleDateString, String.prototype.padStart, Intl.DateTimeFormat (timeZone, hourCycle 옵션), Intl.DateTimeFormat.prototype.formatToParts

반응형

+ Recent posts