대니
danny's blog@dannywon_dev
발행 20 · 대기 85

1인 개발자. 클로드 코드를 팀처럼 굴린다. 매일 겪은 것만 쓴다.

버그와 함정

getMonth() 가 서버와 브라우저에서 다른 날짜를 뱉는다

한 페이지 안에서 헤더는 8.4, 모달은 8.5였다. 타임존 버그를 규칙 대신 타입으로 막기로 한 이유

운영 화면과 로컬에서 같은 날짜가 하루 다르게 뜬 적이 있다면 이 글이 맞다.

2026-08-04, 운영 중인 서비스에서 같은 전시 기간이 두 곳에 다르게 떠 있었다.

  • 운영 화면: 2026.8.4 - 9.13
  • 내 노트북(로컬): 2026.8.5 - 9.13

더 이상한 건 운영 한 페이지 안에서도 값이 갈렸다는 것이다. 페이지 헤더는 8.4, 그 위에 띄운 모달은 8.5. 같은 데이터, 같은 함수였다.

원인은 서버가 UTC 로 돌고 브라우저는 한국시간이라는 것. DB 에는 한국시간 8월 5일 00시가 2026-08-04 15:00:00+00 으로 정확하게 들어 있었다. 문제는 그걸 글자로 바꾸는 함수가 getMonth() · getDate() 를 쓰고 있었다는 점이다. 이 함수들은 코드가 도는 컴퓨터 시간대를 따른다. 헤더는 서버에서 만들어서 8월 4일, 모달은 브라우저에서 만들어서 8월 5일이 됐다.

저장은 옳았고 읽는 쪽이 틀렸다

세어봤다.

  • 시각을 담는 DB 컬럼 54개는 전부 시간대를 함께 저장하는 타입이었다. 저장은 옳았다
  • 그런데 코드 전체에서 「한국시간」을 못 박은 곳은 딱 두 군데였다 — 채팅 목록의 「오늘/어제」 표시, 통계의 시간대별 집계
  • 둘 다 예전에 같은 문제로 한 번 데인 자리였다. 주석에 "서버 시간대가 오늘/어제 경계를 흔들지 않도록 고정" 이라고 이유까지 적혀 있었다

입구도 새고 있었다. 날짜를 저장하는 경로가 3곳인데 규칙이 서로 달랐다. 한 곳은 UTC 자정, 한 곳은 한국시간 자정, 한 곳은 DB 세션 기준. 데이터가 이미 두 종류로 섞여 있었다.

「진행 중인가 끝났는가」를 판정하는 코드는 6벌이었고 종료일 처리가 서로 달랐다. 그래서 한 화면에선 끝난 전시가 다른 화면에선 진행 중인 날이 하루 생긴다.

제일 무서웠던 건 다른 것이었다. 9월에 열리는 큰 행사가 9/7 오전 9시 종료로 저장돼 있었다. 행사 마지막 날 오전 9시부터 참가자가 자기 자료를 못 고치게 된다.

처음 떠오른 처방을 버렸다

처음 생각한 건 「전부 한국시간으로 통일한다」 였다. 함수마다 시간대를 명시하고, 앞으로 코드를 추가할 때도 그렇게 하기로 규칙을 세우는 것.

그런데 위의 「두 군데」가 그 방식이 이미 두 번 실패한 증거였다. 규칙이 없어서 틀린 게 아니다. 아플 때마다 그 자리만 고치고 옆칸은 그대로 뒀기 때문에 틀린 것이다. 주석에 이유까지 적어두고도 그랬다. 같은 처방을 세 번째로 쓰면 세 번째로 샌다.

갈린 지점은 여기였다 — 규칙을 사람이 기억해서 지킬 것인가, 구조가 대신 지키게 할 것인가.

답은 이미 같은 프로젝트 안에 있었다. 행사 일정을 담는 다른 테이블은 시간 개념이 아예 없는 날짜 타입을 쓰고 있었고, 거기서만 이 버그가 한 번도 안 났다. 8월 5일이 어디서 읽든 8월 5일이었기 때문이다.

동시에 값이 두 종류라는 것도 분명해졌다.

  • 달력에 동그라미 치는 것 — 전시 기간, 행사일, 마감일. 뉴욕에서 봐도 8월 5일이어야 한다. 보는 사람 시간대로 바꾸면 틀린 날짜가 된다
  • 그때 일어난 일 — 문의가 온 시각, 메시지 시각. 이건 반대로 보는 사람 시간대로 바꿔야 맞다

하나의 도구(formatDate)로 둘 다 처리하고 있었으니, 호출하는 쪽이 매번 틀릴 수밖에 없었다.

바꾼 것

  • 행사 날짜를 「시간 없는 날짜」 타입으로 옮기기로 했다. 규칙을 지키는 주체를 사람에서 타입으로 옮기는 것이다. 시간대 개념이 없는 타입에는 애초에 어긋날 여지가 없다
  • 표시 함수를 역할로 갈랐다 — 달력 날짜용과 시각 기록용. 이름이 갈리면 호출부가 고민하지 않는다
  • 판정 6벌을 하나로 합친다
  • 전역 규칙 파일을 하나 새로 만들었다. 배포는 UTC, 개발은 한국시간이라 프로젝트를 가리지 않고 똑같이 걸리는 문제였기 때문이다

그리고 제일 중요한 게 남았다. 테스트를 고치는 게 재발 방지의 핵심이었다.

기존 테스트는 로컬 시간대로 값을 만들어 기대값과 비교하고 있었다. 그래서 UTC 에서 돌려도 통과했다. 버그를 잡으라고 만든 테스트가 버그를 못 보고 있었던 것이다. 테스트와 코드가 같은 잘못된 가정을 공유하면 통과는 아무 의미도 없다.

절대 시각을 넣고, 목표 시간대 기준으로 기대값을 적도록 바꾼다.

← 목록으로