특정 HTML 요소들은 날짜나 시간(또는 둘 다) 값을 사용해요. 이 문서에서는 이러한 값들을 지정할 때 사용하는 문자열의 형식에 대해 아주 자세히 설명해 드릴게요.
이러한 형식을 사용하는 요소들에는 사용자가 날짜, 시간, 혹은 두 가지 모두를 선택하거나 지정할 수 있게 해주는 특정 형태의 <input> 요소가 있어요. 또한 콘텐츠가 삽입되거나 삭제된 날짜 혹은 날짜와 시간을 지정하는 datetime 속성을 가진 <ins> 요소와 <del> 요소도 포함됩니다.
<input>의 경우, 해당 요소의 value에 날짜나 시간(또는 둘 다)을 나타내는 문자열이 포함될 때의 type 값들은 다음과 같아요.
💡 [강사님의 팁]
React나 Next.js로 개발하실 때 폼(Form) 상태 관리를 하다 보면, 위<input>타입들의value값을 핸들링하는 게 생각보다 까다로울 때가 있어요. 특히 TypeScript를 사용하신다면event.target.value는 항상string타입으로 들어오기 때문에, 이를 자바스크립트의Date객체로 변환하거나, 반대로Date객체를YYYY-MM-DD형태의 문자열로 파싱해서 넣어주는 유틸리티 함수를 꼼꼼히 만들어두는 것이 실무에서 큰 도움이 됩니다!
HTML에서 날짜와 시간 문자열이 어떻게 작성되고 파싱(해석)되는지 그 복잡한 내용을 파헤치기 전에, 가장 자주 쓰이는 날짜/시간 문자열 형식들이 어떻게 생겼는지 감을 잡을 수 있는 예제들을 먼저 살펴볼게요.
| 문자열 (String) | 날짜 및/또는 시간 (Date and/or time) | 세부 정보 |
|---|---|---|
2005-06-07 | 2005년 6월 7일 | [자세히] |
08:45 | 오전 8시 45분 | [자세히] |
08:45:25 | 오전 8시 45분 25초 | [자세히] |
0033-08-04T03:40 | 33년 8월 4일 오전 3시 40분 | [자세히] |
1977-04-01T14:00:30 | 1977년 4월 1일 오후 2시 00분 30초 | [자세히] |
1901-01-01T00:00Z | 1901년 1월 1일 자정 (UTC 기준) | [자세히] |
1901-01-01T00:00:01-04:00 | 1901년 1월 1일 미 동부 표준시(EST) 자정에서 1초 지난 시간 | [자세히] |
HTML 요소들에서 사용하는 날짜 및 시간 관련 문자열의 다양한 형식들을 살펴보기 전에, 이 형식들이 어떻게 정의되어 있는지에 대한 몇 가지 근본적인 사실을 이해하는 게 좋아요. HTML은 날짜와 시간 문자열을 표기할 때 ISO 8601 표준의 변형을 사용한답니다.
HTML 사양서에는 이 문자열들을 파싱하는 알고리즘이 포함되어 있는데, 이게 사실 ISO 8601보다 더 엄격하고 정밀해요. 그래서 기대했던 날짜/시간 문자열의 형태와 미묘한 차이가 있을 수 있으니, 여러분이 사용 중인 형식이 실제로 HTML과 완벽하게 호환되는지 형식 설명을 한 번쯤 검토해 보시는 걸 강력히 추천해요.
HTML의 날짜와 시간은 언제나 ASCII 문자 집합을 사용하는 문자열로만 표현됩니다.
HTML에서 사용하는 기본 날짜 문자열 형식을 단순하게 만들기 위해, 명세서에서는 모든 연도를 현대의 (또는 연장된, proleptic) 그레고리력(Gregorian calendar)을 사용하여 표기하도록 요구하고 있어요. 유저 인터페이스(UI) 상에서는 다른 달력을 사용해서 날짜를 입력할 수 있게끔 제공할 수 있지만, 시스템 내부적으로 다뤄지는 값은 항상 그레고리력을 사용해야 합니다.
그레고리력은 1582년이 되어서야 만들어졌지만(비슷했던 율리우스력을 대체하면서요), HTML의 목적을 위해 이 그레고리력의 규칙을 기원후 1년(1 C.E.)까지 소급해서 연장 적용해요. 혹시라도 아주 오래된 과거의 날짜를 다룰 일이 있다면 이 점을 꼭 고려하셔야 해요.
HTML 날짜 목적상, 연도는 항상 최소 4자리 숫자여야 해요. 1000년 이전의 연도에는 앞에 0(zero)을 채워 넣어서 패딩(padding)을 해줘요. 예를 들어 72년은 0072로 표기하는 식이죠. 기원후 1년 이전의 연도는 아예 지원하지 않기 때문에, HTML에서는 기원전 1년(1 B.C.E. 또는 1 B.C.) 혹은 그 이전 연도를 처리할 수 없답니다.
1년은 보통 365일이지만, 윤년(leap years)일 경우에는 예외입니다.
윤년은 연도가 400으로 나누어 떨어지거나, 또는 4로 나누어 떨어지지만 100으로는 나누어 떨어지지 않는 해를 말해요. 보통 달력상의 1년은 365일이지만, 실제로 지구라는 행성이 태양을 한 바퀴 도는 데 걸리는 시간은 약 365.2422일이에요. 윤년은 달력이 실제 지구가 궤도에 있는 위치와 동기화될 수 있도록 조율해 주는 역할을 하죠. 4년마다 달력에 하루를 더해주면 평균 1년의 길이가 365.25일이 되니까, 실제 공전 주기와 꽤 근접해지는 셈이에요.
이 알고리즘의 세부 조정들(400으로 나누어 떨어질 때는 윤년을 적용하고, 100으로 나누어 떨어질 때는 윤년을 건너뛰는 방식)은 평균 연수를 훨씬 더 정확한 일수(365.2425일)에 가깝게 맞춰줍니다. 과학자들은 남은 1만 분의 3일을 처리하고 지구 자전이 자연스럽게 점진적으로 느려지는 현상을 보상하기 위해, 아주 가끔 달력에 '윤초(leap seconds)'를 추가하기도 한답니다. (진짜예요!)
평소에 02월, 즉 2월은 보통 28일이지만, 윤년에는 29일이 됩니다.
1년에는 1월부터 12월까지 번호가 매겨진 12개의 달이 있어요. 이 달들은 항상 두 자리의 ASCII 문자열로 표현되며, 값은 01에서 12까지의 범위를 가져요. 월 번호와 그에 해당하는 영어 이름 (그리고 며칠까지 있는지)에 대한 내용은 아래의 '월의 일수' 섹션에 있는 표를 참고해 주세요.
1월, 3월, 5월, 7월, 8월, 10월, 12월은 31일로 이루어져 있어요. 4월, 6월, 9월, 11월은 30일이고요. 2월은 대부분의 해에는 28일이지만, 윤년에는 29일이 됩니다. 자세한 내용은 아래 표에 정리해 두었어요.
| 월 번호 (Month number) | 이름 (영어) (Name) | 길이 (일수) (Length in days) |
|---|---|---|
01 | January (1월) | 31 |
02 | February (2월) | 28 (윤년에는 29) |
03 | March (3월) | 31 |
04 | April (4월) | 30 |
05 | May (5월) | 31 |
06 | June (6월) | 30 |
07 | July (7월) | 31 |
08 | August (8월) | 31 |
09 | September (9월) | 30 |
10 | October (10월) | 31 |
11 | November (11월) | 30 |
12 | December (12월) | 31 |
주 문자열은 특정 연도의 몇 번째 주인지를 지정해요. 유효한 주 문자열은 유효한 연도 숫자(최소 4자리) 뒤에 하이픈 문자(-, U+002D), 그 뒤에 대문자 W(U+0057), 그리고 마지막으로 해당 연도의 주차를 나타내는 두 자리 숫자로 구성돼요.
연도의 주차는 01부터 53 사이의 두 자리 문자열이에요. 매주는 월요일에 시작해서 일요일에 끝나요. 그 말은 즉, 1월의 첫 며칠이 이전 연도의 마지막 주에 속할 수도 있고, 12월의 마지막 며칠이 다음 연도의 첫 주에 속할 수도 있다는 뜻이에요. 1년의 첫 주(1주 차)는 그 해의 첫 번째 목요일이 포함된 주를 뜻해요.
예를 들어, 1953년의 첫 번째 목요일은 1월 1일이었기 때문에, 그 주(12월 29일 월요일에 시작된 주)가 1953년의 첫 번째 주가 되는 거예요. 따라서 1952년 12월 30일은 1953-W01 주차에 속하게 됩니다.
연도가 53주를 가지는 경우는 다음과 같아요:
나머지 모든 해는 52주로 이루어져 있어요.
| 주 문자열 (Week string) | 주 및 연도 (날짜 범위) |
|---|---|
2001-W37 | 2001년 37주차 (2001년 9월 10일~16일) |
1953-W01 | 1953년 1주차 (1952년 12월 29일~1953년 1월 4일) |
1948-W53 | 1948년 53주차 (1948년 12월 27일~1949년 1월 2일) |
1949-W01 | 1949년 1주차 (1949년 1월 3일~9일) |
0531-W16 | 531년 16주차 (531년 4월 13일~19일) |
0042-W04 | 42년 4주차 (42년 1월 21일~27일) |
연도와 주 번호 모두 앞부분에 0을 채워서 연도는 4자리로, 주는 2자리로 길이를 꼭 맞춰야 한다는 점을 잊지 마세요.
월 문자열은 단순히 '1월' 같은 일반적인 달이 아니라, 시간상에서 특정한 연도와 짝지어진 달을 나타내요. 즉, "1972년 1월"처럼 연도와 월이 결합된 특정 지점이죠.
유효한 월 문자열은 유효한 연도 숫자(최소 4자리), 그 뒤에 하이픈 문자(-, U+002D), 그리고 1월을 뜻하는 01부터 12월을 뜻하는 12 사이의 두 자리 숫자 월 번호로 구성됩니다.
| 월 문자열 (Month string) | 연도 및 월 |
|---|---|
17310-09 | 17310년 9월 |
2019-01 | 2019년 1월 |
1993-11 | 1993년 11월 |
0571-04 | 571년 4월 |
0001-07 | 기원후 1년 7월 |
모든 연도는 최소 4자리 문자라는 점 주목해 주세요! 4자리보다 짧은 연도는 앞에 0이 채워져서 표현된답니다.
유효한 날짜 문자열은 월 문자열 바로 뒤에 하이픈 문자(-, U+002D)가 붙고, 그 뒤에 두 자리 숫자의 일(day)이 붙는 형식이에요.
| 날짜 문자열 (Date string) | 전체 날짜 (Full date) |
|---|---|
1993-11-01 | 1993년 11월 1일 |
1066-10-14 | 1066년 10월 14일 |
0571-04-22 | 571년 4월 22일 |
0062-02-05 | 62년 2월 5일 |
시간 문자열은 분, 초, 또는 밀리초 단위까지 정밀하게 시간을 지정할 수 있어요. 하지만 시간이나 분 한 가지만 달랑 명시하는 건 허용되지 않아요. 유효한 시간 문자열은 최소한 두 자리 시간 뒤에 콜론(:, U+003A), 그리고 두 자리 분으로 구성되어야 합니다. 분 뒤에는 선택적으로 또 다른 콜론과 두 자리 초 숫자가 올 수 있어요. 밀리초 또한 선택적으로 추가할 수 있는데, 소수점 문자(., U+002E) 뒤에 한 자리에서 세 자리 숫자를 덧붙이면 됩니다.
몇 가지 기본 규칙을 더 알아볼게요:
00이고, 오후 11시는 23이죠. 00에서 23 범위를 벗어나는 값은 절대 허용되지 않아요.00에서 59 사이의 두 자리 숫자여야 해요. 역시나 이 범위를 벗어나면 안 됩니다.00에서 59 사이여야 해요. 60이나 61 같은 값으로 윤초를 직접 지정할 수는 없습니다.| 시간 문자열 (Time string) | 시간 (Time) |
|---|---|
00:00:30.75 | 오전 12:00:30.75 (자정에서 30.75초 지난 시간) |
12:15 | 오후 12:15 |
13:44:25 | 오후 1:44:25 (오후 1시 44분에서 25초 지남) |
유효한 datetime-local 문자열은 date (날짜) 문자열과 time (시간) 문자열이 대문자 T 또는 띄어쓰기(공백) 문자로 연결된 형태예요. 문자열 안에는 시간대에 대한 정보가 전혀 포함되어 있지 않습니다. 즉, 날짜와 시간은 해당 사용자의 로컬 시간대(Local time zone) 기준이라고 가정하고 동작하는 거죠.
datetime-local input 요소의 value를 설정할 때, 이 문자열은 표준 형태로 정규화(normalized) 돼요. 정규화된 datetime 문자열은 날짜와 시간을 구분할 때 항상 대문자 T를 사용하고, 문자열의 시간 부분은 가능한 한 가장 짧게 변환된답니다. 즉, 초 부분이 :00이라면 그 부분은 쿨하게 생략해 버리는 방식이에요.
| 날짜/시간 문자열 | 정규화된 날짜/시간 문자열 | 실제 날짜와 시간 |
|---|---|---|
1986-01-28T11:38:00.01 | 1986-01-28T11:38:00.01 | 1986년 1월 28일 오전 11:38:00.01 |
1986-01-28 11:38:00.010 |
정규화를 거치고 나면 이전의 문자열과 똑같아진다는 걸 알 수 있어요. 띄어쓰기는 | 1986년 1월 28일 오전 11:38:00.01 |
0170-07-31T22:00:00 |
정규화된 형태에서는 초 값이 0임을 나타내는 | 170년 7월 31일 오후 10:00 |
글로벌 날짜 및 시간 문자열은 날짜와 시간뿐만 아니라 그 사건이 일어나는 특정 시간대(Time zone) 정보까지 포함합니다. 유효한 글로벌 날짜 및 시간 문자열은 지역 날짜/시간 문자열과 동일한 형식에다가 맨 뒤 시간 끝부분에 '시간대 문자열'을 추가로 붙인 형태예요.
시간대 오프셋 문자열은 표준 시간 기준점으로부터 양수(+) 혹은 음수(-) 방향으로 몇 시간, 몇 분이 차이 나는지를 나타내요. 여기에는 두 가지 표준 시간 기준이 있는데 서로 아주 비슷하지만 완벽하게 똑같지는 않아요:
Z이며, 오프셋은 경도 0도(영국 그리니치 천문대를 지나는)의 본초 자오선 시간으로부터 해당 지역 시간대가 얼마나 차이 나는지를 나타냅니다.시간대 문자열은 날짜 및 시간 문자열의 시간 바로 뒤에 찰싹 붙여 적습니다. 만약 시간이 UTC 기준으로 지정되어 있다면, 시간대 오프셋 자리에 알파벳 Z만 적어주면 돼요. 그 외의 경우에는 아래의 방식대로 오프셋을 만들어 붙입니다:
+, U+002B)를, 서쪽에 있다면 마이너스 문자(-, U+002D)를 씁니다.00에서 23 사이여야 해요.:) 문자.00에서 59 사이여야 해요.이 형식은 이론상 -23:59부터 +23:59까지의 시간대를 모두 표현할 수 있지만, 현재 존재하는 시간대 오프셋 범위는 -12:00에서 +14:00까지예요. 게다가 현재 어느 국가의 시간대도 정각에서 00, 30, 45분이 아닌 다른 애매한 분 단위로 오프셋을 적용하는 곳은 없습니다. 물론 각 국가는 원할 때 마음대로 본인들의 시간대 정책을 변경할 수 있으니, 나중에는 얼마든지 달라질 가능성도 열려 있어요.
| 글로벌 날짜 및 시간 문자열 | 실제 글로벌 날짜 및 시간 | 본초 자오선 기준 날짜 및 시간 |
|---|---|---|
2005-06-07T00:00Z | 2005년 6월 7일 자정 (UTC) | 2005년 6월 7일 자정 |
1789-08-22T12:30:00.1-04:00 | 1789년 8월 22일 미 동부 일광 절약 시간대(EDT) 오후 12시 30분에서 0.1초 지남 | 1789년 8월 22일 오후 4시 30분에서 0.1초 지남 |
3755-01-01 00:00+10:00 | 3755년 1월 1일 호주 동부 표준시(AEST) 자정 | 3754년 12월 31일 오후 2시 00분 |
컴퓨터의 데이터 저장 방식이나 정밀도의 한계 때문에, 클라이언트(브라우저) 측과 서버 측에서 발생할 수 있는 몇 가지 재밌는 이슈들을 알아두시면 좋습니다.
💡 [강사님의 팁]
이런 숫자 정밀도와 관련된 날짜 이슈는 프론트엔드 면접에서 JavaScript의 근본적인 숫자 처리 방식에 대한 깊이를 물어볼 때 은근히 자주 등장하는 단골 질문이기도 해요. 자바스크립트는 숫자를 64비트 부동소수점(double precision float) 방식으로 처리한다는 점을 면접관에게 곁들여 설명하면 아주 훌륭한 대답이 될 거예요!
자바스크립트는 다른 모든 숫자들과 마찬가지로 날짜를 저장할 때 배정도 부동소수점(double precision floating points) 방식을 사용해요. 즉, 개발자가 강제로 정수 변환을 하거나 비트 연산(자바스크립트의 모든 비트 연산자는 32비트 부호 있는 2의 보수 정수를 사용하니까요)을 난무하지 않는 이상, 자바스크립트 코드 자체는 Y2K38 문제의 직격탄을 맞지 않습니다.
진짜 문제는 서버 측에 있어요. 2^31 - 1 보다 큰 날짜 값을 저장해야 할 때 터지죠. 이 문제를 해결하려면 서버에서 날짜를 저장할 때 부호 없는(unsigned) 32비트 정수나 부호 있는 64비트 정수, 또는 배정도 부동소수점 방식을 사용하도록 싹 고쳐야 해요. 만약 서버가 PHP로 작성되어 있다면, PHP를 최신 버전으로 업그레이드하고 하드웨어 자체도 x86_64나 IA64 아키텍처로 업그레이드해야 할 수도 있습니다. 기존 하드웨어에 묶여 있다면 32비트 가상 머신 안에서 64비트 하드웨어를 에뮬레이션해 볼 수도 있겠지만, 안정성도 떨어지고 성능 저하는 불 보듯 뻔해서 대부분의 VM들은 이런 가상화를 지원하지 않아요.
많은 서버에서는 날짜를 문자열이 아니라 특정 크기의 숫자 포맷으로 저장해요. 형식에 얽매이지 않고 (엔디언 문제는 논외로 치더라도요) 단순 숫자로 기록하죠. 10,000년이 지난 후에도 이 숫자들은 그저 평소보다 자릿수가 좀 더 커질 뿐이라서, 상당수의 서버들은 10,000년 이후에 제출된 폼 데이터를 받아도 큰 문제 없이 버틸 수 있어요.
진짜 치명적인 문제는 바로 클라이언트 측, 프론트엔드 코드에 있습니다. 연도가 4자리를 넘어가게 되면 날짜 문자열 파싱에서 에러가 터지거든요!
<input type="datetime-local" value="+010000-01-01T05:00" />
우리는 연도가 꼭 5자리뿐만 아니라, 그 이상 얼마나 늘어나든 처리할 수 있도록 코드를 단단히 준비해 둬야 해요. 아래 자바스크립트 함수는 이런 값을 프로그래밍 방식으로 안전하게 세팅하는 예시입니다.
function setValue(element, date) {
const isoString = date.toISOString();
element.value = isoString.substring(0, isoString.indexOf("T") + 6);
}
여러분이 죽고 난 뒤 아주 먼 훗날의 세기에나 일어날 Y10K 문제를 왜 지금 벌써 걱정해야 하냐고요? 아이러니하게도 여러분이 이미 이 세상에 없기 때문에, 여러분의 소프트웨어를 사용하는 회사들은 코드를 깊이 파악하고 고쳐줄 원작자 없이 그 망가진 소프트웨어 안에 고립될 수밖에 없기 때문이랍니다. (책임감을 가지자고요!)
<input><ins> 와 <del>: 콘텐츠가 삽입되거나 삭제된 날짜 혹은 지역 날짜와 시간을 지정하는 datetime 속성을 참고하세요.Date 객체Intl.DateTimeFormat 객체