JavaScript 개발자라면 누구나 Date 객체 때문에 한 번쯤은 당황해 보셨을 겁니다. “왜 1월이 0이지?”, “분명 함수에 값을 넘겼을 뿐인데, 원본 날짜가 바뀌었네?”, “서버/클라이언트에서 시간이 다르게 찍히는데 이게 타임존 문제인가?” 같은 질문들은 프로젝트 규모가 커질수록 더 자주, 더 비싸게(버그 비용) 돌아옵니다. Date는 오래된 API이고, 당시의 제약과 관습을 끌어안은 채 오늘날의 요구사항을 버티고 있습니다. 그래서 일정/예약/마감/통계처럼 날짜와 시간이 중요한 기능을 만들면 결국 외부 라이브러리(moment, dayjs 등)에 의존하거나, 팀 내부에서 “날짜 처리 규칙”을 별도로 만들게 되죠.
이런 배경에서 TC39는 Temporal이라는 새로운 날짜/시간 API를 제안했고, Temporal은 Date가 겪어온 혼란을 “패치”가 아니라 “재설계”로 풀려고 합니다. Temporal은 불변성(immutable), 명확한 타입 분리(날짜만/시간만/타임존 포함 등), 그리고 표준화된 타임존/달력 모델을 중심 원칙으로 잡고 있습니다.
현재 Temporal은 Stage 3단계로 API 설계가 상당히 안정화되어 있지만, 브라우저 전반에서 기본 제공되는 수준까지는 아직 진행 중인 상태입니다. 그래서 실무에서는 폴리필과 함께 도입하는 전략이 많이 사용됩니다. 이번 글에서는 Temporal API로 해결하는 날짜/시간 처리를 살펴보고자 합니다.

미리 요점만 콕 집어보면?
Date 객체는 “못 쓴다”기보다는, 현대 웹앱이 요구하는 복잡한 요구를 담기에는 구조적으로 불편한 점이 많습니다. 특히 월 인덱싱, 가변성, 타임존 모델의 부재는 현업에서 실제 버그로 이어지기 쉬운 대표 원인입니다.
Date에서 월(month)은 0부터 시작합니다. 즉 1월은 0, 12월은 11입니다. 반면, 일(Day)은 1부터 시작합니다. 같은 객체 안에서 어떤 값은 0-based이고 어떤 값은 1-based라는 점이, 실무에서 실수의 씨앗이 됩니다. 이런 방식은 역사적으로 여러 언어/라이브러리에서 “인덱스 기반” 관습으로 쓰여온 흔적이지만, 오늘날 기준으로는 직관성이 낮고 실수 유발 확률이 높습니다.
const date = new Date(2024, 0, 15); // 1월 15일
const date2 = new Date(2024, 3, 1); // 4월 1일 (3이 4월)
특히 폼 입력처럼 사용자가 “3월”을 선택해 들어오는 값은 보통 1~12의 자연스러운 값인데, 개발자가 이를 Date 생성자에 그대로 넘기면 한 달이 밀립니다. 반대로 이미 어딘가에서 0-based로 변환된 값을 다시 -1하는 이중 보정 버그도 흔합니다. 리뷰에서 놓치기 쉬운 데다, 특정 날짜에서만 터져서 QA를 통과하고 프로덕션에서 발견되는 경우도 많습니다.
Date의 setter는 대부분 원본을 직접 수정합니다. 즉 “값을 바꾸는 연산”이 아니라, “객체를 변형(mutate)하는 연산”입니다.
const start = new Date(2024, 0, 15);
start.setMonth(5); // 원본이 변경됨!
console.log(start); // 6월로 바뀐 start
문제는 Date 객체를 함수에 전달하는 순간부터입니다. 함수 내부에서 setMonth 같은 setter를 호출하면, 호출자 입장에서는 “그냥 날짜를 넘겼을 뿐인데” 원본이 바뀌어버립니다. React에서도 날짜를 state로 들고 있을 때 이 문제가 더 체감됩니다. 상태 업데이트는 “새 값(새 참조)”으로 교체하는 패턴이 안전한데, Date를 mutate하면 참조 자체는 그대로라서 코드가 복잡해질수록 버그가 숨어들기 쉽습니다. “렌더가 무조건 안 된다”라고 단정하기보다는, “참조가 그대로인 변경이 섞이면서 상태 추적이 어려워지고, 최적화/비교 로직과 충돌하기 쉽다”가 더 정확한 표현입니다.
여기서 가장 오해가 많은 부분이 있습니다. Date가 “타임존을 전혀 못 다룬다”는 건 정확하지 않습니다. Date 자체는 내부적으로 UTC 기반 타임스탬프를 들고 있고, Intl.DateTimeFormat의 timeZone 옵션을 사용하면 “표시(포맷팅)”는 다른 타임존으로도 가능합니다.
다만 진짜 한계는 “표시”가 아니라 “생산, 연산, 해석”입니다. 예를 들어 ‘사용자가 뉴욕 시간으로 선택한 2026-03-10 01:30’을 “뉴욕 타임존 기준의 현지 시간”으로 정확히 해석하고, DST 전환 규칙까지 고려해 더하고 빼고, 그 결과를 다른 타임존으로 안정적으로 변환하는 모델은 Date만으로 깔끔하게 만들기 어렵습니다. 그래서 글로벌 예약/일정/마감 기능을 만들면 코드가 빠르게 복잡해지고, 결국 라이브러리를 쓰거나 팀 규칙으로 보완하게 됩니다. Temporal은 바로 이 지점을 “타임존이 포함된 타입”과 “절대 시간(Instant)”을 분리해서 해결하려고 합니다.
Temporal은 Date의 단점들을 단순히 “메서드 몇 개 추가”로 해결하지 않습니다. 설계 철학부터 바꾸었습니다. Temporal을 이해할 때 핵심은 불변성, 타입 분리, 명시적 타임존 모델 세 가지입니다.

