JavaScriptCore(JSC)에서 JavaScript의 날짜/시간 처리 제안인 Temporal을 구현한 과정과 주요 기능(지속 시간 정밀도, 새 날짜 타입, relativeTo, 캘린더, 테스트, 진행 상황)을 정리한다.
JavaScriptCore에서 Temporal 제안을 구현하기지난 1년 동안 저는 JavaScript의 날짜 및 시간 처리를 위한 Temporal 제안을 JavaScriptCore(JSC)에 구현하는 작업을 해 왔습니다. JavaScriptCore는 WebKit 브라우저 엔진에 포함된 JavaScript 엔진입니다. 제가 시작했을 때 Temporal은 일부가 구현되어 있었고, Duration, PlainDate, PlainDateTime, Instant 타입을 지원했습니다. 하지만 Temporal과 관련된 많은 test262 테스트가 통과하지 못했고, PlainMonthDay, PlainYearMonth, ZonedDateTime 객체에 대한 지원도 없었습니다. 또한 relativeTo 매개변수에 대한 지원이 없었고, "iso8601" 캘린더만 지원했습니다.
개념적으로 duration은 시간 구성 요소의 10-튜플(10개 항목의 묶음)이거나, "years", "months", "weeks", "days", "hours", "seconds", "milliseconds", "microseconds", "nanoseconds" 필드를 가진 레코드입니다.
duration이 사용되는 한 가지 방식은 두 날짜 사이의 차이를 표현하는 것입니다. 예를 들어, 주어진 날짜부터 2027년 말까지의 기간을 구하고 싶다면 다음과 같은 JS 코드를 작성할 수 있습니다:
javascript> const duration = (new Temporal.PlainDate(2026, 1, 26)).until(new Temporal.PlainDate(2027, 12, 31), { largestUnit: "years" }) > duration Temporal.Duration <P1Y11M5D> { years: 1, months: 11, days: 5}
이 경우 until 메서드는 1년, 11개월, 5일로 구성된 duration을 반환합니다. duration은 날짜 간 차이를 나타낼 수 있으므로 음수가 될 수도 있습니다:
javascript> const duration = (new Temporal.PlainDate(2027, 12, 31)).until(new Temporal.PlainDate(2026, 1, 26), { largestUnit: "years" }) > duration Temporal.Duration <P1Y11M5D> { years: -1, months: -11, days: -5}
나노초로 변환했을 때, duration의 days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds를 합한 값은 절댓값이 최대 10^9 × 2^53에 이르는 수가 될 수 있습니다. 이 수는 32비트 정수로도, 64비트 배정밀도(double) 값으로도 표현하기에는 너무 큽니다. (2^53이라는 수의 의미가 궁금하다면, JavaScript의 MAX_SAFE_INTEGER에 대한 MDN 문서를 참고하세요.)
왜 이렇게 큰 수를 다룰 수 있어야 하는지 이해하기 위해, duration에 포함된 나노초 총합을 생각해 봅시다. 앞의 예시에서 정의한 duration 변수에 이어서:
javascript> duration.total({unit: "nanoseconds", relativeTo: new Temporal.PlainDate(2025, 12, 15)}) 60912000000000000
1년 11개월 5일은 60912000000000000 나노초, 즉 약 6.1e16 나노초입니다. Temporal에서 유효한 시작/종료 날짜라면 어떤 값이든 이 계산을 허용하고자 하며, Temporal의 유효 연도 범위는 -271821부터 275760까지이므로 결과는 매우 커질 수 있습니다. (기본적으로 Temporal은 캘린더에 대해 ISO 8601 표준을 따르며, 이는 확장(프로렙틱) 그레고리력을 사용한다는 뜻입니다. 또한 이 예시는 시간대가 없는 PlainDate를 사용하므로 계산이 일광 절약 시간제(DST)의 영향을 받지 않습니다. Temporal의 ZonedDateTime 타입으로 계산할 때는 명세가 시간대 수학이 올바르게 수행되도록 보장합니다.)
구현체가 이런 요구사항을 충족하기 쉽게 하기 위해, 명세는 duration을 내부적으로 Internal Duration Records로 표현하고, 필요에 따라 JavaScript 수준의 duration 객체와 Internal Duration Records(여기서는 "내부 duration"이라고 부르겠습니다) 사이를 변환합니다. 내부 duration은 duration의 날짜 구성 요소(years, months, weeks, days 필드)와, 허용된 범위 내에 있으며 최대 2^53 × 10^9 - 1까지 커질 수 있는 단일 정수인 "time duration"을 짝지어 저장합니다.
구현체가 반드시 이 표현을 사용해야 하는 것은 아니고, 명세가 규정하는 것과 관찰 가능한 결과가 동일하기만 하면 됩니다. 하지만 기존 구현은 충분하지 않았기 때문에, 저는 명세의 접근 방식을 매우 가깝게 따르는 방식으로 duration을 다시 구현했습니다.
이 작업은 JSC에 반영(land)되었습니다.
Temporal의 날짜 타입에는 PlainDate, PlainDateTime, Instant, ZonedDateTime, PlainMonthDay, PlainYearMonth가 있습니다. 마지막 두 타입은 부분 날짜(partial date)를 나타냅니다. 즉, 특정 달의 (월, 일) 쌍이거나, 특정 연도의 (년, 월) 쌍입니다. 부분 날짜는 모든 필드를 알 수 없거나(또는 모든 필드가 중요하지 않은) 날짜를 표현할 때, 누락된 부분에 기본값을 채운 "완전한 날짜"보다 더 나은 해법입니다.
Temporal의 ZonedDateTime 타입은 날짜와 시간대를 함께 나타내며, 시간대는 UTC로부터의 수치 오프셋이거나 이름이 붙은 시간대일 수 있습니다.
저는 PlainMonthDay와 PlainYearMonth를 모든 연산과 함께 구현했습니다. ZonedDateTime은 완전히 구현되어 있으며, 이를 위한 일련의 PR 중 첫 번째 PR이 제출되었습니다.
1년을 며칠로 바꾸고 싶다면 어떻게 할까요? Temporal은 그 작업을 할 수 있지만, 한 가지 문제가 있습니다. ISO 8601 캘린더(그레고리력과 유사)를 사용할 때, 윤년이 있기 때문에 연도를 일로 변환하는 결과는 "어느 연도인지"에 따라 달라집니다. 일부 캘린더에는 윤달도 있으므로, 연도를 월로 변환하는 것도 어느 해인지에 따라 달라집니다. 마찬가지로, 월을 일로 변환하는 것도 매달 길이가 다르기 때문에 일관된 답이 없습니다.
그 때문에 다음 코드는 결과를 계산하기 위한 정보가 부족하여 예외를 던집니다:
javascript> const duration = Temporal.Duration.from({ years: 1 }) > duration.total({ unit: "days" }) Uncaught RangeError: a starting point is required for years total
위와 같이 정의한 duration도 시작점을 넘기면 동작하게 만들 수 있는데, 이를 relativeTo 매개변수로 전달할 수 있습니다:
javascript> duration.total({ unit: "days", relativeTo: "2025-01-01" }) 365 > duration.total({ unit: "days", relativeTo: "2024-01-01" }) 366
relativeTo 매개변수에 전달된 문자열은, 어떤 형식을 따르는지에 따라 PlainDate 또는 ZonedDateTime으로 자동 변환됩니다.
저는 relativeTo 매개변수를 갖는 모든 연산에 대해 이를 지원하도록 구현했습니다. 모든 날짜 타입에 대한 구현이 반영되면, 이 작업을 일련의 pull request로 제출할 예정입니다.
ISO8601이 아닌 캘린더로 날짜를 표현하는 기능은 아직 진행 중인 작업입니다. ICU 라이브러리는 기본적인 날짜 계산을 이미 수행할 수 있지만, ISO8601이 아닌 캘린더의 날짜를 내부적으로 표현하고, 계산을 위해 올바른 ICU 함수를 호출하려면 많은 접착 코드(glue code)가 필요합니다. 이 작업은 여전히 진행 중입니다. Temporal 명세는 ISO8601이 아닌 캘린더 지원을 요구하지 않지만, 별도의 제안인 Intl Era Month Code에서는 적합한 구현체가 지원해야 할 캘린더 집합을 제안합니다.
JavaScript 테스트 스위트는 test262라고 하며, JavaScript의 모든 새 제안은 test262 테스트를 동반해야 합니다. 모든 JS 구현이 국제화(internationalization)를 지원해야 하는 것은 아니기 때문에, ISO가 아닌 캘린더나(또는 UTC 시간대 이외의) 이름이 붙은 시간대를 포함하는 Temporal 테스트는 test262의 별도 intl402 하위 디렉터리에 구성되어 있습니다.
test262 스위트에는 Temporal에 대한 테스트가 6,764개 있으며, 이 중 1,791개는 2025년에 추가되었습니다. Igalia는 지난 1년 동안 테스트 커버리지를 늘리는 데 수백 시간을 투자했습니다.
이 모든 작업은 JSC의 Technology Preview에서 플래그 뒤에 숨겨져 있으며, 사용해 보려면 --useTemporal=1 플래그를 전달해야 합니다.
위에서 논의한 모든 구현 작업(ISO가 아닌 캘린더 제외)은 완료되었지만, 저는 JSC 코드 오너들의 리뷰를 받기 위해 점진적으로 코드를 제출하는 접근을 따르고 있습니다. 2025년 동안 이미 약 40개의 pull request를 반영(land)했으며, PlainYearMonth, ZonedDateTime, relativeTo 작업을 완료하기 위해 적어도 25개는 더 제출할 것으로 예상합니다.
제가 구현한 모든 코드를 기준으로 보면, Temporal에 대한 intl402가 아닌 test262 테스트의 100%가 통과하는 반면, JSC의 현재 HEAD 버전은 테스트의 절반도 통과하지 못합니다.
Igalia의 동료들과 저는 Temporal이 완전히 통합된 미래의 JavaScript 표준을 기대하고 있습니다. 이를 통해 JavaScript 프로그램이 날짜를 더 견고하고 효율적으로 다룰 수 있게 될 것입니다. 브라우저 전반에서의 일관된 구현은 이 미래로 가는 핵심 단계입니다. 한 걸음씩, 우리는 이 목표에 가까워지고 있습니다.
이 작업을 후원해 준 Bloomberg에 감사드립니다.
정정 요청이나 주제 제안을 하려면, 공개 이슈 트래커에 이슈를 등록해 주세요.