본문으로 건너뛰기
2026. 7. 19
© WONKOOK LEE

이 필드는 UTC여야 할까요?

하나의 값이 위 눈금자의 경계에는 꼭 맞고 아래 눈금자는 칸 한가운데를 가로지르는 그림

프론트엔드에서 시간을 다루며 매번 어렴풋이 넘겼던 것들을 세 편에 걸쳐 정리하는 시리즈입니다. 개념 → 언어 → 계약 순으로 내려갑니다.

  1. '오후 3시'는 언제인가요?
  2. 왜 날짜만 넣었는데 하루 전이 나올까요?
  3. 이 필드는 UTC여야 할까요? — 이 글

"시간은 전부 UTC로 통일하자"는 말을 여러 번 들었고 저도 여러 번 했습니다. 대체로 좋은 조언입니다. 그런데 어느 날 생일 필드를 UTC로 저장한 뒤로 미국 사용자들의 생일이 하루씩 앞당겨 표시되는 것을 보고 나서, 이 조언에 조건이 붙어 있었다는 걸 알았습니다.

조건은 "절대시각이라면" 입니다. 그리고 우리가 시간 필드라고 부르는 것들 중 절대시각은 절반쯤밖에 되지 않습니다.

어떤 시간 필드를 UTC로 두고 어떤 필드를 날짜로 두어야 할까요? 그 판단 기준은 무엇일까요?


이 글에서 다루는 내용

1편에서 시간의 종류를 나눴고 2편에서 Date 하나로 그것을 다 담으려다 생기는 문제를 봤습니다. 이 글은 그 구분을 API 계약과 저장소 스키마로 옮깁니다.

어떤 필드가 UTC 절대시각이어야 하고 어떤 필드가 날짜여야 하는지, 미래의 약속을 절대시각으로 굳혀 저장하면 왜 나중에 틀리는지, DB·서버·클라이언트가 각각 무엇을 책임져야 하는지를 봅니다. 마지막으로 이 구분을 타입 시스템이 대신 지켜주는 Temporal의 현재 상태(2026년 7월 기준)와 도입 전략을 정리합니다.





1. 계약은 시간의 종류를 드러내야 합니다

가장 흔한 실수는 시간 필드를 전부 같은 모양으로 내려보내는 것입니다. createdAtbirthDatedueDate도 전부 2026-07-30T00:00:00Z라면, 받는 쪽은 이 값들을 똑같이 다룰 수밖에 없습니다. 계약이 구분을 잃어버린 순간 클라이언트는 추측을 시작합니다.


여섯 가지 모양

1편의 분류를 계약 필드로 옮기면 이렇게 됩니다.

의미표기클라이언트가 해야 할 일
절대시각2026-07-30T06:00:00Z가입 시각, 결제 승인 시각사용자 타임존으로 변환해 표시
로컬 날짜2026-07-30생일, 근무일, 급여 지급일변환하지 않고 그대로 표시
로컬 일시2026-07-30T09:00:00지점의 현지 오픈 시각그 지점 기준으로 해석
로컬 시각09:00근무 시작 시각 규칙날짜와 조합해서 해석
미래의 약속2026-07-30T09:00:00[Asia/Seoul]예약, 반복 일정표시 시점에 절대시각으로 환산
기간PT8H30M근로시간, 영상 길이시각이 아니므로 변환 금지

표기가 다르면 클라이언트가 실수하기 어려워집니다. 2026-07-30을 받은 쪽은 여기에 시차를 적용할 생각을 잘 하지 않습니다. 반면 2026-07-30T00:00:00Z를 받으면 절대시각처럼 보이니 습관적으로 변환하고, 그 순간 하루가 밀립니다.


이름도 계약의 일부입니다

표기만큼 이름도 중요합니다. 뒤에 At을 붙이면 절대시각, Date를 붙이면 날짜라는 규칙을 팀에서 정해 두면 대부분의 오해가 사라집니다.

