KST 타임존 삽질기 — Vercel 서버 시간이 한국이 아니었다

문제 해결 노트··17분 읽기·
KST 타임존 에러를 해결하는 과정 — UTC 서버 시간과 한국 시간 차이 문제

토요일 밤 8시 45분, 회차가 안 바뀐다

토요일 저녁 8시 45분. 로또 추첨이 끝났는데 앱에는 여전히 이전 회차가 표시되고 있었습니다. 사용자가 보기엔 "앱이 고장 난" 거죠.

원인을 찾아봤더니 두 가지 문제가 겹쳐 있었습니다.

문제 1: 회차가 하드코딩되어 있었다

AI한테 "로또 회차 계산 만들어줘"라고 했더니, 그 시점의 회차 번호를 코드에 그대로 박아넣었습니다. 시간이 지나면 당연히 안 맞게 되는 구조였죠.

문제 2: Vercel 서버가 미국에 있었다

하드코딩을 고쳐도 문제가 남아있었습니다. 서버 시간이 UTC 기준이라, 한국 시간으로 토요일 밤 9시인데 서버에서는 아직 토요일 낮 12시. 새 회차 판단이 9시간 늦게 되는 겁니다.

Before/After — "회차 계산 만들어줘" vs 제약조건 명시

Before — 초급 프롬프트:
로또 회차 계산하는 기능 만들어줘.

이러면 AI가 현재 시점의 회차를 기준으로 하드코딩합니다.

After — 중급 프롬프트:
당신은 시간대(timezone) 처리 전문가입니다.

[현재 상태]
- Next.js 앱이 Vercel(미국 서버)에 배포되어 있습니다
- 로또 추첨은 매주 토요일 20:35 KST에 진행됩니다
- 서버 시간은 UTC인데 한국 사용자 대상 서비스입니다

[요청]
1. 하드코딩 없이, 2002-12-07(1회) 기준일부터 현재 KST까지 주 수를 계산하는 회차 로직
2. UTC → KST 변환이 모든 API에서 일관되게 적용되는 구조
3. 외부 시간 API(WorldTimeAPI) + 폴백 시스템
4. 토요일 20:35 이후에 새 회차로 넘어가는 판단 로직

"서버 시간은 UTC다"라는 제약조건 하나가 결과를 완전히 바꿨습니다.

3단계 폴백 — AI가 만든 첫 버전에서 서머타임 구멍 발견

AI가 만든 해결 구조는 /api/time/kst 엔드포인트에서 한국 시간을 반환하고, WorldTimeAPI가 실패하면 서버 UTC에 +9시간을 하는 폴백이었습니다.

근데 첫 결과에서 문제를 발견했습니다. 폴백이 단순히 Date.now() + 96060*1000이었거든요. Vercel 서버가 서머타임 적용 국가에 있으면 getHours()가 로컬 시간대 기준이라 이중 변환이 생길 수 있었습니다.

[피드백]
폴백 로직에서 단순히 +9시간을 하면,
Vercel 서버의 로컬 시간대에 따라 오차가 생깁니다.

실제로 getHours()를 쓰면 서버 시간대 기준값이 나오고,
getUTCHours()를 써야 UTC 기준값이 나옵니다.

[요청]
1. 폴백 계층을 3단계로 만들어주세요:
   - 1순위: WorldTimeAPI (Asia/Seoul) + 5분 캐싱
   - 2순위: Intl.DateTimeFormat('ko-KR', {timeZone: 'Asia/Seoul'})로 변환
   - 3순위: 최후 수단으로 getUTCHours() 기반 +9시간
2. getHours() 대신 getUTCHours()를 사용하도록 전부 교체
3. 각 폴백이 실패했을 때 로그를 남겨주세요

이 피드백이 핵심이었습니다. getHours()getUTCHours() 변경 하나가 이중 변환 문제를 잡았고, 2순위 폴백으로 Intl.DateTimeFormat을 넣으니 서머타임과 무관하게 정확한 KST가 나왔습니다.

실제 검증 결과: UTC 03:25 입력 → KST 12:25 출력. 정확합니다.

"토요일 20:34는 이번 회차, 20:35는 다음 회차"

회차 전환 시점을 정확히 잡는 게 가장 까다로웠습니다. 단순히 "토요일이면 다음 회차"가 아니라 분 단위로 판단해야 하거든요.

당신은 날짜/시간 로직 전문가입니다.

