[번역] 자바스크립트의 Date가 곧 수정됩니다

eunbinn·2024년 9월 8일
28

FrontEnd 번역

목록 보기
36/36
post-thumbnail

원문: https://docs.timetime.in/blog/js-dates-finally-fixed/

문제

최근 ECMAScript에 제안된 모든 변경 사항 중에서 저의 최애는 단연 Temporal API입니다. 이 제안은 꽤나 발전된 제안이며, 이미 FullCalendar 팀에서 제공하는 폴리필을 통해 이 API를 사용할 수 있습니다.

이 API는 매우 훌륭하기 때문에 여러 블로그 포스트를 통해 주요 기능들을 설명하려고 합니다. 이 첫 번째 포스팅에서는 주요 장점 중 하나인 '시간대가 있는 날짜와 시간(Zoned Date Time)'을 표현할 수 있는 기본 객체를 마침내 갖게 되었다는 점을 중점적으로 설명하겠습니다.

그런데... “시간대가 있는 날짜와 시간(Zoned Date Time)"이라니, 이게 무슨 말일까요?

실생활 속의 날짜 vs 자바스크립트의 날짜

실생활 속에서 날짜를 말할 때 우리는 보통 "2024년 8월 4일 오전 10시 30분에 병원 진료 예약이 있어요"라고 말하며 시간대를 생략합니다. 일반적으로 대화 상대는 우리를 알고 있고, 제가 날짜에 대해 이야기 할 때 보통 제 시간대(저의 경우 유럽/마드리드)를 기준으로 이야기한다는 것을 알고있기 때문에 시간대를 생략해도 문제가 되지 않습니다.

하지만 안타깝게도 컴퓨터는 그렇지 않습니다. 자바스크립트에서 "Date" 객체로 작업할 때 우리는 숫자를 다루게 됩니다.

공식 문서를 살펴보면 다음과 같이 이야기 합니다.

"ECMAScript 시간 값은 숫자형(Number)입니다. 이는 밀리초 단위의 정밀도로 특정 시점을 나타내는 유한한 정수이거나, 특정 시점을 나타내지 않는 NaN 입니다"

자바스크립트에서 날짜는 UTC 기반이지만, 윤초(leap seconds)를 무시하는 POSIX 시간을 따르기 때문에 정확한 UTC 시간과는 차이가 있을 수 있는 매우 중요한 사실이 있습니다. 하지만 더 큰 문제는 오직 숫자로만 표기하게 되면 날짜의 기본적인 의미가 사라진다는 것 입니다. 실생활 속의 날짜가 주어졌을 때 그에 상응하는 자바스크립트 날짜는 얻을 수 있지만 그 반대는 불가능합니다.

예를 들어 제가 카드로 결제하는 순간을 기록하고 싶다고 가정해 보겠습니다. 많은 사람이 아래와 같이 하고 싶은 유혹을 느낄 것입니다.

const paymentDate = new Date("2024-07-20T10:30:00");

제 디바이스는 CET 표준 시간대에 접속해 있으므로, 이 코드를 작성할 때 브라우저는 “이 CET 인스턴스가 주어졌을 때 UNIX 기준 시간으로부터의 밀리초를 계산”합니다.

date에 실제로 저장되는 내용은 아래와 같습니다.

paymentDate.getTime();
// 1721464200000

이 정보를 어떻게 읽느냐에 따라 각각 다른 '실생활 속의 날짜'를 얻을 수 있습니다.

CET 관점에서 읽어보면 10시 30분이 됩니다.

d.toLocaleString();
// '20/07/2024, 10:30:00'

ISO 관점에서 읽으면 8시 30분이 됩니다.

d.toISOString();
// '2024-07-20T08:30:00.000Z'

많은 사람들은 UTC로 작업하거나 ISO 형식으로 날짜를 전달하면 안전하다고 생각하지만, 여전히 정보가 손실될 수 있으므로 이는 옳지 않습니다.

UTC로는 충분하지 않습니다.

오프셋을 포함하여 ISO 형식의 날짜로 작업하더라도, 다음에 해당 날짜를 표시하려고 할 때는 UNIX 기준 시간으로부터 경과한 밀리초와 오프셋만 알 수 있습니다. 하지만 이 정보만으로는 결제가 이루어진 순간에 대한 현실적인 시간대를 알기에는 여전히 충분하지 않습니다.

