자바스크립트 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 😱 원본도 바뀌어 버림!
시간대 처리도 큰 문제였습니다.
Date는 UTC와 시스템 로컬 시간대만 알고 있어서 “뉴욕 시간으로 오후 3시”와 “서울 시간으로 오후 3시”를 동시에 다루려면 온갖 수작업이 필요했거든요.
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를 쓰는데요.
서버 로그의 타임스탬프처럼 “정확히 언제 일어났는가”만 중요한 상황에 딱 맞습니다.
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시간)
그 밖에도 연/월만 다루는 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로 어떻게 처리하는지 몇 가지 살펴볼게요.
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은 TC39 Stage 3 제안으로 2026년 3월 TC39 회의에서 Stage 4 승격이 논의될 예정인데요. 이미 주요 브라우저에서 구현이 진행되고 있습니다.
Firefox가 139 버전(2025년 5월)부터 기본 지원을 시작해서 첫 번째로 Temporal을 탑재한 브라우저가 되었고, Chrome도 144 버전(2026년 1월)부터 기본 지원하고 있어요. Safari는 아직 Technology Preview에서 플래그를 켜야 쓸 수 있고, Edge는 베타 빌드에서 실험적으로 지원하고 있습니다.
아직 모든 브라우저에서 지원되지 않기 때문에 프로덕션에서 쓰려면 폴리필이 필요한데요.
@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()로 특정 필드만 바꿀 수 있어요.
이 모든 연산이 불변이라 원본 걱정 없이 안심하고 쓸 수 있죠.
지금 당장 프로덕션에 적용하기 어려운 환경이라도 폴리필을 활용해서 새 프로젝트부터 도입해보면 좋겠습니다.
Temporal을 한번 써보면 다시 Date로 돌아가기 싫어질 거예요 😄
Temporal API에 대해 더 깊이 알고 싶다면 MDN의 Temporal 문서와 TC39 공식 문서를 참고해보세요.
This work is licensed under
CC BY 4.0