Node.js 26's default Temporal API fixes a one-hour DST drift that Date still has
Node.js 26 기본 Temporal API, Date의 서머타임 한 시간 오차를 바로잡습니다
Node.js 26에서 기본 활성화된 Temporal API로 달력 기준 하루 더하기를 처리하면, 서머타임 전환 때 Date에서 생기는 한 시간 오차를 피할 수 있습니다. 다만 작성자의 벤치마크에서는 Temporal의 .add()가 Date보다 약 19배 느렸고, 잘못된 날짜 입력도 기본 설정에서는 오류 대신 보정하므로 주의가 필요합니다.
- 주제
AI 요약
일정·결제 코드에서 자주 쓰는 new Date(now.getTime() + 24 * 60 * 60 * 1000)는 달력상 하루가 아니라 정확히 24시간을 더합니다. 서머타임을 적용하는 시간대에서는 시계가 한 시간 앞뒤로 바뀌는 날, 원하는 현지 시각과 결과가 어긋날 수 있습니다. 글쓴이는 공식 node:26과 node:24 이미지를 실행해 이 차이와 성능, 날짜 검증 동작을 확인했습니다.
달력 하루와 24시간은 다릅니다
시험에는 2026년 3월 8일 미국 동부의 서머타임 시작 시점(America/New_York)을 사용했습니다. 시작 시각은 3월 7일 밤 10시였고, 같은 시각의 다음 날을 계산했습니다. Date에 24시간을 더하면 현지 시각은 밤 11시가 됩니다. 반면 Temporal.ZonedDateTime.add({ days: 1 })는 밤 10시를 유지하고 UTC 오프셋을 -05:00에서 -04:00으로 바꿉니다. 사람이 말하는 ‘달력상 다음 날 같은 시각’에 맞는 계산입니다.
연산 비용은 Temporal에서 더 큽니다
글쓴이는 인스턴트에 초를 더하고 필드를 읽는 작업을 100만 번 반복해 세 차례 측정했습니다. Date는 65.4~72.8ms, Temporal은 1,839.7~2,071.5ms로 약 27배 느렸습니다. 이후 연산을 분리하자 차이가 어디서 생기는지 더 분명해졌습니다. 더하기만 측정하면 Date 59.6ms, Temporal 1,152.9ms로 약 19배 차이가 났고, 이미 만든 ZonedDateTime에서 시(hour)를 읽는 작업은 각각 11.5ms와 71.9ms였습니다. 글에서는 Temporal 객체가 불변이며 달력과 시간대 맥락을 함께 다루는 점을 비용의 배경으로 설명합니다.
따라서 달력 기준 계산이 필요한 코드와 밀리초 단위 처리 코드를 구분해야 합니다. 대량 날짜 범위를 훑는 작업처럼 반복 횟수가 많은 핫패스라면 성능을 직접 재는 편이 좋습니다. 반면 날짜가 아닌 고정 시간 간격을 다루는 코드에서는 Date가 여전히 적절합니다.
입력 날짜가 잘못됐는지 기본값만으로 알 수는 없습니다
Date는 new Date(2026, 1, 30)을 2026년 3월 2일로 정규화합니다. 글쓴이는 Temporal.PlainDate.from({ year: 2026, month: 2, day: 30 })이 오류를 낼 것으로 예상했지만, 기본 overflow 설정인 constrain은 결과를 2월 28일로 보정합니다. 잘못된 입력을 반드시 거부해야 하는 경계에서는 { overflow: 'reject' }를 지정해야 RangeError가 발생합니다. 형식이 잘못된 날짜 문자열이나 알 수 없는 시간대 식별자는 별도 옵션 없이도 오류를 냅니다.
Node.js 26에서 달라진 점
Node.js 24에서는 기본 상태로 Temporal 전역 객체를 사용할 수 없고, --harmony-temporal 플래그를 붙여야 합니다. 글쓴이가 확인한 Node 24.21.0의 플래그 활성화 결과는 Node 26의 기본 동작과 같았습니다. 따라서 26의 변화는 Temporal이 새로 생겼다기보다, 플래그 없이 기본 제공된다는 점입니다. 반대로 Node 26에서는 --no-harmony-temporal로 비활성화할 수 있습니다. 의존 라이브러리가 같은 이름의 폴리필을 제공한다면 충돌 여부를 확인해야 합니다.
실행 중 활성 상태를 알려주는 process.features 항목은 없었습니다. process.config.variables의 관련 값은 빌드 설정을 보여줄 뿐, --no-harmony-temporal로 끈 상태도 반영하지 않았습니다. 글에서는 런타임 확인 방법으로 typeof Temporal을 제시합니다. 기본 활성화만으로 프로세스 시작 비용이 늘어나는지도 20회씩 비교했지만, 기본 설정과 비활성화 설정의 시작 시간 차이는 측정 오차 범위였습니다.
코드 점검과 적용 시 주의점
글쓴이는 결제·일정 코드에서 86400000이나 24 * 60 * 60 * 1000을 검색해 달력 하루를 더하는 코드를 찾으라고 권합니다. 다만 이런 계산을 감춘 헬퍼 함수까지 검색 결과에 나타나지는 않습니다. Temporal로 바꾸는 일은 작더라도 해당 코드가 달력 날짜를 계산하는지, 고정 시간 간격을 계산하는지 먼저 살펴야 합니다. 사용자 입력을 검증한다면 overflow: 'reject'도 명시해야 합니다.
dev.to 반응
- @dev_in_the_fog — 실제 엔지니어링 과정에서 부딪히는 문제와 바로 적용할 해결책을 기록한 글은 커뮤니티에 큰 가치를 줍니다. 정말 세심한 글입니다.
- @alexgeorgiev17 — 감사합니다. 커뮤니티에 도움이 되는 글을 쓰려고 합니다.
- @emmawingding —
overflow: 'constrain'이 기본값이라는 부분은 조용히 문제를 일으킬 만한 내용입니다. 알려줘서 감사합니다. 저도 오류가 날 거라고 생각했습니다. 국제화 관점에서 덧붙이면, 벽시계 시각과 절대 시각의 차이는 산술뿐 아니라 화면 표시에서도 나타납니다.Date에 24시간을 더한 값을Intl.DateTimeFormat에 넘기면 바뀐 벽시계 시각이 그대로 표시됩니다. 예약 시각이 사용자가 고른 시각과 달라 보이게 됩니다.ZonedDateTime을Intl.DateTimeFormat.format()에 바로 넣으면 된다고 생각했습니다. 표시와 일정 계산이 같은 모델을 쓰며, 다른 시간대나 달력으로 현지화해도 표시 단계에서 오차가 다시 생기지 않습니다. 검색할 때는86400000상수뿐 아니라 내부에서n * 86400000을 계산하는 헬퍼도 살펴야 합니다.- @alexgeorgiev17 — 감사합니다. 저도
overflow: 'constrain'부분에 놀랐습니다. 다만 Intl 관련해서는 작은 정정이 있습니다. Node 26에서Intl.DateTimeFormat.format()에ZonedDateTime을 넘기면 실제로TypeError가 발생합니다. 명세에 따른 동작이며, 포매터의timeZone과 객체의 시간대가 서로 충돌하지 않게 하려는 의도입니다. 대신zdt.toLocaleString("en-US", {...})를 쓰면 객체 자체의 시간대를 사용합니다. 계산에 쓴 객체를 그대로 표시하면 오차가 다시 끼어들지 않는다는 취지는 같습니다. 헬퍼를 짚어준 것도 좋습니다.86400000검색만으로는daysToMs()같은 함수 안의 계산을 찾지 못합니다.864e5와24 * 60 * 60도 검색할 만합니다. - @emmawingding — 아, 바로잡아줘서 감사합니다. 직접 테스트해줘서 더 고맙습니다. 객체 자체의
toLocaleString을 쓰면 서로 다른timeZone옵션을 맞출 필요가 없어서 더 낫겠네요.864e5와 풀어 쓴24 * 60 * 60도 점검 목록에 넣겠습니다.delayMs()처럼 무해해 보이는 이름의 함수 안에 곱셈을 숨겨두는 코드 때문에 저도 곤란했던 적이 있습니다.
- @alexgeorgiev17 — 감사합니다. 저도
- @kyisaiah47 — 가을철에 현지 시각이 두 번 발생하는 경우에는 결과가 달라졌나요? Temporal은 기본으로 어느 쪽 시각을 고르나요?
- @alexgeorgiev17 — 아니요, 그 부분은 달라지지 않았습니다. 새벽 1시 30분이 두 번 나타나면 Temporal은
Date와 마찬가지로 앞선 시각인 EDT(-04:00)를 기본 선택합니다. 다만 선택지를 지정할 수 있습니다.{ disambiguation: "later" }는 두 번째 1시 30분을 고르고,"reject"는 모호한 시각이라는 오류를 냅니다. 글의 오차는 산술과 봄철 시계 이동에 관한 내용입니다.
- @alexgeorgiev17 — 아니요, 그 부분은 달라지지 않았습니다. 새벽 1시 30분이 두 번 나타나면 Temporal은
- @argumentmoney8117 — 좋은 글입니다. 기억에 남은 부분은 Temporal이
Date의 타입 혼동, 즉 인스턴트와 달력 연산의 혼동은 바로잡지만 기본 동작만으로 검증 부담까지 해결하지는 않는다는 점입니다.overflow: 'constrain'은 잘못된 입력을 마지막 유효 날짜로 조용히 보정합니다. 잘못된 날짜가 실제 오류인 입력 경계에서는overflow: 'reject'를 쓰고, 2월 28일로 보정하는 편이 나은 UI 코드에서는 기본값을 받아들이는 식이 적절해 보입니다. 질문이 하나 있습니다. 일정 처리 핫패스에서.add()의 약 19배 오버헤드를 고려하면 계산한ZonedDateTime을 캐시하시겠습니까, 아니면 호출당 절대 비용이 작아 대개 문제가 되지 않습니까?
원문: dev.to / 번역·요약: Trawling