Unix 타임스탬프 초와 밀리초 구분하는 법
초, 밀리초, 마이크로초 타임스탬프를 자릿수로 구분하는 방법과 UTC, KST, ISO 8601 오프셋 변환, 2038년 문제, 로그 시각을 맞출 때 생기는 실수를 정리합니다.
같은 시각이 숫자로는 전혀 다르게 보입니다
API 응답에 "created_at": 1700000000이 들어 있고, 다른 서비스 로그에는 1700000000000이 찍혀 있다고 해 봅시다. 두 값은 같은 시각을 가리킵니다. 단위만 다릅니다. 초 단위 값은 1970년 1월 1일 00:00:00 UTC부터 흐른 초 수이고, 밀리초 단위 값은 같은 기준점부터 흐른 밀리초 수입니다.
new Date(1700000000 * 1000).toISOString() // '2023-11-14T22:13:20.000Z'
new Date(1700000000000).toISOString() // '2023-11-14T22:13:20.000Z'
new Date(1700000000000000 / 1000).toISOString() // '2023-11-14T22:13:20.000Z' (마이크로초)
new Date(1700000000000000000 / 1e6).toISOString()// '2023-11-14T22:13:20.000Z' (나노초)
마이크로초와 나노초도 같은 기준점을 쓰고 단위만 다릅니다. 실무에서는 초와 밀리초가 대부분이고, 마이크로초와 나노초는 로깅 라이브러리나 분산 추적 도구에서 종종 등장합니다.
자릿수로 단위를 구분합니다
값만 보고 단위를 알아내야 할 때는 자릿수가 가장 빠른 힌트입니다.
| 자릿수 | 단위 | 예시 | UTC 시각 |
|---|---|---|---|
| 10자리 | 초 | 1000000000 |
2001-09-09T01:46:40Z |
| 10자리 | 초 | 1700000000 |
2023-11-14T22:13:20Z |
| 10자리 | 초 | 9999999999 |
2286-11-20T17:46:39Z |
| 13자리 | 밀리초 | 1000000000000 |
2001-09-09T01:46:40Z |
| 13자리 | 밀리초 | 1794268800000 |
2026-11-10T00:00:00Z |
10자리 초 단위 값은 2001년부터 2286년까지 대부분의 현실적인 시각을 덮습니다. 13자리 밀리초 값도 같은 기간을 가리킵니다. 그래서 자릿수 규칙은 실무에서 충분히 유용하지만, 1970년 근처처럼 작은 값을 다룬다면 자릿수만으로 구분하기 어렵습니다. 값의 출처를 같이 확인하세요.
날짜·시간 변환기는 입력 문자열의 형식을 자동으로 감지합니다. 10자리 이하 숫자는 Unix 초로, 13자리 이하 숫자는 밀리초 타임스탬프로 인식하도록 되어 있습니다. 입력이 비어 있으면 현재 시각을 기준으로 여러 형식을 함께 보여 줍니다. 이 도구는 감지 결과를 기준으로 여러 형식의 문자열을 한꺼번에 계산하므로, 어떤 형식으로 읽혔는지 목록에서 확인하면 됩니다.
감지 순서에도 주의할 점이 있습니다. 감지 규칙은 위에서 아래로 확인하며, 먼저 맞는 형식을 씁니다. 20261110처럼 8자리 숫자는 ISO 8601 기본 형식(YYYYMMDD)으로 먼저 잡힙니다. 초 단위 타임스탬프가 아니라 날짜 값으로 읽힙니다. 숫자만 있는 입력을 넣을 때는 결과 형식 이름을 꼭 확인하세요.
언어별 기본 단위
같은 숫자를 넘겨도 언어와 라이브러리마다 기본 단위가 다릅니다. 이 차이가 가장 많이 사고를 냅니다.
- JavaScript:
Date.now()와new Date(숫자)는 밀리초를 씁니다. - Java:
System.currentTimeMillis()는 밀리초,Instant.ofEpochSecond()는 초를 받습니다. - Python:
time.time()은 초를 소수점 형태의 실수로 돌려주고,datetime.fromtimestamp()도 초 단위입니다.
JavaScript에서 초 단위 값을 그대로 넣으면 다음과 같이 1970년 근처 날짜가 나옵니다.
new Date(1700000000).toISOString() // '1970-01-20T16:13:20.000Z'
반대로 밀리초 값을 초 단위 함수에 넣으면 연도가 5만 년을 넘어갑니다. 이 두 가지 증상은 로그에서 금방 눈에 띄므로, 날짜가 1970년이거나 수만 년 뒤로 나오면 단위를 먼저 의심하세요.
UTC와 KST 변환
타임스탬프 자체는 시간대가 없습니다. 특정 순간을 나타내는 숫자입니다. 화면에 보여 줄 때만 시간대를 입힙니다. 로그를 읽을 때는 UTC로 저장된 값을 KST(UTC+9)로 바꿔 읽는 경우가 많습니다.
new Date(1700000000000).toLocaleString('sv-SE', { timeZone: 'Asia/Seoul' })
// '2023-11-15 07:13:20'
같은 순간이 UTC로는 2023년 11월 14일 22시 13분이고, KST로는 11월 15일 07시 13분입니다. 날짜가 바뀌는 시각이 달라지기 때문에, 하루 단위 집계를 할 때는 어느 시간대의 "하루"인지 정하고 그 기준으로 잘라야 합니다.
시간대 이름 Asia/Seoul은 IANA 시간대 이름입니다. UTC+9 같은 고정 오프셋 대신 이 이름을 쓰면, 한국은 서머타임이 없으므로 결과가 같지만 다른 나라 데이터를 섞어도 안전합니다.
ISO 8601 표기와 오프셋
타임스탬프 숫자 대신 문자열로 주고받는 경우에는 ISO 8601 형식이 많이 쓰입니다. 이때 시간대 표기를 빠뜨리면 같은 문자열이 서버마다 다르게 해석됩니다.
Date.parse('2026-11-10T09:00:00+09:00') // 1794268800000 (= 2026-11-10T00:00:00Z)
Date.parse('2026-11-10T09:00:00Z') // 1794301200000 (UTC 09시)
뒤에 Z나 +09:00 같은 오프셋이 있으면 시각이 하나로 정해집니다. 오프셋이 없으면 해석은 실행 환경의 로컬 시간대에 달립니다.
TZ=UTC node -e "console.log(Date.parse('2026-11-10T09:00:00'))"
# 1794301200000
TZ=Asia/Seoul node -e "console.log(Date.parse('2026-11-10T09:00:00'))"
# 1794268800000
같은 문자열인데 실행 환경의 시간대에 따라 9시간 차이가 납니다. 공백으로 구분한 2026-11-10 09:00:00 형식도 오프셋이 없으면 같은 방식으로 로컬 시간을 따릅니다. 로그나 API에는 항상 오프셋이나 Z를 함께 기록하는 것을 기본으로 삼으세요.
DB에서도 같은 문제가 생깁니다. 시간대 정보가 없는 timestamp 타입에 KST 값을 넣고 다른 서버에서 UTC로 읽으면 9시간이 어긋납니다. 저장 기준을 UTC로 통일하고, 화면에서만 KST로 바꾸는 것이 가장 흔한 방법입니다.
2038년 문제
초 단위 타임스탬프를 부호 있는 32비트 정수에 담으면 저장 가능한 최댓값은 2147483647입니다. 이 값은 UTC 기준 2038년 1월 19일 03시 14분 07초입니다. 바로 다음 초에 값이 넘쳐서 음수로 바뀌고, 날짜는 1901년 12월 13일 20시 45분 52초로 돌아갑니다.
new Date(2147483647 * 1000).toISOString() // '2038-01-19T03:14:07.000Z'
new Date(2147483647 * 1000).toLocaleString('sv-SE', { timeZone: 'Asia/Seoul' })
// '2038-01-19 12:14:07'
new Date(-2147483648 * 1000).toISOString() // '1901-12-13T20:45:52.000Z'
현재 대부분의 서버와 언어 런타임은 64비트 정수를 쓰므로 당장 문제가 되는 일은 드뭅니다. 그래도 오래된 임베디드 장치, 32비트 바이너리, 고정 길이 컬럼으로 초를 저장한 레거시 DB에서는 여전히 위험합니다. 새로 설계한다면 밀리초를 64비트 정수(MySQL의 BIGINT, PostgreSQL의 bigint)로 저장하거나, 시각을 ISO 8601 문자열이나 timestamptz로 저장하는 쪽을 택합니다.
로그 시각을 맞출 때 생기는 실수
사고 원인 분석에서 자주 나오는 실수들입니다.
- 서버 로그는 UTC인데 애플리케이션 로그는 KST로 찍혀 있어서, 두 로그를 맞춰 보면 9시간 차이가 납니다.
- 로그 수집기가 시간대 정보를 버리고 시각만 저장해서, 날짜 경계 근처의 이벤트가 다른 날로 잡힙니다.
- 밀리초 값을 초 단위로 잘못 읽어 날짜가 수만 년 뒤로 나옵니다.
- ObjectId 같은 식별자에 포함된 시각을 혼동합니다. 24자리 16진수 MongoDB ObjectId의 앞 8자리는 생성 시각을 초 단위로 담고 있습니다.
65a1b2c3은1705095875초이고 UTC로는 2024년 1월 12일 21시 44분 35초입니다. 같은 값을 KST로 읽으면 2024년 1월 13일 06시 44분 35초입니다. - 장애 시각을 사람이 말로 옮기면서 시간대를 빼먹습니다. 슬랙이나 티켓에는 "KST 14:05 (UTC 05:05)"처럼 두 가지를 같이 적으세요.
흔한 실수
- 타임스탬프 단위를 확인하지 않고 바로 날짜로 바꿉니다.
- 오프셋이 없는 날짜 문자열을 서버 시간대에 맡깁니다.
- 로그 시각과 DB 시각을 같은 시간대라고 가정합니다.
- 집계 기준 날짜를 UTC로 잘라서 한국 시간 새벽 데이터가 전날로 들어갑니다.
- 32비트 정수에 초 단위 값을 저장합니다.
점검 목록
- 타임스탬프의 자릿수를 세어 초인지 밀리초인지 먼저 확인합니다.
- 변환 결과가 1970년이나 수만 년 뒤로 나오면 단위를 다시 확인합니다.
- 문자열 시각에
Z또는 오프셋이 붙어 있는지 확인합니다. - 로그와 DB의 시간대를 각각 확인하고, 비교할 때는 UTC로 맞춥니다.
- 날짜별 집계는 어느 시간대의 날짜인지 명시한 뒤 자릅니다.
- 2038년 이후에도 쓰일 필드가 64비트 정수 또는 날짜 타입인지 확인합니다.
- 장애 보고에는 시간대를 함께 적습니다.