{
"createdAt": "2026-07-30T06:00:00Z", // At → 절대시각
"birthDate": "1994-03-07", // Date → 로컬 날짜
"workDate": "2026-07-30", // Date → 로컬 날짜
"workedHours": "PT8H30M", // 기간
"reservation": { // 미래의 약속은 쪼개서 보낸다
"localDateTime": "2026-07-30T15:00:00",
"timeZone": "Asia/Seoul"
}
}

이름이 계약을 말해주면 코드 리뷰에서 잡힙니다. birthDateAt처럼 어정쩡한 이름이 보이면 그 자리에서 무엇인지 묻게 됩니다. 이건 null이 비우기인지 건드리지 않기인지 계약으로 정하는 일과 같은 종류의 문제입니다 — 값에서 의도를 추측하게 두지 않고 계약에 명시하는 것입니다.


Recap

시간 필드를 전부 UTC 문자열로 통일하면 계약이 구분을 잃고 클라이언트가 추측하게 됩니다. 절대시각·로컬 날짜·로컬 일시·로컬 시각·미래의 약속·기간은 각각 다른 표기를 써야 하며, 표기와 필드명이 함께 의도를 드러내면 오해가 코드 리뷰 단계에서 잡힙니다.




2. 날짜가 도메인일 때

"UTC로 통일"이 특히 위험해지는 영역이 있습니다. 날짜 자체가 도메인의 단위인 경우입니다.


근태와 급여

이 블로그의 도메인 문서에서 다루는 근태·급여 계산은 거의 전부 날짜로 이루어져 있습니다.

  • 2026년 7월 30일이 근무일인지 휴일인지
  • 그날 몇 시간을 일했는지
  • 월급의 산정 기간이 며칠부터 며칠까지인지
  • 급여 지급일이 언제인지

이 값들에는 시각이 없습니다. "7월 30일의 근무"는 그날의 어떤 순간이 아니라 그날 자체입니다. 여기에 T00:00:00Z를 붙이는 순간 이 값은 절대시각이 되고, 조회하는 사람의 타임존에 따라 7월 29일로도 7월 30일로도 보이게 됩니다.

급여 도메인에서 하루가 밀리는 것은 표시가 어긋나는 정도의 문제가 아닙니다. 주휴수당의 요건이 달라지고 연장근로 한도 판정이 달라집니다. 날짜가 판정의 근거인 도메인에서는 날짜를 날짜로 저장해야 합니다.


생일이 하루 앞당겨지는 이유

서두에서 말한 버그를 풀어 보겠습니다.

저장:  1994-03-07  →  1994-03-07T00:00:00Z 로 변환해서 보관

서울에서 조회: 1994-03-07 09:00 → 3월 7일 ✓
로스앤젤레스에서: 1994-03-06 16:00 → 3월 6일 ✗

America/Los_Angeles-08:00이므로 UTC 자정은 전날 오후 4시입니다. 클라이언트가 이 값을 절대시각으로 받아 로컬 렌즈로 날짜를 뽑으면 3월 6일이 나옵니다.

흔한 대응은 "UTC로 표시하자"입니다. 그러면 생일은 맞는데 같은 화면의 가입 시각이 이상해집니다. 진짜 해법은 생일을 절대시각으로 취급하지 않는 것입니다. "1994-03-07"이라는 문자열을 그대로 주고받고, 화면에도 그대로 표시하면 됩니다. 변환할 것이 없으면 변환 버그도 없습니다.


판별 질문

필드 하나를 놓고 고민될 때는 이 질문 하나면 대체로 결론이 납니다.

지구 반대편 사람이 이 값을 볼 때, 다른 날짜나 다른 시각으로 보여야 자연스러운가?

성격표기
그렇다절대시각2026-07-30T06:00:00Z
아니다로컬 날짜/일시2026-07-30, 2026-07-30T09:00:00