[상황]
- 로또 1회차 추첨일: 2002-12-07 (토요일)
- 추첨 시각: 매주 토요일 20:35 KST
- 토요일 20:34 → 현재 회차 예측 표시
- 토요일 20:35 → 다음 회차 예측으로 전환
- 일요일 06:00 → 새 회차 시작 (판매 재개)

[요청]
isAfterSaturdayDraw(date)와 getCurrentRound() 함수를 만들어주세요.
모든 시간 판단은 KST 기준이어야 합니다.

AI가 만든 로직의 핵심 부분:

토요일 판단:
- day === 6 (토요일) → hour > 20 또는 (hour === 20 && minute >= 35)
일요일:
- day === 0 → hour >= 6 (새벽 6시 이후)
월~금:
- 항상 현재 회차

이걸 lottery-constants.ts에 상수로 정의하고, LOTTERY_FIRST_DRAW_DATE = '2002-12-07T20:35:00+09:00'부터 현재 KST까지 주 수를 계산하는 방식으로 회차를 동적으로 구합니다. 하드코딩된 회차 번호가 코드에 한 줄도 없는 구조예요.

이미 엉뚱한 회차로 저장된 39건

타임존 문제를 고치는 과정에서 이미 발생한 피해도 있었습니다. 예측 데이터 39건이 1212회차 대신 1213회차로 잘못 저장되어 있었거든요. 토요일 추첨 직후 서버 시간 기준으로 "아직 이번 회차"인데 클라이언트에서는 "다음 회차"로 판단해서 회차가 역전된 겁니다.

[에러 상황]
prediction_archive 테이블에서 39건의 예측이
실제 회차(1212)가 아닌 다음 회차(1213)로 저장됨.
클라이언트와 서버의 회차 판단이 불일치한 것이 원인.

[요청]
1. 잘못된 39건의 round 값을 1212로 수정하는 SQL
2. 서버 사이드에서 회차를 검증하는 로직 추가
   (클라이언트가 보낸 회차가 서버 판단과 다르면 서버 값으로 보정)

수정 SQL로 39건을 바로잡고, 예측 저장 API에 서버 사이드 회차 검증을 추가했습니다. 이후로는 클라이언트가 잘못된 회차를 보내도 서버에서 보정하는 구조가 됐거든요.

거기에 더해서 SavedPredictions 테이블에도 90개 레코드의 타임스탬프가 KST로 잘못 저장되어 있었습니다. UTC로 저장해야 하는데 KST 시간이 그대로 들어간 거예요. 이건 배치 스크립트(50개씩 처리)로 일괄 수정했습니다.

12개 파일 수정 — "UTC 저장 + KST 표시" 통일

최종적으로 KST 유틸리티 라이브러리를 만들고, 12개 기존 파일에서 시간 관련 로직을 전부 이 라이브러리로 교체했습니다.

| 파일 | 변경 내용 |
|------|----------|
| kst-time.ts (신규) | convertToKST(), getKSTNow(), isAfterSaturdayDraw() 등 |
| api/time/kst/route.ts (신규) | WorldTimeAPI + 3단계 폴백 + 5분 캐싱 |
| lottery-constants.ts | 기준일 2002-12-07, 추첨 시각 상수화 |
| lottery-round-service.ts | DB 기반 회차 + 날짜 기반 폴백 + 1시간 캐싱 |
| LotteryRoundContext.tsx | 30분 주기 갱신 + 페이지 포커스 복귀 시 갱신 |
| countdown-timer.tsx | 하드코딩된 시간 제거 → 상수 사용 |
| predict/archive/save/route.ts | 서버 사이드 회차 검증 추가 |
| 나머지 5개 API/페이지 | getHours()getUTCHours() 교체, UTC 저장 통일 |

원칙은 하나입니다: DB에는 UTC로 저장하고, 화면에 보여줄 때만 KST로 변환한다. 이 규칙을 12개 파일에 일관되게 적용하니까, 이후로 타임존 관련 버그가 사라졌습니다.

한국 사용자 대상 서비스를 Vercel(미국 서버)에서 돌리면 반드시 만나는 문제입니다. "회차 계산 만들어줘"가 아니라 "서버 시간은 UTC고, 한국 시간으로 토요일 20:35에 회차가 바뀌어야 한다"라는 제약조건을 처음부터 AI한테 알려주는 게 핵심이었어요.

공유

댓글