Temporal의 객체는 기본적으로 불변입니다. 즉 값을 수정하는 메서드를 호출해도 원본이 바뀌지 않습니다. 대신 새로운 객체를 반환합니다.
const date = Temporal.PlainDate.from("2024-01-15");
const newDate = date.with({ month: 6 });
console.log(date.toString()); // "2024-01-15"
console.log(newDate.toString()); // "2024-06-15"
이 불변성은 단순히 “취향”이 아니라, 실무에서 디버깅 비용을 줄이고 데이터 흐름을 안전하게 만드는 장치입니다. 함수에 넘겨도 원본이 바뀌지 않으니 사이드 이펙트가 줄고, 상태 관리에서도 “새 객체로 교체”가 더 자연스러워집니다.
Temporal은 “날짜/시간을 하나의 만능 타입”으로 뭉치지 않습니다. 대신 용도별 타입을 제공합니다. Temporal.PlainDate는 타임존이 없는 “순수한 날짜”입니다. 생일, 기념일, 공휴일처럼 “어느 나라에서 보든 같은 달력 날짜”를 다룰 때 적합합니다. 이 타입을 쓰면 코드에서도 의도가 바로 드러납니다.
const birthday = Temporal.PlainDate.from("2024-01-15");
console.log(birthday.month); // 1 (드디어 1부터 시작)
Temporal.ZoneDateTime은 “타임존을 포함하는 날짜+시간”입니다. 글로벌 예약/이벤트처럼 “현지 시간 + 타임존 규칙”이 중요한 기능에서 핵심 역할을 합니다. Temporal.Instant는 타임존과 무관한 “절대 시간(타임스탬프)”을 나타내고, 서버 저장 용도로 특히 적합합니다.
Temporal은 IANA 타임존을 명시적으로 다룰 수 있도록 설계되어 있습니다. ZoneDateTime은 “Instant + timeZone + calendar” 조합으로 이해할 수 있고, 변환도 명시적으로 수행합니다.
const seoulTime = Temporal.ZonedDateTime.from({
timeZone: "Asia/Seoul",
year: 2024,
month: 1,
day: 15,
hour: 14,
minute: 0,
second: 0,
});
const nyTime = seoulTime.withTimeZone("America/New_York");
console.log(seoulTime.toString());
console.log(nyTime.toString());
ZoneDateTime.from()이 객체 형태 입력을 받는 방식은 명세/문서에도 예제가 제시되어 있습니다.
서머타임(DST) 같은 경계도 Temporal은 타입과 타임존 규칙을 기반으로 처리합니다. 아래 예시처럼 특정 시점에 “존재하지 않는 시간”이 생기는 구간에서도 결과가 일관되게 계산됩니다.
const beforeDST = Temporal.ZonedDateTime.from({
timeZone: "America/New_York",
year: 2024,
month: 3,
day: 10,
hour: 1,
minute: 0,
second: 0,
});
const afterDST = beforeDST.add({ hours: 1 });
console.log(afterDST.hour); // 3 (2시가 건너뛰는 구간이면 3이 될 수 있음)
Temporal을 “대체 API”로만 보면 어렵게 느껴질 수 있습니다. 그런데 실무에서 Temporal이 진짜 빛나는 패턴은 의외로 단순합니다. “생성/연산/표시/저장”을 각각 어떤 타입으로 다룰지 기준만 세우면 됩니다.
PlainDate로 날짜 연산을 하면, 달력 규칙(말일 처리 등)을 엔진이 책임지는 구조를 만들 수 있습니다.
const date = Temporal.PlainDate.from("2024-01-15");
const in7Days = date.add({ days: 7 });
console.log(in7Days.toString()); // "2024-01-22"
또한 차이 계산도 “단위”를 명시해 의도를 분명히 할 수 있습니다.
const start = Temporal.PlainDate.from("2024-01-15");
const end = Temporal.PlainDate.from("2024-02-02");
const diff = start.until(end, { largestUnit: "day" });
console.log(diff.days); // 18글로벌 예약 시스템을 생각해 보겠습니다. 핵심은 다음 한 줄로 요약됩니다. “생성은 사용자의 타임존(ZonedDateTime), 저장은 절대 시간(Instant), 표시는 다시 사용자 타임존(ZonedDateTime)입니다.
class ReservationSystem {
createReservation(userTimeZone, year, month, day, hour, minute = 0) {
const reservation = Temporal.ZonedDateTime.from({
timeZone: userTimeZone,
year,
month,
day,
hour,
minute,
second: 0,
});
// 서버 저장은 절대 시간(Instant) 문자열로
return reservation.toInstant().toString();
}
displayReservation(instantString, userTimeZone) {
const instant = Temporal.Instant.from(instantString);
const userTime = instant.toZonedDateTimeISO(userTimeZone);
};
}
}
이 패턴의 장점은 “사용자가 어디에 있던 저장 값은 흔들리지 않는다”라는 점입니다. 서버에는 Instant로 저장되기 때문에 타임존이 바뀌거나, 서버 환경이 바뀌어도 데이터가 흔들리지 않습니다. 화면에 보여줄 때만 사용자의 타임존으로 변환해 “현지 시간”을 만들면 됩니다. Date로도 구현은 가능하지만, DST 경계나 타임존 변환 로직이 코드 곳곳에 스며들기 쉬운 반면, Temporal은 타입 모델 자체가 그 복잡함을 흡수하는 방향입니다.
기존 프로젝트에서는 한 번에 갈아엎기보다 “경계부터” 바꾸는 것이 현실적입니다. 예를 들어 서버 저장/전송은 epoch milliseconds 기반인데, 화면 로직만 Temporal로 옮길 수 있습니다.
function dateToTemporalZoned(date, timeZone = "Asia/Seoul") {
return Temporal.Instant.fromEpochMilliseconds(date.getTime())
.toZonedDateTimeISO(timeZone);
}
그리고 현재는 네이티브 지원이 환경마다 다르기 때문에, 실무에서는 폴리필을 함께 쓰는 접근이 일반적입니다. 여기서 중요한 점은, “곧 모든 환경에서 기본 제공될 것이다”라고 단정하기보다는, 안정화된 제안이지만, 브라우저 전반에서 ‘기본 제공’은 아직 제한적이며, 폴리필로 지금부터 사용 가능하다 정도로 설명하는 게 가장 정확합니다.
Temporal API는 “Date의 불편함을 조금 덜어주는 도구”가 아니라, JavaScript의 날짜/시간 모델을 현대적으로 다시 세운 제안입니다. 불변성을 기본값으로 두고, 날짜/시간/타임존을 명확한 타입으로 분리하며, 글로벌 서비스에서 필연적으로 마주치는 타임존과 DST 같은 난제를 언어 차원에서 다루도록 설계되었습니다.
다만 Date가 타임존을 “전혀 못 한다”는 식으로 이해하면 오해가 생길 수 있습니다. Date는 Intl.DateTimeFormat을 통해 타임존을 지정해 “표시”는 할 수 있지만, “특정 타임존 기준 생성, 연산, 해석”을 안정적으로 풀기에는 모델이 부족했던 것이고, Temporal은 그 부족함을 타입과 규약으로 해결하려는 방향입니다.
현재 Temporal은 Stage 3으로 명세가 성숙해졌고, 일부 환경에서 구현이 진행 중입니다. 그러나 여전히 널리 기본 탑재된 기능이라고 말하기는 아직 이릅니다. 그래서 지금은 폴리필 기반 도입이 가장 현실적인 전략인데요. 새 프로젝트에서 “날짜/시간이 핵심”이라면, Temporal을 적극적으로 고려해 볼만하고, 기존 프로젝트에서도 경계(저장/표시/연산)부터 점진적으로 옮기면, Date로 인해 반복되던 혼란을 큰 비용 없이 줄일 수 있습니다.
©️요즘IT의 모든 콘텐츠는 저작권법의 보호를 받는 바, 무단 전재와 복사, 배포 등을 금합니다.