예를들어, t0라는 타임스탬프가 있다고 가정해보겠습니다. t0가 주어진다면, 사람이 읽을 수 있는 날짜 n개를 얻을 수 있습니다.

다시 말해, 타임스탬프를 사람이 읽을 수 있는 날짜로 변환하는 함수는 일대일 대응이 되지 않습니다. 왜냐하면 하나의 타임스탬프가 여러 개의 "사람이 사용하는 날짜"에 대응될 수 있기 때문입니다.

타임스탬프와 ISO는 동일한 순간을 각각의 방식으로 표기한 것이므로, 결국 ISO 날짜를 저장할 때에도 같은 현상이 발생합니다.

다른 시간대이지만 같은 오프셋을 가질 수 있으므로 오프셋으로 작업하는 경우에도 이러한 문제가 발생합니다.

여전히 문제가 명확하게 이해되지 않는다면 예시를 통해 설명해 드리겠습니다. 여러분이 마드리드에 살다가 시드니로 여행을 떠났다고 가정해 보겠습니다.

몇 주 후 마드리드로 돌아와서 확인한 거래 명세서에서 16일 새벽 2시에 3.50달러가 결제 되었다는 이해 할 수 없는 내역을 발견했다고 가정해 보겠습니다. 제가 뭘 하고 있었을까요? 그날 밤 저는 일찍 잠자리에 들었는데.. 이해가 되지 않습니다.

한참을 고민하다가 다음날 아침에 마신 커피에 해당하는 요금이 청구되었다는 사실을 깨닫게 됩니다. 이 글을 읽으셨다면, 그 은행이 모든 거래 시간을 UTC로 저장하고, 애플리케이션이 이를 디바이스의 시간대로 변환한다는 사실을 추론하셨을 것입니다.

단순한 해프닝으로 끝날 수도 있지만, 만약 은행에서 하루에 한 번 무료 현금 인출 프로모션을 적용한다면 어떻게 될까요? 그 날은 언제 시작하고 끝날까요? UTC 기준? 호주 시간대 기준? 상황이 복잡해집니다.

이쯤이면 타임스탬프만으로 작업하는 것이 문제라는 것을 느끼셨길 바랍니다. 하지만 다행히도 이제 이에 대한 해결책이 있습니다.

ZonedDateTime

새로운 Temporal API는 날짜와 시간을 해당 시간대와 함께 표현하도록 특별히 설계된 Temporal.ZonedDateTime 객체를 도입했습니다. 또한 날짜를 나타내는 문자열의 직렬화 및 역직렬화를 표준화하기 위해 RFC 3339의 확장을 제안했습니다.

image

예를 들면 아래와 같습니다.

   1996-12-19T16:39:57-08:00[America/Los_Angeles]

이 문자열은 1996년 12월 19일 16시 39분 57초를 나타내며, UTC에서 -08:00 오프셋을 가지며, 시간대를 인식하는 애플리케이션이 고려할 수 있도록 관련 표준 시간대(“태평양 표준시”)를 추가로 지정합니다.

이 API를 사용하면 다음과 같은 다양한 캘린더로 작업할 수 있습니다.

  • 불교력
  • 태음력 (chinese)
  • 콥트력
  • 단기력 (dangi)
  • 에티오피아 달력
  • 그레고리력
  • 히브리력
  • 힌두력
  • 히즈라력 (islamic)
  • 히즈라력(움 알쿠라) (islamic-umalqura)
  • 탁상 히즈라력(islamic-tbla)
  • 히즈라 상용력(islamic-civil)
  • 사우디 아라비아식 히즈라력(islamic-rgsa)
  • 일본력
  • 페르시안력
  • 대만력

이 중에서 가장 흔한 것은 ISO8601(그레고리력의 표준 적용)이며, 여러분은 이것을 가장 자주 사용하게 될 것입니다.

기본 동작

날짜 생성