"결제가 승인된 순간"은 뉴욕 사용자에게 뉴욕 시각으로 보여야 자연스럽습니다 — 절대시각입니다. "생일"과 "근무일"은 어디서 보든 같은 날짜여야 자연스럽습니다 — 로컬 날짜입니다.


Recap

날짜가 판정의 근거인 도메인에서는 날짜를 절대시각으로 바꾸는 순간 조회 타임존에 따라 하루가 밀립니다. 근태·급여처럼 날짜 단위로 규칙이 걸린 영역에서는 이것이 표시 오류가 아니라 계산 오류가 됩니다. "지구 반대편에서 다른 값으로 보여야 자연스러운가"라는 질문 하나로 대부분의 필드를 분류할 수 있습니다.




3. 미래의 약속은 절대시각이 아닙니다

여기가 이 글에서 가장 반직관적인 부분입니다. 결제 시각처럼 이미 일어난 일은 절대시각으로 저장하는 것이 맞습니다. 그런데 앞으로 일어날 일은 그렇지 않습니다.


tzdb가 바뀌면 미래의 점이 움직입니다

1편 4절에서 타임존 규칙이 정치적 결정으로 계속 바뀐다고 했습니다. 그 사실이 여기서 청구서로 돌아옵니다.

2026년 1월에 밴쿠버 지사와의 회의를 잡았다고 하겠습니다. 12월 25일 현지 시각 오전 9시입니다. 당시 규칙으로 밴쿠버의 12월은 -08:00이었으므로, 이걸 절대시각으로 환산하면 2026-12-25T17:00:00Z입니다. 이 값을 DB에 저장했습니다.

그리고 3월에 브리티시컬럼비아가 시계를 앞당긴 뒤 되돌리지 않기로 했습니다. tzdb 2026b가 이 변경을 반영했고, 이제 밴쿠버의 12월은 -07:00입니다.

저장한 값:  2026-12-25T17:00:00Z   (변하지 않는다)
당시 의도: 밴쿠버 오전 9시
지금 해석: 17:00Z − 07:00 = 밴쿠버 오전 10시 ← 한 시간 어긋났다

tzdb 갱신 전후로 같은 UTC 값이 서로 다른 현지 시각을 가리키게 되는 타임라인

저장한 값은 그대로인데 의미가 달라졌습니다. 회의는 원래 오전 9시에 하기로 한 것이지 17:00Z에 하기로 한 것이 아닙니다. 절대시각으로 굳히는 순간 "오전 9시"라는 원래 의도가 사라졌고, 규칙이 바뀌자 되살릴 방법이 없어졌습니다.

이건 가정이 아닙니다. 2026년에만 브리티시컬럼비아·앨버타·모로코 세 곳에서 실제로 일어났고, 모로코의 변경(9월 20일)은 이 글을 쓰는 시점 기준으로 아직 오지 않은 미래입니다. 그 지역의 9월 이후 일정을 절대시각으로 저장해 둔 시스템이 있다면 이미 어긋나 있습니다.


그래서 무엇을 저장하나

미래의 약속은 의도를 그대로 저장합니다. 벽시계시각과 타임존 식별자를 함께 두고, 절대시각은 필요할 때 계산합니다.

// 나쁨 — 의도가 사라진다
{ "meetingAt": "2026-12-25T17:00:00Z" }

// 좋음 — 의도가 남는다
{
"meetingLocalDateTime": "2026-12-25T09:00:00",
"meetingTimeZone": "America/Vancouver"
}

// RFC 9557 한 줄로도 같은 정보를 담을 수 있다
{ "meetingAt": "2026-12-25T09:00:00-08:00[America/Vancouver]" }

RFC 9557 형태는 오프셋과 타임존을 함께 담기 때문에 규칙이 바뀌었는지 감지할 수도 있습니다. 저장된 오프셋과 지금 계산한 오프셋이 다르면 규칙이 바뀐 것이니, 사용자에게 "이 일정의 시각이 변경되었습니다"라고 알려줄 수 있습니다. 캘린더 앱들이 하는 일이 정확히 이것입니다.

