CS · Time · Backend

ISO 8601 - 시각을 글자로 적는 법

03/04/2025가 3월 4일인지 4월 3일인지는 읽는 사람의 나라가 정한다. 그 다툼을 끝내려고 정한 꼴이 ISO 8601이고, 큰 단위부터 적는다는 규칙 하나에서 나머지 성질이 전부 따라 나온다.

·시간이라는 데이터 7편
목차
  1. 같은 글자가 두 날짜로 읽힌다
  2. 큰 단위부터 적는다
  3. T와 Z는 무엇인가
  4. 오프셋을 붙이는 꼴은 셋이다
  5. 오프셋이 있어도 지역은 모른다
  6. 규격은 생각보다 넓다
  7. 사람에게 보여 줄 꼴은 다른 문제다
  8. 실무에서: 만들 때보다 읽을 때가 위험하다
  9. 정리

시각을 글자로 적어야 하는 순간이 온다. 파일에 남기거나 네트워크로 보낼 때다.

같은 글자가 두 날짜로 읽힌다

03/04/2025가 며칠인가.

미국에서 읽으면 3월 4일이고 유럽에서 읽으면 4월 3일이다. 글자는 하나인데 뜻이 둘이다. 그리고 어느 쪽으로 읽었는지 알 방법이 값 안에 없다.

이건 같은 수가 표에 따라 다른 글자로 읽히던 것과 정확히 같은 모양이다. 다만 여기서는 표가 나라의 습관이라 더 조용히 어긋난다. 1일부터 12일 사이에는 두 해석 다 그럴듯한 날짜가 나와서 틀린 걸 눈치채기 어렵다.

이 다툼을 끝내려고 정한 꼴이 ISO 8601이다.

큰 단위부터 적는다

규칙의 핵심은 하나다. 큰 단위를 먼저 적는다.

2025년
-
09월
-
30일
T
15시
:
00분
:
00초
+09:00오프셋
큰 단위 작은 단위
자릿수가 고정이라 앞에서 잘라도 뜻이 남는다
2025-09-30그날
2025-09그달
2025그해

왼쪽일수록 큰 단위라 글자 정렬이 곧 시간 순서가 된다. 끝의 오프셋이 있어야 이 값이 어느 순간인지 정해진다.

년, 월, 일, 시, 분, 초. 왼쪽으로 갈수록 크다. 그리고 각 자리는 0으로 채워 자릿수를 고정한다. 3월은 3이 아니라 03이다.

이 두 가지에서 좋은 성질이 따라 나온다.

  • 읽는 순서에 다툼이 없다. 앞의 넷은 년, 그다음 둘은 월이다
  • 글자 정렬이 곧 시간 순서가 된다. 자릿수가 고정이라 사전순 비교가 대소 비교와 같아진다
  • 앞에서 잘라도 뜻이 산다. 2025-09는 9월, 2025는 그해다

두 번째가 실무에서 특히 값지다. 시각을 문자열로 들고 있어도 그냥 정렬하면 시간순이 된다. 로그 파일을 정렬하는 것만으로 시간순이 되는 이유가 이것이다.

다만 오프셋이 섞이면 이 성질이 깨진다. 2025-09-30T15:00:00+09:00과 2025-09-30T07:00:00Z는 같은 순간인데 글자로는 앞엣것이 커 보인다. 문자열 정렬에 기대려면 전부 같은 오프셋으로, 대개 Z로 맞춰 두어야 한다.

T와 Z는 무엇인가

두 글자가 눈에 걸린다.

**T**는 날짜와 시각을 가르는 구분자다. 2025-09-30T15:00:00에서 가운데 있는 것. 공백을 써도 사람은 읽지만, 공백은 값이 잘리거나 따옴표가 필요해지는 자리를 만든다. 그래서 규격은 T를 쓴다.

**Z**는 오프셋 0을 뜻한다. +00:00의 줄임이다. 군용 시간대 이름에서 온 표기라 “줄루”라고 읽기도 한다. Z는 지역이 아니라 기준선 그 자체라는 걸 기억하면 된다.

오프셋을 붙이는 꼴은 셋이다

끝에 붙는 부분이 이 값의 뜻을 정한다.

plaintext
2025-09-30T15:00:00+09:00   오프셋 붙음 - 순간이 하나로 정해진다
2025-09-30T06:00:00Z        오프셋 0   - 위와 같은 순간이다
2025-09-30T15:00:00         아무것도 없음 - 순간이 안 정해진다

셋째 줄이 앞 글에서 본 팻말 없는 값이다. 규격상 틀린 표기는 아니고 “지역 시각”을 뜻하는 정당한 꼴이지만, 시스템 사이에서 주고받으면 받는 쪽이 자기 마음대로 해석한다.