Temporal API는 날짜를 생성할 때, Temporal.ZonedDateTime 객체를 통해 상당한 이점을 제공합니다. 뛰어난 기능 중 하나는 서머타임(DST)과 같은 까다로운 상황을 포함하여 시간대를 손쉽게 처리할 수 있다는 점입니다. 예를 들어 다음과 같이 Temporal.ZonedDateTime 객체를 생성한다고 가정해 보겠습니다.

const zonedDateTime = Temporal.ZonedDateTime.from({
  year: 2024,
  month: 8,
  day: 16,
  hour: 12,
  minute: 30,
  second: 0,
  timeZone: "Europe/Madrid",
});

단순히 날짜와 시간을 설정하는 것이 아니라, 이 날짜가 지정된 시간대 내에서 정확하게 표시되도록 하는 것입니다. 이러한 정밀성 덕분에 서머타임의 변화나 현지 시간 조정이 있더라도, 설정한 날짜는 항상 정확한 시점을 반영하게 됩니다.

이 기능은 여러 지역에서 일관성을 유지해야 하는 이벤트를 예약하거나 작업을 기록할 때 특히 유용합니다. 시간대를 날짜 생성 프로세스에 직접 통합함으로써 Temporal은 서머타임 또는 시간대 차이로 인한 예기치 않은 시간 이동과 같은 기존 날짜 객체의 함정을 제거합니다. 따라서 전 세계 시간 일관성이 중요한 최신 웹 개발에서 Temporal은 편의성을 넘어 필수 요소입니다.

이 API가 왜 좋은지 궁금하다면 시간대 정의의 변경을 처리하는 방법을 설명하는 이 글을 읽어보세요.

날짜 비교

ZonedDateTime 은 compare 라는 이름의 정적 메서드를 제공하는데, 이 메서드는 주어진 2개의 ZonedDateTime one과 two에 대해서 아래와 같이 반환합니다.

  • one이 two보다 작으면 -1
  • 두 인스턴스가 시간대와 달력을 무시하고 동일한 정확한 순간을 설명하는 경우 0
  • one이 two보다 크면 1

서머타임이 종료된 후 시계 시간이 반복되거나, 실제 시간보다 늦은 값이 시계 시간보다 빠르거나, 그 반대의 경우와 같은 비정상적인 경우의 날짜를 쉽게 비교할 수 있습니다.

const one = Temporal.ZonedDateTime.from(
  "2020-11-01T01:45-07:00[America/Los_Angeles]"
);
const two = Temporal.ZonedDateTime.from(
  "2020-11-01T01:15-08:00[America/Los_Angeles]"
);

Temporal.ZonedDateTime.compare(one, two);
// => -1
// (현실 세계에서 `one` 이 더 이르기 때문에)

멋진 내장 함수

ZonedDateTime에는 몇 가지 미리 계산된 속성이 있어 작업을 더 쉽게 만들어 줍니다.

hoursInDay

hoursInDay는 읽기 전용 속성으로, 현재 날짜 시작(일반적으로 자정)부터 같은 시간대의 다음 날짜 시작까지의 실제 시간(보통 자정)을 zonedDateTime.timeZone에 반환합니다.

Temporal.ZonedDateTime.from("2020-01-01T12:00-08:00[America/Los_Angeles]")
  .hoursInDay;
// => 24
// (일반적인 날)
Temporal.ZonedDateTime.from("2020-03-08T12:00-07:00[America/Los_Angeles]")
  .hoursInDay;
// => 23
// (서머타임이 이 날 시작됩니다)
Temporal.ZonedDateTime.from("2020-11-01T12:00-08:00[America/Los_Angeles]")
  .hoursInDay;
// => 25
// (서머타임이 이 날 종료됩니다)

또 다른 유용한 속성으로는 daysInYear, inLeapYear가 있습니다.

타임존 변환

ZonedDateTimes은 원하는 대로 ZonedDateTimes을 변경할 수 있는 .withTimeZone 메서드를 제공합니다.

zdt = Temporal.ZonedDateTime.from("1995-12-07T03:24:30+09:00[Asia/Tokyo]");
zdt.toString(); // => '1995-12-07T03:24:30+09:00[Asia/Tokyo]'
zdt.withTimeZone("Africa/Accra").toString(); // => '1995-12-06T18:24:30+00:00[Africa/Accra]'