반복 일정은 더 말할 것도 없습니다. iCalendar 표준(RFC 5545)이 DTSTARTTZID를 붙이도록 한 것도, 매주 화요일 오전 10시라는 규칙을 절대시각의 나열로는 표현할 수 없기 때문입니다.


과거와 미래는 반대로 다룹니다

저장 형태이유
과거 기록절대시각 (…Z)이미 일어난 순간은 변하지 않는다
미래 약속로컬 일시 + 타임존 ID규칙이 바뀌어도 의도가 보존된다

과거 기록에 "그때 현지 시각이 몇 시였는지"를 보여줘야 한다면 타임존 식별자를 함께 저장합니다. 절대시각만으로는 복원할 수 없기 때문입니다. 다만 그 조합도 완벽하진 않습니다 — tzdb는 과거 데이터를 정정하기도 하므로, 법적 증빙처럼 엄밀함이 필요하면 당시 표시했던 문자열 자체를 남겨두는 편이 확실합니다.


Recap

미래 시각을 절대시각으로 굳혀 저장하면 tzdb 규칙이 바뀔 때 원래 의도와 어긋나고, 벽시계시각을 이미 잃었으므로 되살릴 수 없습니다. 2026년의 브리티시컬럼비아·앨버타·모로코 변경이 실제 사례입니다. 미래의 약속은 로컬 일시와 타임존 식별자로 저장하고 절대시각은 그때그때 계산해야 하며, 과거 기록은 반대로 절대시각으로 저장합니다.




4. 경계 설계 — 누가 무엇을 책임지나

계약이 정해졌으면 각 계층이 무엇을 지켜야 하는지도 정해집니다.


데이터베이스

DB 타입은 이미 시간의 종류를 구분하고 있습니다. 다만 이름이 헷갈리게 지어져 있을 뿐입니다.

DB절대시각로컬 일시로컬 날짜
PostgreSQLtimestamptztimestampdate
MySQLTIMESTAMPDATETIMEDATE

PostgreSQL의 timestamptz는 이름과 달리 타임존을 저장하지 않습니다. 입력값을 UTC로 정규화해 보관하고 조회 시 세션 타임존으로 되돌려줄 뿐입니다. 즉 절대시각 타입입니다. 반대로 timestamp는 아무 변환도 하지 않는 로컬 일시 타입입니다.

MySQL도 구조는 같지만 함정이 하나 더 있습니다. TIMESTAMP는 32비트 유닉스 시간을 쓰기 때문에 2038년 1월 19일이 상한입니다. 만료일이나 예약처럼 먼 미래를 담는 컬럼에는 쓸 수 없습니다. 그래서 실무에서는 DATETIME에 UTC 값을 넣고 애플리케이션 계층에서 규율을 잡는 선택을 자주 합니다.


서버

  • 프로세스 타임존을 UTC로 고정합니다. DB 세션 타임존도 마찬가지입니다. 어딘가에서 로컬 렌즈에 의존하고 있다면 개발 단계에서 드러나게 하는 편이 낫습니다.
  • 응답에는 항상 오프셋을 붙입니다. 2026-07-30T06:00:00처럼 오프셋 없는 절대시각을 내려보내는 것은 계약 결함입니다. 받는 쪽이 UTC로 읽을지 로컬로 읽을지 알 수 없습니다.
  • 판정은 서버에서 합니다. 2편에서 본 것처럼 기기 시계는 틀릴 수 있고, 사용자 브라우저의 tzdb는 낡아 있을 수 있습니다.

클라이언트

표시 타임존을 무엇으로 삼을지 정하는 것이 클라이언트의 핵심 결정입니다. 선택지는 둘입니다.

방식적합한 서비스주의점
브라우저 감지 (resolvedOptions().timeZone)개인용 서비스, 커머스출장·VPN으로 값이 바뀐다
사용자 설정값조직용 서비스, 근태·급여설정 UI와 기본값 정책이 필요하다

