모든 개발자가 출시해 본 7가지 Unix 타임스탬프 버그
프로덕션 사고를 일으키는 타임스탬프 실수들: 조용한 단위 불일치, 시간대 가정, 잊어버린 ×1000, 문자열 저장, 모호한 파싱, 일광 절약 경계 오류. 각 버그에 증상, 근본 원인, 실용적 해결책을 함께 담았습니다.
버그 1: JavaScript에서 × 1000 누락
new Date(1700000000)은 2023년 11월이 아니라 1970년 1월 근처의 날짜를 만듭니다. JavaScript의 Date 생성자는 밀리초를 기대하지만, 대부분의 서버 API와 데이터베이스는 Unix 초를 반환합니다. 이 버그는 백엔드 페이로드가 유효한 JSON이고 UI가 단지 잘못된 연도를 렌더링하기 때문에 보통 관리 대시보드에서 먼저 나타납니다.
- Wrong: new Date(response.created_at) — if created_at is Unix seconds
- Right: new Date(response.created_at * 1000)
- Detection: if your date shows a year near 1970, the timestamp is in seconds not milliseconds
- Safe wrapper: const toDate = ts => new Date(ts < 1e11 ? ts * 1000 : ts)
버그 2: 시스템 경계에서 단위 혼용
JavaScript는 밀리초를 반환합니다. Python, Go, 대부분의 서버 측 API는 초를 반환합니다. JavaScript가 변환 없이 Python 백엔드로 타임스탬프를 보내면 백엔드는 밀리초 값을 초로 해석해 이벤트를 55792년에 둡니다. 반대 방향도 똑같이 나쁩니다: UI가 초를 받아 그대로 new Date()에 넘기고 1970년 1월을 표시합니다.
- JavaScript sends: Date.now() → 1700000000000 (ms)
- Python receives: datetime.fromtimestamp(1700000000000) → year 55792
- Fix: divide by 1000 before sending to a non-JavaScript system
- Better fix: use ISO 8601 strings at API boundaries — they are unambiguous
버그 3: 서버 로컬 시간대에 의존
Python의 datetime.fromtimestamp()를 UTC 지정 없이 쓰는 코드는 서버에 설정된 시간대에 따라 다른 결과를 냅니다. 개발에서는 동작하고 다른 리전에 배포하면 프로덕션에서 깨집니다. 같은 부류의 버그는 코드가 명시적 timeZone 옵션 없이 날짜를 포맷하고 그 결과를 테스트에서 스냅샷할 때 Node에서도 나타납니다.
- Wrong: datetime.fromtimestamp(ts) — uses server's local timezone
- Right: datetime.fromtimestamp(ts, tz=datetime.timezone.utc)
- Wrong in JS: date.toLocaleDateString() — uses the Node process timezone
- Right in JS: date.toISOString() or Intl.DateTimeFormat with an explicit timeZone option
버그 4: 타임스탬프를 문자열로 저장
타임스탬프를 VARCHAR 열에 포맷된 문자열로 저장하는 것은 시간으로 정렬하거나 범위를 조회하거나 산술을 해야 하기 전까지는 무해해 보입니다. 날짜의 문자열 비교는 엄격하게 0으로 채운 ISO 8601 필드에서만 동작합니다. Nov 15, 2023이나 1/2/24 같은 사람이 읽기 쉬운 문자열은 표시 형식이지 저장 형식이 아닙니다.
- Wrong: INSERT INTO events (created) VALUES ('Nov 15, 2023') — not sortable
- Wrong: INSERT INTO events (created) VALUES ('2023-11-15') — loses time component
- Right: use a native DATETIME/TIMESTAMPTZ column or BIGINT for Unix milliseconds
- If a string is unavoidable: use ISO 8601 with full precision: '2023-11-15T06:13:20Z'
버그 5: 모호한 날짜 문자열 파싱
new Date('2024-01-01')과 new Date('2024/01/01')은 같아 보이지만 다르게 동작합니다. 하이픈 ISO 8601 형식은 UTC 자정으로 파싱되고, 슬래시 형식은 구현에 따라 다르며 대부분의 브라우저는 로컬 자정으로 파싱합니다. 날짜 선택기, API 페이로드, 데이터베이스 행이 같은 달력 날짜를 보여주면서 서로 다른 순간을 나타낼 수 있습니다.
- new Date('2024-01-01') → January 1, 2024 00:00:00 UTC (correct, explicit)
- new Date('2024/01/01') → January 1, 2024 00:00:00 local time (varies by runtime)
- new Date('January 1, 2024') → local time, may be rejected by strict parsers
- Safe rule: always use ISO 8601 with explicit timezone: new Date('2024-01-01T00:00:00Z')
버그 6: 내일을 얻으려 24시간 더하기
86,400,000밀리초를 더하는 것은 내일을 얻는 깔끔한 방법처럼 보이지만, 로컬 달력 날짜가 항상 24시간인 것은 아닙니다. 일광 절약 시간 전환 동안 로컬 하루는 23시간이나 25시간일 수 있습니다. 제품 로직이 다음 로컬 달력 날짜를 의미한다면, UTC 밀리초의 기간 산술이 아니라 대상 시간대의 달력 산술을 쓰세요.
- 달력 날짜에는 잘못됨: next = new Date(date.getTime() + 86400000)
- 로컬 Date 코드에서 더 나음: Date를 복사한 뒤 setDate(d.getDate() + 1) 호출
- 서버 코드용: 시간대 인식 라이브러리나 데이터베이스 날짜 산술 사용
- 반복 일정용: local_date, local_time, timezone_id를 저장하고 각 발생을 계산
- 대상 시간대의 일광 절약 시작일과 종료일에 대한 테스트 추가
버그 7: 끝을 포함하는 하루 필터
흔한 분석 쿼리는 created_at >= start와 created_at <= endOfDay를 씁니다. 합리적으로 보이지만, 데이터베이스가 밀리초·마이크로초·나노초를 저장할 때 정밀도 버그를 만듭니다. 더 안전한 패턴은 반열린 범위입니다: 시작 이상이고 다음 기간의 시작보다 엄격히 작음.
- Risky: WHERE created_at <= '2026-05-19 23:59:59'
- Safer: WHERE created_at >= '2026-05-19' AND created_at < '2026-05-20'
- Half-open ranges work for seconds, milliseconds, microseconds, and nanoseconds
- The same pattern works for months: created_at >= monthStart AND created_at < nextMonthStart
프로덕션 예방 체크리스트
대부분의 타임스탬프 사고는 명명, 테스트, 명시적 경계로 예방할 수 있습니다. 목표는 모든 날짜 코드 줄을 영리하게 만드는 것이 아니라 단위·시간대·범위 의미를 지루할 만큼 명백하게 만드는 것입니다.
- 숫자 필드에 단위를 넣어 명명: createdAtMs, expiresAtSeconds, imported_at_ms
- 저장에는 UTC를, 표시에는 명시적 IANA 시간대를 사용
- 사람이 검사하는 API 경계에서는 Z나 오프셋이 있는 ISO 8601 문자열을 선호
- 날짜 필터에는 반열린 범위를 사용
- 1월 1일, 윤일, 일광 절약 전환, 2038년 경계 주변을 테스트
Unix 타임스탬프 버그 FAQ
- 가장 흔한 Unix 타임스탬프 버그는?
- 밀리초가 기대되는 곳에 초를 넘기거나 그 반대입니다. JavaScript에서 new Date(unixSeconds)는 생성자가 밀리초를 기대하므로 1970년에 떨어집니다; 먼저 10자리 값에 1000을 곱하세요.
- 초 대 밀리초 버그를 어떻게 예방하나요?
- 필드에 단위를 넣어 명명하고(createdAtMs, expiresAtSeconds), 시스템 경계에서 변환하며, 값이 자기 기술적이 되도록 API 계약에서 ISO 8601 문자열을 선호하세요.
- 왜 86,400,000ms를 더하면 '내일'이 깨지나요?
- 로컬 달력 날짜는 일광 절약 전환 동안 23시간이나 25시간이므로 고정 24시간을 더하면 잘못된 날짜에 떨어질 수 있습니다. 대상 시간대의 달력 산술을 쓰세요.