이벤트 (예: 무료 체험 만료, 구독 카운트다운, SLA 기간) 사이에 경과된 시간 또는 남은 시간을 계산하고 이러한 기간을 현지화되고 인간이 읽을 수 있는 형식으로 표시하려면 지금까지는 수동 계산이나 외부 라이브러리가 필요했습니다. 기존 Date 객체는 에포크 밀리초만 추적하므로 달력 월 산술, 일광 절약 시간 (DST) 전환, 단위 분산에서 오류가 발생하기 쉽습니다.
Temporal API는 Temporal.ZonedDateTime를 Temporal.Duration 및 Intl.DurationFormat와 페어링하여 DST 안전 시간차, 구성 가능한 단위 분산, 캘린더 인식 기간 비교, 현지화된 형식을 제공합니다.
구현 방법
Temporal.ZonedDateTime엔드포인트 가져오기: 시스템 시간대의 현재 시간에Temporal.Now.zonedDateTimeISO()를 사용하거나 대상 이벤트의 시간대가 사용자의 시스템 기본값과 다른 경우 명시적인 IANA 시간대 문자열(Temporal.Now.zonedDateTimeISO(timeZone))을 전달합니다..since()및.until()를 사용하여 차이 계산:now.since(start, options)를 사용하여 과거 이벤트 이후 경과된 시간을 계산합니다.now.until(end, options)를 사용하여 향후 이벤트까지 남은 시간을 계산합니다.largestUnit(예:'year','month','day','hour') 및smallestUnit(예:'minute')를 전달하여 결과Temporal.Duration의 균형을 맞추고 반올림합니다.
- 독립형
Temporal.Duration객체 균형 조정 또는 비교:Temporal.Duration.from({ minutes: 90 })를 사용하여 독립형 기간을 만들고duration.round({ largestUnit: 'hour' })를 사용하여 단위를 재조정합니다(달력 월, 연도 또는 일광 절약 시간 전환에 걸쳐 조정할 때relativeTo제공).Temporal.Duration.compare(d1, d2, { relativeTo })를 사용하여 두 기간을 비교합니다. 기간에 달력 단위 (years,months,weeks)가 포함되거나 일광 절약 시간제 경계를 넘는 경우PlainDate또는ZonedDateTime를relativeTo로 전달해야 합니다.
- 사람과 기계에 적합한 형식:
- 현지화된 사용자 대상 텍스트에는
duration.toLocaleString(locale, options)(Intl.DurationFormat에 위임)를 사용합니다. duration.toString()는 기계가 읽을 수 있는 ISO 8601 기간 문자열 (예:P1M2DT4H)에만 사용합니다.
- 현지화된 사용자 대상 텍스트에는
예: 무료 체험 만료 차이 및 현지화된 형식
// 1. Get the current time in an explicit target IANA time zone (or omit the
// argument to use the system's current time zone).
const tz = 'America/New_York';
const now = Temporal.Now.zonedDateTimeISO(tz);
// 2. Combine separate date and time strings directly.
// Do not build an ISO string with template literals; chain Temporal methods instead.
const startDateStr = '2025-01-01';
const startTimeStr = '12:00:00';
const endDateStr = '2025-01-31';
const endTimeStr = '12:00:00';
const start = Temporal.PlainDate.from(startDateStr).toPlainDateTime(startTimeStr).toZonedDateTime(tz);
const end = Temporal.PlainDate.from(endDateStr).toPlainDateTime(endTimeStr).toZonedDateTime(tz);
// 3. Calculate differences using .since() and .until()
// ZonedDateTime differences default to largestUnit: 'hour' (days are not a fixed
// length across DST, so Temporal won't balance into days unless asked). Pass
// largestUnit: 'day' or a calendar unit ('week', 'month', 'year') when you want a
// human-scale breakdown instead of e.g. PT720H. smallestUnit rounds away
// seconds and sub-second noise so the formatted string stays readable.
const timeActive = now.since(start, { largestUnit: 'year', smallestUnit: 'minute' });
const timeRemaining = now.until(end, { largestUnit: 'year', smallestUnit: 'minute' });
// 4. Format durations for display with .toLocaleString() (Intl.DurationFormat)
// instead of manually concatenating unit numbers and suffixes. 'en-US' is used
// here for deterministic output; pass undefined to use the user's locale.
console.log(`Active: ${timeActive.toLocaleString('en-US', { style: 'narrow' })}`);
console.log(`Remaining: ${timeRemaining.toLocaleString('en-US', { style: 'long' })}`);
// Use .toString() for ISO 8601 serialization (e.g., API payloads or <time datetime="...">)
console.log(`ISO 8601: ${timeRemaining.toString()}`);
// 5. Check status via .sign, and compare durations with Temporal.Duration.compare()
const isExpired = timeRemaining.sign < 0;
// Pass relativeTo when comparing durations that may include calendar months/years or DST shifts
const isPastHalfway = Temporal.Duration.compare(timeActive, timeRemaining, { relativeTo: now }) > 0;
예: 독립형 광고 길이 균형 맞추기 (round 및 Intl.DurationFormat)
// Convert a raw unit count (e.g., 90 minutes) into a balanced breakdown (1 hour, 30 minutes)
const rawDuration = Temporal.Duration.from({ minutes: 90 });
const balanced = rawDuration.round({ largestUnit: 'hour' });
// Format via duration.toLocaleString() or directly with Intl.DurationFormat
const formatter = new Intl.DurationFormat('en-US', { style: 'long' });
console.log(formatter.format(balanced)); // "1 hour, 30 minutes"
전략적 구현 및 권장사항
- 권장사항: 특정 시간대에 발생하는 실제 이벤트 (예: 정기 결제 갱신 또는 이벤트 카운트다운)와 관련된 계산에는
Temporal.ZonedDateTime를 사용하고, 시스템 기본값이 아닌 시간대를 기준으로 계산할 때는 명시적timeZone를Temporal.Now.zonedDateTimeISO(timeZone)에 전달합니다. - 권장사항:
`${dateStr}T${timeStr}`와 같은 ISO 문자열을 보간하는 대신Temporal.PlainDate.from(dateStr).toPlainDateTime(timeStr).toZonedDateTime(tz)또는Temporal.PlainDate.from(dateStr).toZonedDateTime({ timeZone: tz, plainTime: timeStr })와 같은 Temporal 메서드를 사용하여 별도의 날짜 및 시간 문자열을 결합합니다. - 시간을 초과하여 기간의 균형을 맞추려면
.since(),.until()또는.round()에largestUnit(예:'day','month','year')를 전달해야 합니다(DO).ZonedDateTime차이 메서드는 기본적으로'hour'입니다. - 두 기간을 비교할 때
Temporal.Duration.compare(d1, d2, { relativeTo })를 사용하고, 기간에 달력 단위 (months,years) 또는 시간대 인식 일이 포함될 때마다relativeTo앵커를 제공합니다. ${duration.years}y ${duration.months}m...를 수동으로 보간하는 대신duration.toLocaleString()또는new Intl.DurationFormat(locale, { style })('long','short','narrow'또는'digital')를 사용하여 사용자 디스플레이의 기간 형식을 지정해야 합니다.duration.toString()는 기계 가독형 ISO 8601 기간 문자열 (예:PT1H30M)에만 사용하고 사용자 대상 문구로는 사용하지 마세요.PlainTime를HH:MM(예:<input type="time">)로 렌더링할 때는 문자열.slice(0, 5)대신.toPlainTime().toString({ smallestUnit: 'minute' })를 사용해야 합니다.- 인스턴스를 직접 수정하지 마세요.
Temporal객체는 변경할 수 없습니다..add(),.subtract(),.round(),.with()과 같은 작업은 새 인스턴스를 반환합니다. - 차이 자체에
Date/.getTime()산술을 사용하지 마세요.Date에는 시간대를 인식하는 캘린더 연산이 없으며 밀리초 정밀도만 있습니다. - 권장사항: 계산된
Temporal.Duration(또는Temporal.ZonedDateTime.compare())의 네이티브.sign속성을 사용하여 기간이 과거/만료된 시점 (sign < 0)을 나타내는지 또는 미래의 대기 중인 시작을 나타내는지 확인합니다.
대체 전략
Temporal의 대체 및 브라우저 지원
아직 네이티브 Temporal API를 지원하지 않는 브라우저의 경우 기능 감지를 사용하고 temporal-polyfill (~15KB min+gz)를 동적으로 로드합니다.
temporal-polyfill를 가져와도 Temporal 객체가 전역 범위에 자동으로 할당되지 않습니다. 코드가 globalThis.Temporal에 의존하는 경우 명시적으로 할당합니다.
// Check if Temporal is supported natively
(async () => {
if (typeof Temporal === 'undefined') {
// Load the polyfill conditionally
const module = await import('https://esm.sh/temporal-polyfill');
globalThis.Temporal = module.Temporal;
// Extend Date.prototype if needed
Date.prototype.toTemporalInstant = module.toTemporalInstant;
}
initializeApp();
})();