근태나 급여처럼 조직의 시간 기준이 따로 있는 서비스는 대개 후자가 맞습니다. 직원이 해외 출장 중에 앱을 열었다고 해서 근무일이 바뀌면 곤란하기 때문입니다. 브라우저 감지값은 "설정과 다른데 괜찮으신가요"라고 물어보는 힌트로 쓰는 정도가 적당합니다.


집계의 경계는 누구의 하루인가

마지막으로 자주 놓치는 지점입니다. "7월 매출"은 누구의 7월일까요?

-- 이 쿼리의 경계는 DB 세션 타임존에 달려 있다
SELECT sum(amount) FROM orders
WHERE created_at >= '2026-07-01' AND created_at < '2026-08-01';

created_at이 절대시각이라면 이 경계는 조회 환경에 따라 아홉 시간씩 움직입니다. 서울 기준 7월 1일 0시부터 세야 한다면 그 사실을 쿼리가 명시해야 합니다.

-- 기준 타임존을 계약으로 드러낸다
SELECT sum(amount) FROM orders
WHERE created_at >= timestamptz '2026-07-01 00:00+09'
AND created_at < timestamptz '2026-08-01 00:00+09';

집계 API라면 기준 타임존을 파라미터로 받는 것이 정직합니다. 받지 않는다면 문서에 "모든 집계는 Asia/Seoul 기준"이라고 못박아야 합니다. 그것도 하지 않으면 대시보드 숫자가 안 맞는다는 문의가 분기마다 들어옵니다.


Recap

DB 타입은 이미 절대시각·로컬 일시·로컬 날짜를 구분하고 있으므로 의미에 맞는 타입을 골라야 하며, MySQL TIMESTAMP의 2038년 상한처럼 타입별 제약도 함께 봐야 합니다. 서버는 UTC로 고정하고 응답에 오프셋을 반드시 붙이며 판정을 책임집니다. 클라이언트는 표시 타임존을 브라우저 감지로 할지 사용자 설정으로 할지 정해야 하고, 집계 API는 기준 타임존을 계약에 드러내야 합니다.




5. Temporal — 타입이 계약을 대신 말합니다

여기까지 온 규율을 사람이 지키는 데는 한계가 있습니다. Date로는 "이 값은 로컬 날짜입니다"라고 표현할 방법이 없어서, 변수명과 주석과 코드 리뷰에 기대야 했습니다. Temporal은 그 구분을 타입으로 만듭니다.


개념이 그대로 타입이 됐습니다

1편에서 나눈 종류들과 Temporal의 타입이 거의 일대일로 대응합니다. Java의 java.time을 써 보신 분이라면 더 익숙할 것입니다.

1편의 개념Temporaljava.time계약 표기
절대시각Temporal.InstantInstant2026-07-30T06:00:00Z
로컬 날짜Temporal.PlainDateLocalDate2026-07-30
로컬 시각Temporal.PlainTimeLocalTime09:00:00
로컬 일시Temporal.PlainDateTimeLocalDateTime2026-07-30T09:00:00
미래의 약속Temporal.ZonedDateTimeZonedDateTime…+09:00[Asia/Seoul]
연·월Temporal.PlainYearMonthYearMonth2026-07
월·일Temporal.PlainMonthDayMonthDay03-07
기간Temporal.DurationDuration / PeriodPT8H30M

타입이 다르면 섞이지 않습니다. PlainDate에는 시차를 적용할 메서드 자체가 없고, TypeScript를 쓴다면 잘못된 조합이 타입 검사에서 걸립니다. 계약에서 정한 구분이 코드에서도 강제되는 셈입니다.


몇 가지 설계 결정

// 1. 불변이다 — add는 새 객체를 돌려준다
const d = Temporal.PlainDate.from("2026-01-31");
d.add({ months: 1 }); // 2026-02-28 (넘친 날짜를 잘라낸다)
d.toString(); // "2026-01-31" — 원본은 그대로

