트러블슈팅

서버와 브라우저의 날짜가 하루 어긋나 생긴 React hydration 오류

개개발 노하우 노트2026년 11월 10일 · 4분 읽기

자체 호스팅한 Next.js 블로그 관리 화면을 실제 서버에 올렸더니, 로컬에서는 한 번도 보지 못한 React hydration 오류가 났습니다. 원인은 날짜를 그리는 코드가 실행 환경의 기본 시간대를 따라간다는 점이었습니다. 이 글은 서버 렌더링 결과와 브라우저 렌더링 결과가 날짜 하나 때문에 어긋난 과정과, 표시 시간대를 명시해서 해결한 방법을 정리한 기록입니다.

상황

글 목록과 템플릿 미리보기에 작성일을 보여 주는 화면이었습니다. 미리보기용 글의 작성 시각은 코드에 고정해 둔 값이라, 언제 열어도 같은 날짜가 나와야 했습니다. 개발 PC에서 개발 서버로 띄웠을 때는 문제가 없었고, 컨테이너로 빌드해 사내 서버에 배포한 뒤 실제 브라우저로 접속했을 때 처음 오류가 보였습니다.

증상

  • 템플릿 목록 화면에서 React hydration 오류(418)가 브라우저 콘솔에 찍혔습니다.
  • 화면 자체는 대체로 그려졌기 때문에 눈으로만 보면 놓치기 쉬웠습니다.
  • 서버가 보낸 HTML과 브라우저가 그린 텍스트를 나란히 비교해 보니, 같은 글의 날짜가 서버 쪽은 9월 30일, 브라우저 쪽은 10월 1일로 하루씩 달랐습니다. 이런 썸네일이 다섯 개였습니다.

원인

저장된 시각은 하나인데, 그 시각을 "몇 월 며칠"로 바꾸는 순간 시간대가 끼어듭니다. `Intl.DateTimeFormat`이나 `toLocaleDateString`에 `timeZone` 옵션을 주지 않으면 실행 환경의 기본 시간대를 씁니다. 컨테이너는 UTC였고 브라우저는 한국 시간이었습니다. 자정 근처의 UTC 시각은 한국 시간으로 넘기면 날짜가 바뀌기 때문에, 서버가 렌더링한 문자열과 브라우저가 hydration 중에 다시 만든 문자열이 달라졌습니다.

React는 hydration할 때 클라이언트 첫 렌더링 결과가 서버 HTML과 같다고 가정합니다. 공식 문서도 서버와 클라이언트에서 서로 다른 데이터를 렌더링하는 것을 대표적인 불일치 원인으로 꼽고, 이 오류를 다른 버그처럼 고쳐야 한다고 적고 있습니다.

로컬에서 재현되지 않았던 이유도 같습니다. 개발 PC에서는 Node 프로세스와 브라우저가 모두 같은 시스템 시간대를 쓰니 두 결과가 우연히 일치했습니다.

해결

화면에 날짜를 표시하는 모든 곳에 시간대를 명시했습니다. 글 목록, 블록 미리보기, 글 관리, 상세 화면, 그리고 공용 날짜 포맷 함수가 대상이었습니다. 저장하는 값은 그대로 UTC 시각으로 두고, 사람에게 보여 줄 때만 서비스 기준 시간대로 바꾸는 방식입니다.

```ts

const dateFormatter = new Intl.DateTimeFormat("ko-KR", {

timeZone: "Asia/Seoul",

year: "numeric",

month: "long",

day: "numeric",

});

export function formatPostDate(value: string | Date) {

return dateFormatter.format(new Date(value));

}

```

포맷 객체를 공용 함수 하나로 모아 두면, 컴포넌트마다 `toLocaleDateString()`을 직접 부르다가 한 곳에서 옵션을 빠뜨리는 일을 줄일 수 있습니다.

`suppressHydrationWarning`으로 경고를 숨기는 방법은 쓰지 않았습니다. 공식 문서에서도 이 속성은 한 단계 깊이에만 적용되는 비상구이고, 불일치한 텍스트를 React가 고쳐 주지 않는다고 설명합니다. 즉 경고만 사라지고 서버가 보낸 틀린 날짜가 그대로 남습니다. 현재 시각처럼 정말로 서버와 클라이언트 값이 다를 수밖에 없는 경우가 아니라면, 렌더링 결과를 같게 만드는 쪽이 맞습니다.

다시 겪지 않으려면

  • 날짜·시간을 문자열로 바꾸는 코드에는 항상 `timeZone`을 명시합니다. 기본값에 기대지 않습니다.
  • 저장은 UTC, 표시는 서비스 기준 시간대로 나누고, 표시용 포맷 함수는 공용으로 하나만 둡니다.
  • 서버 컨테이너의 시간대를 바꿔서 맞추는 방법은 임시방편입니다. 접속하는 사용자의 시간대가 다르면 다시 깨집니다.
  • 배포 후에는 실제 배포 주소에서 서버 HTML과 브라우저 렌더링 텍스트를 비교하고, 콘솔의 hydration 오류를 확인합니다. 로컬 성공만으로 배포 성공을 판단하지 않습니다.
  • hydration 경고는 숨기지 말고 원인을 찾습니다. 날짜, 난수, 브라우저 전용 API가 렌더링 경로에 들어가 있는지부터 봅니다.

참고 자료