← 전체 글로 돌아가기

Next.js

React hydration 경고를 시간 표시 하나로 재현하고 고친 기록

서버와 브라우저의 시간이 달라서 발생한 React hydration 경고를 작은 재현 코드로 좁힌 과정과 수정 기준을 적었습니다.

처음에는 데이터 요청 문제라고 생각했다

개발 서버 콘솔에 Text content does not match server-rendered HTML 경고가 찍혔다. 화면은 보였기 때문에 배포를 미뤄도 되는 경고처럼 느껴졌지만, 새로고침 직후 버튼이 잠깐 다른 위치에 나타나는 것도 같이 보였다. 목록 데이터나 캐시를 의심했는데, API 응답을 고정해도 경고는 남았다.

문제를 줄이려고 컴포넌트 안에 있는 값을 하나씩 상수로 바꿨다. 마지막까지 남은 코드는 아래처럼 현재 시간을 바로 렌더링하는 부분이었다.

export function UpdatedAt() {
  return <time>{new Date().toLocaleString('ko-KR')}</time>
}

서버는 UTC 환경이었고 내 브라우저는 KST였다. 같은 순간을 읽어도 문자열이 달라질 수밖에 없었다.

재현 조건을 고정했다

테스트에서는 시간을 주입할 수 있게 바꾸고, 서버와 클라이언트가 처음 그리는 문자열을 동일하게 만들었다.

type Props = { iso: string }

export function UpdatedAt({ iso }: Props) {
  const [ready, setReady] = useState(false)

  useEffect(() => setReady(true), [])

  if (!ready) return <time dateTime={iso}>{iso.slice(0, 10)}</time>
  return <time dateTime={iso}>{new Date(iso).toLocaleString('ko-KR')}</time>
}

실제 앱에서는 서버가 iso를 내려주고, 첫 렌더는 날짜의 고정된 부분만 표시했다. 브라우저 전용 서식은 hydration이 끝난 다음에만 실행한다. 단순히 suppressHydrationWarning을 붙이는 방법도 있지만, 원인을 숨기는 쪽이라 이번 수정 기준에서는 제외했다.

확인한 항목

  • new Date()toLocaleString()이 JSX 평가 중 호출되지 않는가
  • 서버와 브라우저의 timezone, locale 차이를 테스트했는가
  • 시간 값을 props나 API 응답처럼 주입할 수 있는가
  • hydration 경고를 끄기 전에 첫 HTML과 첫 클라이언트 렌더를 비교했는가

수정 후의 기준

시간·무작위 값·브라우저 API는 서버 렌더 결과에 직접 넣지 않는다. 꼭 필요하다면 서버에서 결정한 값을 전달하고, 사용자 locale에 따른 표현은 hydration 뒤에 바꾼다. 이번 건은 경고 한 줄을 없앤 것보다 “서버가 처음 그릴 수 있는가?”를 먼저 묻는 습관을 남긴 일이었다.