// 2. 애매한 연산은 옵션으로 명시하게 한다
d.add({ months: 1 }, { overflow: "reject" }); // RangeError를 던진다

// 3. DST의 모호함도 옵션이다
Temporal.PlainDateTime.from("2026-11-01T01:30:00")
.toZonedDateTime("America/New_York", { disambiguation: "earlier" });
// 두 번 오는 01:30 중 앞의 것 (-04:00)
// "later"는 뒤의 것, "reject"는 예외, 기본값 "compatible"은 앞의 것

2편 4절에서 본 함정들이 여기서는 선택지로 드러납니다. Date는 조용히 3월 3일에 착지하고 조용히 01:00으로 밀어냈지만, Temporal은 어느 쪽을 원하는지 묻거나 최소한 기본값이 무엇인지 문서에 적어 둡니다.

그리고 타임존과 캘린더는 객체가 아니라 문자열 ID로 다룹니다. 표준화 과정에서 커스텀 타임존·캘린더 객체를 지원하려던 설계를 걷어내고 단순화한 결과입니다.

Temporal.Now.timeZoneId();     // "Asia/Seoul" — 메서드다 (괄호 필요)
Temporal.Now.instant(); // 현재 절대시각
Temporal.Now.plainDateISO(); // 오늘 날짜 (로컬)
Temporal.Now.plainDateISO("America/New_York"); // 뉴욕의 오늘

toString()1편 5절에서 본 RFC 9557 형식을 그대로 뱉습니다. 계약 표기와 언어 타입이 같은 표준을 공유하게 된 셈입니다.


2026년 7월 현재 상태

여기가 실무 판단에 필요한 부분입니다.

항목상태
표준화2026년 3월 TC39 Stage 4, ES2026에 포함
Chrome / Edge144부터 네이티브 지원 (2026년 1월)
Firefox139부터 네이티브 지원
SafariTechnology Preview에 플래그로만. 정식 릴리스 아직
BaselineSafari 때문에 아직 아님. 전역 커버리지 대략 64%
Node.js26(2026년 5월)부터 플래그 없이 사용 가능

서버는 지금 바로 쓸 수 있고, 브라우저는 폴리필이 필요합니다.


도입 전략

무리 없는 순서는 이렇습니다.

첫째, 서버부터 시작합니다. Node 26 이상이라면 폴리필 없이 도입할 수 있고, 실행 환경이 하나라 리스크가 낮습니다. 시간 관련 로직이 서버에 몰려 있는 서비스라면 이것만으로 대부분의 이득을 얻습니다.

둘째, 경계에서만 변환합니다. 외부 라이브러리나 브라우저 API는 여전히 Date를 요구합니다. 도메인 안쪽은 Temporal로 두고 경계에서 바꿔 끼웁니다.

// Date → Temporal
const instant = legacyDate.toTemporalInstant();
const zdt = instant.toZonedDateTimeISO("Asia/Seoul");

// Temporal → Date
const legacy = new Date(instant.epochMilliseconds);

// 문자열 ↔ Temporal
Temporal.Instant.from("2026-07-30T06:00:00Z");
Temporal.PlainDate.from("2026-07-30");

셋째, 브라우저는 폴리필로 메웁니다. 선택지는 셋입니다.

패키지성격비고
@js-temporal/polyfill챔피언들이 만든 레퍼런스 구현명세 충실. gzip 약 45KB
temporal-polyfillFullCalendar 팀의 경량 구현더 작고, 트리셰이킹용 함수 API도 제공
temporal-polyfill-lite그레고리력만 쓰는 경우의 최소 구현가장 작지만 범위가 좁다

45KB는 결코 작지 않습니다. 그래서 모든 페이지에 무조건 얹기보다는 시간 로직이 몰린 화면에서만 동적으로 불러오거나, Safari 지원이 붙을 때까지 클라이언트는 Intl + 문자열로 버티는 선택도 충분히 합리적입니다.


Recap

