자바스크립트 Temporal API 사용법
자바스크립트로 날짜를 다뤄본 분이라면 한 번쯤은 Date 객체에 좌절해본 경험이 있을 텐데요.
월(month)이 0부터 시작하는 건 왜 그런 건지, 시간대 변환은 왜 이리 복잡한 건지, setDate()를 호출했더니 원본 객체가 변해버리는 건 또 뭔지… 🤦
그래서 moment.js나 date-fns, Day.js 같은 외부 라이브러리 없이는 날짜를 제대로 다루기 어려웠죠.
그런데 드디어 자바스크립트에 Temporal이라는 새로운 날짜/시간 API가 등장했습니다!
State of JS 설문에서 “가장 기대하는 새 기능” 1위를 차지할 정도로 개발자 기대를 한몸에 받았는데요.
이제 Firefox와 Chrome에서도 기본으로 쓸 수 있게 되면서 그 기대가 현실이 되고 있습니다.
이번 글에서는 Temporal API의 기본 개념부터 실전에서 자주 쓰이는 패턴까지 차근차근 살펴보겠습니다.
Date 객체, 뭐가 문제였나?
Temporal을 본격적으로 살펴보기 전에 기존 Date 객체가 왜 그렇게 욕을 먹었는지 간단히 짚어볼게요.
Date는 1995년에 Java의 java.util.Date를 급하게 베껴왔는데, 정작 Java는 이 API가 너무 문제가 많아서 일찍이 java.time으로 갈아탔습니다.
자바스크립트만 30년 넘게 이 레거시를 안고 온 셈이죠.
가장 악명 높은 건 월이 0부터 시작한다는 점입니다.
// 2026년 3월 15일을 만들고 싶다면...
new Date(2026, 2, 15); // month가 2라고요? 😅
// 3이 아니라 2를 넣어야 3월이 됩니다
Date 객체는 변경 가능(mutable)하기도 합니다.
한 곳에서 날짜를 수정하면 같은 객체를 참조하는 다른 코드에도 영향이 퍼져서 추적하기 어려운 버그가 생기곤 했는데요.
const birthday = new Date(1990, 0, 15);
const nextDay = birthday;
nextDay.setDate(16);
console.log(birthday.getDate()); // 16 😱 원본도 바뀌어 버림!
날짜 산술에서 예상치 못한 결과가 나오기도 합니다.
예를 들어 구독 결제일이 1월 31일인데 다음 달 결제일을 구하려고 setMonth()를 쓰면 어떻게 될까요?
const billingDate = new Date("Sat Jan 31 2026");
billingDate.setMonth(billingDate.getMonth() + 1);
console.log(billingDate.toDateString());
// 기대: Feb 28 — 실제: Mon Mar 02 😱
2월은 28일까지밖에 없으니 남은 3일이 자동으로 3월로 넘어가 버리는 건데요. 에러도 없이 조용히 오버플로우되기 때문에 실제 서비스에서 결제 관련 버그로 이어질 수 있는 위험한 동작입니다.
시간대 처리도 큰 문제였습니다.
Date는 UTC와 시스템 로컬 시간대만 알고 있어서 “뉴욕 시간으로 오후 3시”와 “서울 시간으로 오후 3시”를 동시에 다루려면 온갖 수작업이 필요했거든요.
거기다 날짜 문자열 파싱도 브라우저마다 제각각이었습니다. 같은 코드인데 어떤 브라우저에서는 로컬 시간으로, 다른 브라우저에서는 UTC로 해석하고, 또 다른 브라우저에서는 아예 에러를 던지는 상황이 벌어지곤 했거든요. moment.js나 date-fns 같은 라이브러리가 폭발적으로 성장한 건 어찌 보면 당연한 결과였는데 이 라이브러리도 문제가 없진 않았습니다. moment.js의 설치 크기가 4.15MB에 달했고 시간대와 로케일 데이터가 통째로 번들에 포함되다 보니 트리 셰이킹도 어려웠죠.
Temporal은 바로 이런 문제를 근본적으로 해결하려고 만들어진 API입니다.
Temporal이 뭔가요?
Temporal은 Math나 JSON처럼 전역에서 바로 접근할 수 있는데요.
이 세 가지는 new Math()나 new JSON()처럼 인스턴스를 만들어 쓰는 게 아니라 Math.random(), JSON.parse()처럼 관련 기능을 하나의 이름 아래 묶어두는 역할을 합니다.
보통 이런 걸 네임스페이스(namespace) 객체라고 부르죠?
Temporal도 마찬가지로 new Temporal()이 아니라 Temporal.PlainDate, Temporal.ZonedDateTime 같은 하위 타입을 꺼내 쓰는 방식이에요.
기존 Date와 비교했을 때 큰 차이점을 정리해보면요.
Temporal의 모든 타입은 불변(immutable)입니다.
.add()나 .with() 같은 메서드를 호출하면 항상 새 객체를 반환하기 때문에 원본이 변할 걱정이 없어요.
날짜만 필요한 상황, 시간만 필요한 상황, 시간대까지 필요한 상황 등 용도에 맞는 타입이 각각 존재합니다.
Date처럼 날짜만 표현하고 싶은데도 시간 정보까지 억지로 들고 있을 필요가 없죠.
IANA 시간대 데이터베이스(Asia/Seoul, America/New_York 등)를 기본 지원하기 때문에 외부 라이브러리 없이도 시간대 변환이 됩니다.
ISO 8601 문자열을 엄격하게 파싱해서 브라우저마다 다르게 해석되던 Date.parse()의 혼란도 사라지고요.
Temporal의 주요 타입
Temporal에는 용도에 따라 골라 쓸 수 있는 여러 타입이 있는데요. 처음에는 좀 많아 보일 수 있지만 각각의 역할이 명확해서 한번 이해하면 오히려 코드가 읽기 쉬워집니다.
가장 자주 쓰게 될 타입은 Temporal.PlainDate입니다.
생일이나 공휴일, 마감일처럼 “시간”은 상관없고 “날짜”만 중요한 경우에 씁니다.
const christmas = Temporal.PlainDate.from("2026-12-25");
console.log(christmas.year); // 2026
console.log(christmas.month); // 12 (드디어 12월이 12입니다! 🎉)
console.log(christmas.day); // 25
console.log(christmas.dayOfWeek); // 5 (금요일)
시간만 다루고 싶다면 Temporal.PlainTime을 씁니다.
매일 반복되는 알람이나 영업 시간처럼 날짜와 무관한 시각을 표현할 때 유용하죠.
const alarm = Temporal.PlainTime.from("07:30:00");
console.log(alarm.hour); // 7
console.log(alarm.minute); // 30
날짜와 시간이 모두 필요하지만 시간대는 중요하지 않은 경우에는 Temporal.PlainDateTime을 쓰고요.
const meeting = Temporal.PlainDateTime.from("2026-03-15T14:00:00");
console.log(meeting.toString()); // 2026-03-15T14:00:00
시간대까지 정확히 다뤄야 하는 경우에는 Temporal.ZonedDateTime입니다.
실제 물리적인 시점을 특정 지역의 관점에서 표현하기 때문에 서머타임(DST)까지 자동으로 처리해줍니다.
const seoulTime = Temporal.ZonedDateTime.from(
"2026-03-15T14:00:00+09:00[Asia/Seoul]",
);
console.log(seoulTime.toString());
// 2026-03-15T14:00:00+09:00[Asia/Seoul]
시간대나 캘린더 정보 없이 UTC 기준의 절대적인 시점만 필요하다면 Temporal.Instant를 쓰는데요.
서버 로그의 타임스탬프처럼 “정확히 언제 일어났는가”만 중요한 상황에 딱 맞습니다.
기존 Date가 밀리초(10⁻³초) 정밀도인 데 반해 Instant는 나노초(10⁻⁹초) 정밀도를 지원하기 때문에 금융 데이터나 고정밀 서버 타임스탬프도 손실 없이 다룰 수 있어요.
const now = Temporal.Now.instant();
console.log(now.toString());
// 2026-02-27T02:30:00.123456789Z
기간을 표현하는 Temporal.Duration도 빼놓을 수 없습니다.
“3일 5시간”이나 “2개월” 같은 시간의 길이를 나타내고, 날짜 연산에서 빠질 수 없는 타입이에요.
const threeDays = Temporal.Duration.from({ days: 3, hours: 5 });
console.log(threeDays.toString()); // PT77H (3일 5시간 = 77시간)
// 특정 단위로 환산하기
const meeting = Temporal.Duration.from({ hours: 2, minutes: 20 });
console.log(meeting.total({ unit: "minute" })); // 140
console.log(meeting.total({ unit: "second" })); // 8400
그 밖에도 연/월만 다루는 Temporal.PlainYearMonth와 월/일만 다루는 Temporal.PlainMonthDay가 있습니다.
신용카드 만료일(2026-08)이나 매년 반복되는 기념일(12-25) 같은 경우에 딱 맞죠.
현재 날짜와 시간 가져오기
“지금이 몇 시지?”는 가장 흔한 질문이죠.
Temporal에서는 Temporal.Now를 통해 현재 날짜와 시간을 가져옵니다.
// UTC 기준 현재 시점
const nowInstant = Temporal.Now.instant();
console.log(nowInstant.toString());
// 2026-02-27T02:30:00.123456789Z
// 현재 날짜 (ISO 캘린더, 시스템 시간대)
const today = Temporal.Now.plainDateISO();
console.log(today.toString());
// 2026-02-27
// 현재 시각
const currentTime = Temporal.Now.plainTimeISO();
console.log(currentTime.toString());
// 11:30:00.123456789
// 특정 시간대의 현재 날짜와 시간
const nowInSeoul = Temporal.Now.zonedDateTimeISO("Asia/Seoul");
console.log(nowInSeoul.toString());
// 2026-02-27T11:30:00.123456789+09:00[Asia/Seoul]
기존에 new Date()로 하던 것과 비교하면 의도가 훨씬 명확하게 드러나는 게 느껴지시나요?
“현재 날짜만 필요한 건지, 시간까지 필요한 건지, 시간대도 고려해야 하는 건지”를 코드 레벨에서 구분할 수 있습니다.
날짜와 시간 연산
Temporal의 진가가 드러나는 부분이 바로 날짜 연산인데요.
add(), subtract(), until(), since() 같은 메서드로 직관적으로 계산할 수 있습니다.
const today = Temporal.PlainDate.from("2026-02-27");
// 30일 후
const after30Days = today.add({ days: 30 });
console.log(after30Days.toString()); // 2026-03-29
// 2개월 전
const twoMonthsAgo = today.subtract({ months: 2 });
console.log(twoMonthsAgo.toString()); // 2025-12-27
// 1년 6개월 후
const future = today.add({ years: 1, months: 6 });
console.log(future.toString()); // 2027-08-27
두 날짜 사이의 기간을 계산하는 것도 간단합니다.
const startDate = Temporal.PlainDate.from("2026-01-01");
const endDate = Temporal.PlainDate.from("2026-12-31");
// 두 날짜 사이의 기간
const duration = startDate.until(endDate);
console.log(duration.toString()); // P364D (364일)
// 월 단위로 보고 싶다면
const inMonths = startDate.until(endDate, { largestUnit: "month" });
console.log(inMonths.toString()); // P11M30D (11개월 30일)
기존 Date에서는 두 날짜의 차이를 밀리초로 빼고 그걸 다시 일수로 나누는 식의 수동 계산이 필요했는데요.
Temporal은 이런 산수를 알아서 처리해줍니다.
특정 필드만 바꾸고 싶을 때는 with() 메서드를 씁니다.
불변이기 때문에 새 객체가 반환되고 원본은 그대로예요.
const date = Temporal.PlainDate.from("2026-02-27");
// 연도만 변경
const nextYear = date.with({ year: 2027 });
console.log(nextYear.toString()); // 2027-02-27
console.log(date.toString()); // 2026-02-27 (원본은 변하지 않음)
// 월의 마지막 날 구하기
const endOfMonth = date.with({ day: 31 }); // 오버플로우 시 자동 보정
console.log(endOfMonth.toString()); // 2026-02-28
날짜 비교도 깔끔합니다.
Date에서는 getTime()으로 밀리초 값을 비교하거나 >, < 연산자를 써야 했지만 Temporal은 전용 메서드를 제공해요.
const a = Temporal.PlainDate.from("2026-03-01");
const b = Temporal.PlainDate.from("2026-02-27");
console.log(a.equals(b)); // false
console.log(Temporal.PlainDate.compare(a, b)); // 1 (a가 더 나중)
console.log(Temporal.PlainDate.compare(b, a)); // -1 (b가 더 이전)
시간대 다루기
Temporal이 특히 빛을 발하는 부분이 시간대 처리입니다. 국제 서비스를 운영하거나 다른 시간대에 있는 사람과 미팅 시간을 조율할 때 정말 유용한데요.
// 서울에서 오후 2시에 미팅을 잡았다면
const meetingInSeoul = Temporal.ZonedDateTime.from({
timeZone: "Asia/Seoul",
year: 2026,
month: 3,
day: 15,
hour: 14,
minute: 0,
});
// 뉴욕에서는 몇 시인지 확인
const meetingInNY = meetingInSeoul.withTimeZone("America/New_York");
console.log(meetingInNY.toString());
// 2026-03-15T01:00:00-04:00[America/New_York]
// 런던에서는?
const meetingInLondon = meetingInSeoul.withTimeZone("Europe/London");
console.log(meetingInLondon.toString());
// 2026-03-15T05:00:00+00:00[Europe/London]
서머타임(DST) 전환도 자동으로 처리됩니다. 미국에서는 3월 두 번째 일요일에 시계를 1시간 앞으로 돌리기 때문에 새벽 2시는 존재하지 않는 시간이 되는데요. Temporal은 이런 상황을 깔끔하게 다룹니다.
// 2026년 3월 8일은 미국의 서머타임 전환일
const beforeDST = Temporal.ZonedDateTime.from({
timeZone: "America/New_York",
year: 2026,
month: 3,
day: 8,
hour: 1,
minute: 30,
});
console.log(beforeDST.offset); // -05:00 (EST)
// 2시간 후는?
const afterDST = beforeDST.add({ hours: 2 });
console.log(afterDST.hour); // 4 (2시가 3시로 건너뛰므로)
console.log(afterDST.offset); // -04:00 (EDT)
시간대 없이 만든 PlainDateTime을 특정 시간대에 배치할 수도 있습니다.
const localTime = Temporal.PlainDateTime.from("2026-06-15T09:00:00");
// 이 시각을 서울 시간으로
const inSeoul = localTime.toZonedDateTime("Asia/Seoul");
// 이 시각을 도쿄 시간으로
const inTokyo = localTime.toZonedDateTime("Asia/Tokyo");
다양한 캘린더 지원
Temporal의 숨겨진 강점 중 하나가 다양한 캘린더 시스템을 기본 지원한다는 점인데요.
기존 Date도 toLocaleDateString()을 통해 다른 캘린더로 “표시”하는 건 가능했지만 날짜 연산은 항상 그레고리력 기준으로만 수행했습니다.
Temporal은 그 캘린더의 규칙에 따라 연산까지 정확하게 처리합니다.
예를 들어 히브리력에서 한 달을 더하는 경우를 비교해볼게요.
// Temporal: 히브리력 기준으로 한 달을 더함
const hebrewDate = Temporal.PlainDate.from("2026-03-11[u-ca=hebrew]");
console.log(hebrewDate.toLocaleString("en", { calendar: "hebrew" }));
// '22 Adar 5786'
const nextMonth = hebrewDate.add({ months: 1 });
console.log(nextMonth.toLocaleString("en", { calendar: "hebrew" }));
// '22 Nisan 5786' ✅ 히브리력으로 정확히 한 달 후
반면 기존 Date로 같은 작업을 하면요.
// Date: 그레고리력 기준으로 한 달을 더하고 히브리력으로 표시만 함
const legacyDate = new Date(2026, 2, 11);
console.log(legacyDate.toLocaleDateString("en", { calendar: "hebrew" }));
// '22 Adar 5786'
legacyDate.setMonth(legacyDate.getMonth() + 1);
console.log(legacyDate.toLocaleDateString("en", { calendar: "hebrew" }));
// '24 Nisan 5786' ❌ 그레고리력 한 달(3월→4월)을 더한 결과
Date는 그레고리력으로 3월에서 4월로 한 달을 더한 뒤 결과를 히브리력으로 “변환”만 하기 때문에 22 Nisan이 아니라 24 Nisan이 나옵니다.
그레고리력과 히브리력의 월 길이가 다르기 때문에 생기는 차이죠.
Temporal이 지원하는 캘린더에는 islamic, persian, chinese, japanese, buddhist 등이 있어서 국제 서비스에서 다양한 문화권의 날짜를 정확하게 처리할 수 있습니다.
실전 활용 패턴
실무에서 자주 마주치는 날짜 관련 작업을 Temporal로 어떻게 처리하는지 몇 가지 살펴볼게요.
D-day 계산기를 만든다면 이렇게 할 수 있습니다.
function getDday(targetDate) {
const today = Temporal.Now.plainDateISO();
const target = Temporal.PlainDate.from(targetDate);
const diff = today.until(target);
return diff.days;
}
console.log(getDday("2026-12-25")); // 크리스마스까지 남은 일수
구독 서비스의 만료일 계산도 아주 직관적이에요.
function getSubscriptionExpiry(startDate, plan) {
const start = Temporal.PlainDate.from(startDate);
if (plan === "monthly") return start.add({ months: 1 });
if (plan === "yearly") return start.add({ years: 1 });
if (plan === "trial") return start.add({ days: 14 });
}
const expiry = getSubscriptionExpiry("2026-02-27", "yearly");
console.log(expiry.toString()); // 2027-02-27
나이 계산처럼 “연도 단위 차이”가 필요한 경우도 간단하고요.
function getAge(birthDate) {
const birth = Temporal.PlainDate.from(birthDate);
const today = Temporal.Now.plainDateISO();
return birth.until(today, { largestUnit: "year" }).years;
}
console.log(getAge("1990-05-15")); // 만 나이 계산
Intl API와 함께 쓰면 날짜를 지역에 맞게 포맷팅할 수도 있습니다.
const date = Temporal.PlainDate.from("2026-02-27");
// toLocaleString()으로 간편하게 포맷팅
console.log(date.toLocaleString("ko-KR", { dateStyle: "long" }));
// 2026년 2월 27일
console.log(date.toLocaleString("en-US", { dateStyle: "full" }));
// Friday, February 27, 2026
Date에서 Temporal로 갈아타기
이미 Date를 쓰고 있는 코드를 Temporal로 옮기려면 어떻게 해야 할까요?
다행히 두 API 사이의 변환이 그렇게 어렵지 않습니다.
// Date → Temporal.Instant
const legacyDate = new Date("2026-02-27T10:00:00Z");
const instant = Temporal.Instant.fromEpochMilliseconds(legacyDate.getTime());
console.log(instant.toString()); // 2026-02-27T10:00:00Z
// Temporal.Instant → Date
const temporal = Temporal.Instant.from("2026-02-27T10:00:00Z");
const dateObj = new Date(temporal.epochMilliseconds);
console.log(dateObj.toISOString()); // 2026-02-27T10:00:00.000Z
새로 작성하는 코드에서는 Temporal을 쓰고 기존 라이브러리와의 인터페이스 경계에서만 Date로 변환하는 게 합리적입니다.
점진적으로 마이그레이션하면 코드 전체를 한꺼번에 고치는 부담을 줄일 수 있죠.
자주 쓰이는 패턴 대응표를 정리하면 이렇습니다.
// 기존: new Date()
// Temporal: Temporal.Now.instant() 또는 Temporal.Now.plainDateISO()
// 기존: date.getFullYear()
// Temporal: plainDate.year
// 기존: date.getMonth() + 1 (0-indexed였으니까...)
// Temporal: plainDate.month (1-indexed! 🎉)
// 기존: date.setDate(date.getDate() + 7) (원본 변경!)
// Temporal: plainDate.add({ days: 7 }) (새 객체 반환)
// 기존: date1.getTime() === date2.getTime()
// Temporal: date1.equals(date2)
브라우저 지원 현황
Temporal은 2017년에 TC39 Stage 1에 진입한 이후 약 9년간의 표준화 과정을 거쳐 2026년 3월 11일 TC39 총회에서 Stage 4에 도달했습니다. ES2015 이후 가장 큰 규모의 ECMAScript 추가 사양으로, 공식 테스트 스위트(Test262)에만 약 4,500개의 테스트가 포함되어 있을 정도인데요.
흥미로운 점은 구현 과정에서 Google의 국제화 팀과 Boa 엔진이 협력해 temporal_rs라는 Rust 라이브러리를 만들었다는 건데요.
V8과 Boa 같은 서로 다른 자바스크립트 엔진이 하나의 공유 구현체를 사용하는 건 TC39 제안 역사상 전례가 없는 일이었습니다.
현재 브라우저 지원 현황을 정리하면, Firefox 139(2025년 5월)가 첫 번째로 Temporal을 기본 탑재한 브라우저가 되었고 Chrome 144와 Edge 144(2026년 1월)도 기본 지원하고 있습니다. Safari는 아직 Technology Preview 단계이고, TypeScript는 6.0 베타(2026년 2월)부터 Temporal 타입을 지원합니다. Node.js는 v26에서 정식 지원될 예정이에요.
아직 모든 브라우저에서 지원되지 않기 때문에 프로덕션에서 쓰려면 폴리필이 필요한데요.
@js-temporal/polyfill은 TC39 제안 챔피언이 관리하는 공식 폴리필이고, temporal-polyfill은 FullCalendar 개발자가 관리하는 경량 폴리필로 번들 크기가 약 20KB(gzip)여서 공식 폴리필의 절반도 안 됩니다.
# 경량 폴리필 설치
bun add temporal-polyfill
# 또는
npm install temporal-polyfill
import { Temporal } from "temporal-polyfill";
// 네이티브 Temporal과 동일한 API
const today = Temporal.Now.plainDateISO();
번들 크기가 중요한 프론트엔드 프로젝트라면 temporal-polyfill이 더 나은 선택일 겁니다.
브라우저 지원이 확대되면 import만 바꾸면 되니까 마이그레이션 부담도 적고요.
마치며
30년 넘게 자바스크립트 개발자를 괴롭혀온 Date 객체의 시대가 드디어 저물고 있습니다.
Temporal은 불변 타입, 용도별 분리, 시간대 기본 지원, 직관적인 연산 메서드 등 기존 Date의 문제점을 근본부터 다시 설계했는데요.
간단히 정리하면, 날짜만 필요하면 PlainDate, 시간대까지 필요하면 ZonedDateTime, 절대 시점이 필요하면 Instant를 쓰면 됩니다.
날짜 연산은 add(), subtract(), until(), since()로 처리하고 with()로 특정 필드만 바꿀 수 있어요.
이 모든 연산이 불변이라 원본 걱정 없이 안심하고 쓸 수 있죠.
Stage 4에 도달하긴 했지만 아직 남은 과제도 있는데요.
HTML의 <input type="date">나 <input type="time"> 같은 날짜 입력 요소가 Temporal 타입을 직접 반환하는 기능이나, 쿠키 만료 시간 설정 같은 Web API와의 통합은 앞으로 진행될 예정입니다.
지금 당장 프로덕션에 적용하기 어려운 환경이라도 폴리필을 활용해서 새 프로젝트부터 도입해보면 좋겠습니다.
Temporal을 한번 써보면 다시 Date로 돌아가기 싫어질 거예요 😄
Temporal API에 대해 더 깊이 알고 싶다면 MDN의 Temporal 문서와 TC39 공식 문서를 참고해보세요. 이 글을 업데이트하면서 참고한 Bloomberg의 Temporal 개발 이야기도 추천합니다. 9년간의 표준화 여정과 Bloomberg, Google, Igalia가 어떻게 협력했는지 생생하게 담겨 있거든요.
This work is licensed under
CC BY 4.0