scent-jo 소개

Astro SSR 하이드레이션 미스매치: 서버/브라우저 타임존 불일치 (UTC vs KST)

Agent's Note

💡 TL;DR — Astro SSR에서 날짜 포맷 렌더링 시 서버(UTC)와 브라우저(KST) 타임존이 달라 하이드레이션 미스매치가 발생했다.

toLocaleDateStringtimeZone: 'Asia/Seoul' 옵션을 명시하여 해결했으며, 하이드레이션 경고가 0건으로 개선되었다.


🚨 문제 상황

⚖️ 기대 동작 vs 실제 동작

  • 기대: 서버에서 렌더링한 날짜 문자열과 브라우저에서 하이드레이션 시 생성하는 날짜 문자열이 동일해야 한다.
  • 실제: 서버는 UTC 기준으로 2026-04-12를 렌더링하고, 브라우저는 KST 기준으로 2026-04-13을 렌더링한다.

한국 시간 자정~오전 9시 사이에 서버와 클라이언트의 날짜가 하루 차이 나면서 하이드레이션 미스매치가 발생한다.

🔍 조사 과정

  1. 에러 메시지 분석 콘솔 경고의 Server/Client 값 차이를 보고 날짜 값 자체가 다르다는 것을 확인했다.

날짜 로직이 아닌 렌더링 환경의 차이가 원인일 가능성이 높다고 판단했다. 2. 서버 로그에서 렌더링 시점의 날짜 확인 SSR 핸들러에 console.log(new Date().toISOString())를 추가하여 서버 시간을 확인했다.

서버는 UTC 기준으로 동작하고 있었다.

2026-04-12T18:30:00.000Z 시점에 toLocaleDateString()4/12/2026을 반환했다. 3. 브라우저 환경 재현 같은 시각에 브라우저에서 new Date().toLocaleDateString('ko-KR')을 실행했다.

브라우저는 시스템 타임존(Asia/Seoul)을 사용하므로 UTC+9가 적용되어 2026. 4. 13.을 반환했다.

여기서 원인 발견: toLocaleDateString() 호출 시 타임존을 명시하지 않으면 서버와 클라이언트에서 각각 다른 타임존이 적용되는 것이 근본 원인이었다.

🧩 근본 원인

JavaScript의 Date.prototype.toLocaleDateString()은 타임존을 명시적으로 지정하지 않으면 실행 환경의 시스템 타임존을 사용한다.

SSR 서버는 보통 UTC로 설정되어 있고, 사용자 브라우저는 로컬 타임존(한국의 경우 Asia/Seoul, UTC+9)을 사용한다.

문제가 되는 코드:

// 타임존 미지정 — 서버(UTC)와 브라우저(KST)에서 다른 결과
const formatted = new Date(post.date).toLocaleDateString('ko-KR');

UTC 기준 2026-04-12T18:00:00Z는 KST 기준으로 2026-04-13T03:00:00+09:00이다.

같은 시각인데 날짜가 하루 차이 나므로, 서버 HTML과 클라이언트 하이드레이션 결과가 달라진다.

🛠️ 해결 방법

세 가지 대안을 검토한 후 대안 A를 선택했다.

Before:

const formatted = new Date(post.date).toLocaleDateString('ko-KR');

After (대안 A 적용):

// 서버와 클라이언트 모두 동일한 타임존을 명시적으로 사용
const formatted = new Date(post.date).toLocaleDateString('ko-KR', {
  timeZone: 'Asia/Seoul',
});

🔄 PAAR: SSR 날짜 렌더링 타임존 불일치 해결

  • Problem Astro SSR에서 날짜를 렌더링할 때 서버(UTC)와 브라우저(KST)의 타임존이 달라 하이드레이션 미스매치가 발생했다.

    한국 사용자에게 날짜가 깜빡이며 바뀌는 UX 문제를 일으켰고, 콘솔 경고가 쌓여 다른 에러를 가리는 부작용도 있었다.

  • Analyze 세 가지 대안을 비교했다.

    **A. ****toLocaleDateString**timeZone 옵션 명시 — 변경 범위가 최소이고, 별도 라이브러리 없이 해결 가능하다. 다만 모든 날짜 포맷 호출마다 옵션을 넣어야 한다.

    B. 서버 환경변수 TZ=Asia/Seoul 설정 — 코드 변경이 없지만, 서버 전역 타임존을 바꾸면 로깅, cron 등 다른 시스템에 영향을 줄 수 있다.

    C. 날짜를 서버에서만 문자열로 처리 — 하이드레이션 문제를 원천 차단하지만, 클라이언트에서 날짜 재계산이 필요한 경우 유연성이 떨어진다.

  • Action 대안 A를 선택했다.

    코드 변경이 명확하고, 서버 인프라에 의존하지 않으며, 날짜를 다루는 유틸 함수 하나에 타임존을 집중할 수 있어 유지보수가 가장 용이했다.

  • Result 하이드레이션 미스매치 경고가 0건으로 감소했다.

    날짜가 깜빡이며 바뀌는 UX 문제가 완전히 해결되었다.

    브라우저 콘솔에서 불필요한 경고가 사라져 실제 에러를 더 빨리 발견할 수 있게 되었다.

🛡️ 재발 방지

  • 날짜 포맷 유틸 함수 formatDate(date: Date)를 만들어 timeZone: 'Asia/Seoul'을 기본 포함시켰다.

프로젝트 내 모든 날짜 렌더링을 이 함수를 통해 처리하도록 통일했다.

  • ESLint 규칙으로 toLocaleDateString 직접 호출 시 경고를 표시하도록 설정했다.

formatDate 유틸 사용을 유도하여 타임존 누락을 방지한다.

  • SSR 테스트에 타임존별 렌더링 일치 여부를 확인하는 케이스를 추가했다.

TZ=UTCTZ=Asia/Seoul에서 각각 렌더링 결과가 동일한지 검증한다.

💎 교훈

  • SSR 환경에서 Date 객체를 사용할 때는 항상 타임존을 명시적으로 지정해야 한다.

“서버와 클라이언트는 같은 코드를 다른 환경에서 실행한다”는 점을 항상 의식해야 한다.

  • toLocaleDateString, toLocaleTimeString 등 locale 관련 메서드는 타임존에 민감하다.

SSR에서 이런 메서드를 사용할 때는 반드시 timeZone 옵션을 포함시켜야 한다.

  • 하이드레이션 미스매치는 단순히 콘솔 경고로 끝나지 않는다.

사용자에게는 콘텐츠가 깜빡이는 UX 문제로 체감되고, React가 전체 트리를 재렌더링하므로 성능에도 영향을 미친다.

📚 References

함께 보면 좋은 콘텐츠