Temporal은 1편에서 나눈 시간의 종류를 그대로 타입으로 만들어, 계약의 구분을 코드가 강제하게 합니다. 불변 설계에 애매한 연산은 옵션으로 명시하게 하고 직렬화는 RFC 9557을 따릅니다. 2026년 7월 기준으로 ES2026에 포함되었고 Chrome·Edge·Firefox·Node 26에서 네이티브로 쓸 수 있지만 Safari가 아직이라 Baseline은 아니므로, 서버부터 도입하고 브라우저는 폴리필 비용을 따져 결정하는 것이 현실적입니다.




6. 그래서 지금 무엇을 쓸까요

정리하면 선택지는 다섯 가지 정도입니다.

도구언제 고르나유의할 점
Date + Intl표시가 전부인 화면. 의존성을 늘리기 싫을 때산술은 하지 않는다는 규율이 필요하다
date-fns함수 단위 트리셰이킹이 중요할 때타임존은 v4의 @date-fns/tz로 별도 조합
Day.js기존 Moment 코드를 최소 변경으로 옮길 때플러그인 조합이 늘면 이점이 줄어든다
Luxon타임존 다루기가 잦고 API 일관성이 중요할 때Intl 의존이라 구형 환경에서 주의
Temporal새로 시작하는 코드. 서버 사이드브라우저는 폴리필 비용 검토

Moment.js는 공식적으로 유지보수 모드에 들어간 지 오래입니다. 새 코드에 넣을 이유는 없습니다.

그런데 이 표보다 중요한 것이 하나 있습니다. 어떤 도구를 쓰든, 값의 종류를 구분하지 않으면 같은 버그가 납니다. Luxon으로도 생일을 DateTime에 담으면 하루가 밀리고, Temporal을 써도 계약이 2026-07-30T00:00:00Z를 내려주면 받는 쪽에서 Instant로 읽게 됩니다.

도구는 규율을 거들 뿐입니다. 규율 자체는 계약에서 시작합니다.


Recap

라이브러리는 표시 위주인지, 타임존 연산이 잦은지, 새로 시작하는 코드인지에 따라 고르면 되고 Moment는 새 코드에서 제외합니다. 다만 어떤 도구를 쓰든 계약이 값의 종류를 구분해주지 않으면 같은 버그가 반복되므로, 도구 선택보다 계약 설계가 먼저입니다.




시리즈 정리

세 편을 한 장으로 줄이면 이렇습니다.

다룬 것남길 문장
1편시간의 개념 모델오프셋은 결과값이고 타임존은 규칙입니다
2편JavaScript Date의 함정Date는 숫자 하나에 보이지 않는 로컬 렌즈가 달린 타입입니다
3편API 계약과 Temporal과거는 절대시각으로, 미래는 벽시계시각과 타임존으로 저장합니다

그리고 세 편을 관통하는 질문 하나가 있습니다.

이 값은 지구 반대편에서 다른 숫자로 보여야 하는가?

이 질문에 답할 수 있으면 타입도 표기도 저장 형태도 따라 나옵니다. 답하지 못한 채로 new Date()를 던지는 순간, 그 값은 실행 환경의 타임존이라는 보이지 않는 인자를 하나 더 받게 됩니다. 시간 버그가 늘 남의 자리에서만 재현되는 이유입니다.




References

Temporal

  1. TC39 — Temporal proposal
  2. MDN — Temporal
  3. MDN — Date.prototype.toTemporalInstant()
  4. fullcalendar/temporal-polyfill
  5. @js-temporal/polyfill

표준 문서

  1. RFC 9557 — Timestamps with Additional Information
  2. RFC 5545 — iCalendar
  3. IANA — Time Zone Database

데이터베이스

  1. PostgreSQL — Date/Time Types
  2. MySQL — The DATE, DATETIME, and TIMESTAMP Types

라이브러리

  1. date-fns
  2. Luxon
  3. Moment.js — Project Status


좋은 사람들과 재미있는 일을 하며 열정적이고 즐겁게 살고 싶은 개발자