JavaScript Temporal API 완벽 정리: 이제 Date 지옥은 끝났다 🎉
JS로 날짜 다뤄보신 분들은 아실 거예요. 그 고통을.
UTC랑 로컬 시간 헷갈리고, 월이 0부터 시작해서 1월이 0이고, Date 객체는 어디서든 마음대로 바뀌고, 타임존 변환하다 머리 터지고... 결국 moment.js나 date-fns 없이는 아무것도 못 하는 현실.
근데요, Date는 1995년부터 이랬어요. 거의 30년간 우리가 참고 쓴 거죠. 근데 드디어! 구원이 왔습니다.
Temporal API가 등장했어요. 2017년부터 개발해왔는데 이제 TC39 Stage 3까지 왔고, 크롬/파폭/사파리 전부 구현 중이에요. 2025년 중반이면 다 쓸 수 있을 거예요.
이 글에서 Temporal 제대로 파헤쳐볼게요. 왜 필요한지, 어떻게 쓰는지, moment.js에서 어떻게 갈아타는지까지!
Date가 왜 이렇게 구린 건가요?
Temporal 보기 전에, 왜 대체재가 필요했는지 먼저 알아봐요. Date는 그냥 불편한 게 아니라, 진짜 프로덕션에서 버그 터지는 구조적 문제가 있거든요.
문제 1: 맘대로 바뀌는 값 (Mutability)
```javascript
const meeting = new Date('2025-01-15T10:00:00');
scheduleMeeting(meeting);
// 어디선가...
meeting.setHours(meeting.getHours() + 2);
// 어? 원본이 바뀌어버렸네요?
```
한 번 넘긴 Date 객체는 절대 믿으면 안 돼요. 누가 바꿨을지 모르니까요.
문제 2: 0부터 시작하는 월 (다들 한 번씩 당해봤죠?)
```javascript
// 이거 몇 월일까요?
const date = new Date(2025, 1, 14);
console.log(date.toISOString()); // 2025-02-14 ← 2월이에요!
// 1월 만들려면 0 써야 해요
const january = new Date(2025, 0, 15);
```
12월 25일 만든다고 new Date(2025, 12, 25) 썼다가 2026년 1월 25일 나온 경험, 다들 있으시죠?
문제 3: 타임존 대혼란
```javascript
const date = new Date('2025-01-15T10:00:00');
console.log(date.getHours()); // 내 컴퓨터 타임존 따라 달라요!
```
절대 시간 저장해놓고 로컬로 보여주니까, "도쿄 오전 10시"를 표현할 방법이 없어요.
문제 4: 날짜 계산의 함정
```javascript
// 1월 31일 + 1달 = ?
const jan31 = new Date(2025, 0, 31);
jan31.setMonth(jan31.getMonth() + 1);
console.log(jan31.toDateString()); // "Mon Mar 03 2025" ← 뭐여 이게?
```
2월은 31일이 없어서 3월 3일로 넘어가버려요. 😱
문제 5: 파싱이 운빨
```javascript
new Date('2025-01-15'); // 브라우저마다 UTC/로컬 다름
new Date('01/15/2025'); // MM/DD? DD/MM?
```
Temporal 등장! 제대로 만든 날짜 API
Temporal은 위 문제 전부 해결하려고 처음부터 새로 설계했어요.
핵심 원칙
- 불변(Immutable): 한번 만들면 안 바뀜. 연산하면 새 객체 반환
- 명확한 타입: 날짜/시간/타임존/기간 각각 따로
- 예측 가능: 월은 1부터! 파싱도 엄격하게!
- 타임존 기본 지원: IANA 데이터베이스 내장
- 개발자 친화적: 사람이 생각하는 대로 동작
타입 종류
```javascript
Temporal.PlainDate // 날짜만 (2025-01-15)
Temporal.PlainTime // 시간만 (10:30:00)
Temporal.PlainDateTime // 날짜+시간 (2025-01-15T10:30:00)
Temporal.ZonedDateTime // 날짜+시간+타임존 (2025-01-15T10:30:00[Asia/Seoul])
Temporal.Instant // 절대 시점 (Date 대체, 불변)
Temporal.Duration // 기간 (2시간 30분)
Temporal.PlainYearMonth // 년월만 (2025-01)
Temporal.PlainMonthDay // 월일만 (01-15, 생일 같은 거)
```
타입 이름만 봐도 뭔지 알겠죠? PlainDate 보면 "아 타임존 없는 순수 날짜구나" 바로 알 수 있어요.
Part 1: 날짜 (Temporal.PlainDate)
타임존 없이 순수하게 날짜만 다룰 때 써요. 생일, 공휴일, 마감일 이런 거요.
생성하기
```javascript
// 객체로 (월이 1부터예요! 드디어!)
const date1 = Temporal.PlainDate.from({ year: 2025, month: 1, day: 15 });
console.log(date1.toString()); // "2025-01-15"
// 문자열로
const date2 = Temporal.PlainDate.from('2025-01-15');
// 오늘
const today = Temporal.Now.plainDateISO();
```
정보 가져오기
```javascript
const date = Temporal.PlainDate.from('2025-06-15');
console.log(date.year); // 2025
console.log(date.month); // 6 (진짜 6월!)
console.log(date.day); // 15
console.log(date.dayOfWeek); // 7 (일요일. 월요일=1)
console.log(date.daysInMonth); // 30
console.log(date.inLeapYear); // false
```
날짜 계산 (드디어 정상 작동!)
```javascript
const date = Temporal.PlainDate.from('2025-01-31');
// 1달 더하기 - 알아서 처리해줌!
const nextMonth = date.add({ months: 1 });
console.log(nextMonth.toString()); // "2025-02-28" ← 2월 말일로!
// 2주 3일 더하기
const later = date.add({ weeks: 2, days: 3 });
console.log(later.toString()); // "2025-02-17"
// 체이닝도 깔끔
const result = date
.add({ months: 6 })
.add({ days: 15 })
.subtract({ weeks: 1 });
```
비교하기
```javascript
const date1 = Temporal.PlainDate.from('2025-01-15');
const date2 = Temporal.PlainDate.from('2025-03-20');
// 차이 계산
const diff = date2.since(date1);
console.log(diff.days); // 64
const diffInMonths = date2.since(date1, { largestUnit: 'month' });
console.log(diffInMonths.toString()); // "P2M5D" (2개월 5일)
```
Part 2: 시간 (Temporal.PlainTime)
시계에 보이는 시간이에요. 날짜나 타임존 없이 순수 시간만.
```javascript
const time = Temporal.PlainTime.from({ hour: 10, minute: 30 });
console.log(time.toString()); // "10:30:00"
// 시간 계산
const later = time.add({ hours: 2, minutes: 45 });
console.log(later.toString()); // "13:15:00"
// 자정 넘으면 돌아감
const pastMidnight = time.add({ hours: 16 });
console.log(pastMidnight.toString()); // "02:30:00"
// 반올림
const rounded = time.round({ smallestUnit: 'minute', roundingIncrement: 15 });
console.log(rounded.toString()); // "10:30:00" 또는 "10:45:00"
```
Part 3: 날짜+시간 (Temporal.PlainDateTime)
날짜랑 시간 둘 다 필요한데 타임존은 신경 안 쓸 때.
```javascript
const dt = Temporal.PlainDateTime.from({
year: 2025, month: 1, day: 15,
hour: 10, minute: 30
});
console.log(dt.toString()); // "2025-01-15T10:30:00"
// 따로 만들어서 합치기
const date = Temporal.PlainDate.from('2025-01-15');
const time = Temporal.PlainTime.from('10:30:00');
const combined = date.toPlainDateTime(time);
// 일부만 바꾸기
const newDt = dt.with({ month: 6, day: 20 });
console.log(newDt.toString()); // "2025-06-20T10:30:00"
```
Part 4: 진짜 핵심! 타임존 (Temporal.ZonedDateTime)
Temporal의 꽃이에요. ZonedDateTime은 "서울 오전 10시", "뉴욕 오후 3시" 같은 걸 정확하게 표현해요. 서머타임도 알아서 처리해줘요.
생성하기
```javascript
const zdt = Temporal.ZonedDateTime.from({
year: 2025, month: 1, day: 15,
hour: 10, minute: 30,
timeZone: 'Asia/Seoul'
});
console.log(zdt.toString());
// "2025-01-15T10:30:00+09:00[Asia/Seoul]"
// 도쿄 현재 시간
const tokyoNow = Temporal.Now.zonedDateTimeISO('Asia/Tokyo');
```
타임존 변환
```javascript
const seoulTime = Temporal.ZonedDateTime.from(
'2025-01-15T10:30:00[Asia/Seoul]'
);
// 뉴욕 시간으로
const nyTime = seoulTime.withTimeZone('America/New_York');
console.log(nyTime.toString());
// "2025-01-14T20:30:00-05:00[America/New_York]"
// UTC로
const utc = seoulTime.withTimeZone('UTC');
console.log(utc.toString());
// "2025-01-15T01:30:00+00:00[UTC]"
```
서머타임 처리
```javascript
// 미국은 3월 둘째 일요일에 1시간 앞으로 감
const beforeDST = Temporal.ZonedDateTime.from(
'2025-03-09T01:30:00[America/New_York]'
);
const afterDST = beforeDST.add({ hours: 1 });
console.log(afterDST.toString());
// "2025-03-09T03:30:00-04:00[America/New_York]"
// 1시 30분 + 1시간 = 3시 30분! (2시 30분이 없어요)
```
Part 5: 기간 (Temporal.Duration)
```javascript
const duration = Temporal.Duration.from({ hours: 2, minutes: 30 });
console.log(duration.toString()); // "PT2H30M"
// 기간끼리 더하기
const d1 = Temporal.Duration.from({ hours: 1, minutes: 30 });
const d2 = Temporal.Duration.from({ hours: 2, minutes: 45 });
console.log(d1.add(d2).toString()); // "PT4H15M"
// 150분 → 2시간 30분으로 변환
const unbalanced = Temporal.Duration.from({ minutes: 150 });
const balanced = unbalanced.round({ largestUnit: 'hour' });
console.log(balanced.toString()); // "PT2H30M"
```
실전 예제
```javascript
// 나이 계산
const birthdate = Temporal.PlainDate.from('1990-05-15');
const today = Temporal.Now.plainDateISO();
const age = today.since(birthdate, { largestUnit: 'year' });
console.log(`${age.years}세 ${age.months}개월`);
// D-day 계산
const deadline = Temporal.PlainDate.from('2025-12-31');
const remaining = deadline.since(today);
console.log(`D-${remaining.days}`);
```
Part 6: 실전 활용
글로벌 미팅 시간 맞추기
```javascript
function scheduleMeeting(dateTime, hostTz, attendeeTzs) {
const meeting = Temporal.ZonedDateTime.from(`${dateTime}[${hostTz}]`);
const schedule = new Map();
schedule.set(hostTz, meeting.toString());
for (const tz of attendeeTzs) {
schedule.set(tz, meeting.withTimeZone(tz).toString());
}
return schedule;
}
// 서울 오전 9시에 미팅 잡으면
// 뉴욕이랑 런던은 몇 시?
scheduleMeeting('2025-01-20T09:00:00', 'Asia/Seoul',
['America/New_York', 'Europe/London']);
```
영업일 계산
```javascript
function addBusinessDays(start, days) {
let current = start;
let remaining = days;
while (remaining > 0) {
current = current.add({ days: 1 });
if (current.dayOfWeek < 6) { // 주말 제외
remaining--;
}
}
return current;
}
// 오늘부터 영업일 기준 10일 후
const futureDate = addBusinessDays(Temporal.Now.plainDateISO(), 10);
```
Part 7: moment.js에서 갈아타기
코드 비교
```javascript
// moment.js
const m = moment('2025-01-15');
m.add(1, 'month');
// Temporal
const t = Temporal.PlainDate.from('2025-01-15');
const next = t.add({ months: 1 }); // 원본 안 바뀜!
// moment-timezone
moment.tz('2025-01-15 10:30', 'Asia/Seoul');
// Temporal
Temporal.ZonedDateTime.from('2025-01-15T10:30:00[Asia/Seoul]');
```
마이그레이션 순서
- 신규 코드부터 Temporal 사용
- 어댑터 함수 만들어서 기존 코드랑 연결
- 위험한 날짜 코드부터 순차 교체
- 다 바꾸면 moment.js 삭제
```javascript
// 변환 함수
function dateToTemporal(date) {
return Temporal.Instant.fromEpochMilliseconds(date.getTime());
}
function temporalToDate(instant) {
return new Date(instant.epochMilliseconds);
}
```
Part 8: 지금 쓰려면?
2024년 말 기준으로 아직 브라우저 기본 탑재는 안 됐어요.
| 환경 | 상태 |
|---|---|
| Chrome | 플래그로 사용 가능 |
| Firefox | 개발 중 |
| Safari | 개발 중 |
| Node.js | 플래그로 사용 가능 |
폴리필 쓰면 지금도 OK
```bash
npm install @js-temporal/polyfill
```
```javascript
import { Temporal } from '@js-temporal/polyfill';
const today = Temporal.Now.plainDateISO();
```
네이티브 체크
```javascript
async function getTemporal() {
if (typeof globalThis.Temporal !== 'undefined') {
return globalThis.Temporal;
}
const { Temporal } = await import('@js-temporal/polyfill');
return Temporal;
}
```
마무리
Temporal은 JS 역사상 날짜 처리에 대한 가장 큰 업그레이드예요.
왜 좋냐면:
- 불변 → 실수로 값 바꿀 일 없음
- 타입 명확 → PlainDate, ZonedDateTime 보면 바로 앎
- 상식적 → 월이 1부터, 메서드명도 직관적
- 타임존 네이티브 → 서머타임도 알아서
- 날짜 계산 → 드디어 정상 작동
할 일:
- 폴리필로 지금 당장 써보기
- 새 프로젝트는 Temporal로
- moment.js 탈출 계획 세우기
- 브라우저 지원 현황 체크
Date 지옥은 이제 끝이에요. Temporal 시대가 왔습니다! 🚀
관련 도구 둘러보기
Pockit의 무료 개발자 도구를 사용해 보세요