주고받는 값에는 반드시 오프셋을 붙인다. 이건 API에서 시각을 실어 보내는 기본형이기도 하다.

오프셋이 있어도 지역은 모른다

여기서 비유가 깨진다. 오프셋을 붙였으니 팻말을 다 실은 것 같지만 실은 팻말의 숫자만 실었다.

+09:00을 보고 알 수 있는 건 그때 UTC와 아홉 시간 차이였다는 것뿐이다. 서울인지 도쿄인지는 모른다. 그리고 앞에서 본 대로 미래 시각에는 이걸로 부족하다. 규칙이 바뀌면 그때의 오프셋이 지금과 다를 수 있어서다.

그래서 지역이 필요한 값은 ISO 8601 문자열 하나로 담지 않는다. 지역 이름을 별도 필드로 같이 보낸다.

json
{
  "startsAt": "2026-03-20T09:00:00",
  "timeZone": "America/New_York"
}

규격은 생각보다 넓다

ISO 8601에는 실무에서 잘 안 쓰는 표기가 많이 들어 있다.

표기뜻
2025-273그해의 273번째 날
2025-W40-2그해 40주차 화요일
20250930T150000Z구분 기호를 뺀 축약형
P1Y2M10D1년 2개월 10일이라는 기간

마지막 줄은 성격이 다르다. 앞의 것들이 시점을 적는다면 기간 표기는 길이를 적는다. P로 시작하는 값을 보면 이쪽이다.

이렇게 넓다 보니 “ISO 8601을 지원한다”는 말이 라이브러리마다 다른 범위를 뜻한다. 그래서 인터넷에서 주고받을 때는 RFC 3339라는 더 좁은 규칙을 쓴다. 오프셋을 필수로 하고 애매한 표기를 걷어낸 것이라 서로 다르게 해석할 여지가 적다.

사람에게 보여 줄 꼴은 다른 문제다

ISO 8601은 기계가 주고받는 꼴이다. 화면에 그대로 띄우라는 규격이 아니다.

사람에게는 그 나라 습관대로 보여 주는 게 맞다. 2025년 9월 30일 오후 3시, Sep 30, 2025처럼. 이건 지역·언어 설정에 따라 달라지는 별개의 일이고, 대개 언어가 제공하는 형식화 기능이 맡는다.

두 일을 갈라 두는 것이 중요하다. 저장과 전송은 ISO 8601로, 표시는 사용자 설정에 맞춰서. 이 둘을 한 값으로 해결하려 들면 어느 쪽도 제대로 안 된다.

실무에서: 만들 때보다 읽을 때가 위험하다

내보내는 쪽은 라이브러리가 규격대로 만들어 주니 대개 안전하다. 문제는 받아서 읽을 때다.

  • 직접 파싱하지 않는다. 문자열을 잘라 쓰는 코드는 축약형이나 소수 초를 만나면 깨진다. 언어가 주는 파서를 쓴다
  • 소수 초 자릿수가 제각각이다. 15:00:00.1, .123, .123456이 다 온다. 정규식으로 자릿수를 못 박아 두면 그 자리에서 걸린다
  • 끝의 Z를 버리는 코드를 조심한다. 오프셋을 떼고 앞부분만 쓰면 팻말 없는 값이 되고, 그 순간 뜻이 사라진다
  • 초가 60인 값이 올 수 있다. 윤초를 표기할 자리가 규격에 있다. 대부분의 파서는 이걸 거부한다
  • 받은 값을 그대로 다시 내보내지 않는다. 안쪽 타입으로 파싱했다가 내보낼 때 다시 만든다. 그래야 꼴이 한 가지로 모인다

정리

  • 03/04/2025 같은 표기는 읽는 나라가 뜻을 정한다. ISO 8601은 그 다툼을 끝내려고 만든 꼴이다.
  • 규칙의 핵심은 큰 단위부터, 자릿수 고정이다. 나머지 성질이 여기서 따라 나온다.
  • 글자 정렬이 곧 시간 순서가 되지만, 오프셋을 하나로 맞춰 둔 경우에만 그렇다.
  • T는 날짜와 시각의 구분자, Z는 오프셋 0이다. 지역 이름이 아니다.
  • 오프셋이 있어도 지역은 모른다. 미래의 지역 시각은 지역 이름을 따로 실어야 한다.
  • 규격이 넓어서 인터넷에서는 RFC 3339라는 좁은 규칙을 쓴다.
  • 저장·전송용 꼴과 사람에게 보여 줄 꼴은 다른 일이다. 갈라 두어야 둘 다 된다.

다음 글은 큰 시계 자체를 의심한다. 시계가 뒤로 갈 수 있다면 지금까지의 전제가 하나 무너진다.