기본 연산

.add 메서드로 날짜 연산을 사용하여 기간에 날짜나 시간을 추가할 수 있습니다. 결과는 이 인스턴스의 timeZone 필드의 규칙을 사용하여 자동으로 서머타임(DST)에 맞게 자동으로 조정됩니다.

이 메서드의 가장 큰 장점은 날짜 연산 뿐만 아니라 특정 시간대와 상관없는 단순한 기간을 다룰 수 있다는 점 입니다.

  • 날짜를 더하거나 빼면 서머타임 전환 시 시계 시간이 일관되게 유지됩니다. 예를 들어 토요일 오후 1시에 약속이 있는데 하루 후에 일정을 변경해 달라고 요청하는 경우, 밤새 서머타임 전환이 있더라도 변경된 약속은 여전히 오후 1시에 있을 것으로 예상할 수 있습니다.
  • 시간을 더하거나 뺄 때는 서머타임 전환을 무시해야 합니다. 예를 들어 2시간 후에 만나자고 한 친구가 1시간 또는 3시간 후에 나타나면 짜증이 날 것입니다.
  • 작업 순서는 일관되고 예상 가능해야 합니다. 결과가 서머타임 전환 시점 또는 그 근처에 있는 경우, 모호성은 자동으로(충돌 없이) 그리고 결정적으로 처리되어야 합니다.
zdt = Temporal.ZonedDateTime.from(
  "2020-03-08T00:00-08:00[America/Los_Angeles]"
);
// 서머타임이 시작되는 다음 날을 얻기 위해 하루를 추가합니다
laterDay = zdt.add({ days: 1 });
// => 2020-03-09T00:00:00-07:00[America/Los_Angeles]
// 서머타임에 맞게 조정되었기 때문에 오프셋이 다른 것을 확인할 수 있습니다
laterDay.since(zdt, { largestUnit: "hour" }).hours;
// => 23
// 서머타임으로 인해 한시간이 줄었습니다

laterHours = zdt.add({ hours: 24 });
// => 2020-03-09T01:00:00-07:00[America/Los_Angeles]
// 시간 유닛을 더하는 것은 서머타임을 조정하지 않습니다. 결과는 1:00AM 입니다.
// 서머타임으로 인해 시간을 건너뛰었기 때문에 24시간 후입니다.
laterHours.since(zdt, { largestUnit: "hour" }).hours; // => 24

날짜 간의 차이 계산

Temporal은 .until라는 메서드를 제공하는데, 이 메서드는 zonedDateTime과 메서드의 인자값(이하 other)으로 전달된 두 시간 사이의 차이를 계산하고, 선택적으로 반올림하여 그 결과를 Temporal.Duration 객체로 반환합니다. 만약 other값이 zonedDateTime보다 이르면, 결과로 나오는 기간은 음수가 됩니다. 기본 옵션을 사용할 경우, 반환된 Temporal.Duration을 zonedDateTime에 더하면 other 값과 동일한 시간이 됩니다.

이것은 당연한 연산처럼 보일 수 있지만 전체 사양을 읽고 그 뉘앙스를 이해하는 것이 좋습니다.

결론

Temporal API는 자바스크립트에서 시간 처리 방식을 혁신적으로 변화시켜, 이 문제를 포괄적으로 해결하는 몇 안 되는 언어 중 하나로 만들었습니다. 이 글에서는 사람이 읽을 수 있는 날짜(또는 벽시계 시간)와 UTC 날짜의 차이점, 그리고 Temporal.ZonedDateTime 객체를 사용하여 이러한 날짜를 정확하게 표현하는 방법에 대해 간략히 살펴보았습니다.

다음 글에서는 Instant, PlainDate 및 Duration과 같은 다른 흥미로운 객체에 대해 살펴보겠습니다.

이번 소개가 도움이 되셨기를 바랍니다.

3개의 댓글

comment-user-thumbnail
7일 전

재밌게 잘 보고 갑니다.

답글 달기
comment-user-thumbnail
4일 전

재미있게 잘 봅니다

답글 달기
comment-user-thumbnail
33분 전

자바의 Date 쪽이랑 비슷하다고 보면되겠네요

답글 달기