
1. 문제 상황
검색 기능 리팩토링 이후, 개발 환경에서 다음과 같은 에러가 발생했습니다.
Hydration failed because the server rendered HTML didn't match the client
Next.js에서는 이를 Recoverable Error로 표시하며, 클라이언트에서 다시 렌더링을 시도하고 있었습니다.
2. 에러의 의미
구글링 및 AI와 상의 결과, 이 에러는 단순한 렌더링 문제가 아니었습니다.
서버가 만든 HTML과 클라이언트가 처음 렌더링한 결과가 다르다는 의미였습니다.
3. 실제 발생 상황
로그를 보면 다음과 같은 차이가 있었습니다.
+ <div>
- <p>
즉 서버는 <p>를 렌더하고 클라이언트는 <div>를 렌더하는 상황입니다. 동일한 컴포넌트인데 결과가 달라진 것입니다.
4. 원인 분석
핵심 원인: 이전 리팩토링 과정에서 Persist 캐시 도입
바로 이전의 리팩토링 과정에서 TanStack Query + localStorage persist 구조를 도입했습니다.
이 구조의 문제는 서버 렌더 시점과 클라이언트 렌더 시점이 다르다는 점이었습니다.
서버 렌더 시점
localStorage 접근 불가
- 캐시 없음
- results = []
→ "검색 결과 없음" 또는 "검색 중..." 렌더
클라이언트 렌더 시점
- localStorage에서 캐시 복원
- results 존재
→ 결과 리스트 렌더
결과
서버: <p>
클라이언트: <div>
서버와 클라이언트가 바라보는 렌더 시점이 다르기 때문에 수화 과정에서 hydration mismatch 발생한 것입니다.
5. 문제의 본질과 해결 전략
이번 에러는 단순히 조건문 문제가 아니었습니다. 서버와 클라이언트의 초기 데이터 상태가 다르다는게 문제였습니다.
해결 전략은 캐시 복원이 끌날 때까지 렌더를 통일하는 것입니다.
6. 해결 방법
TanStack Query에서 제공하는 useIsRestoring 훅을 사용했습니다.
적용 코드
const isRestoring = useIsRestoring();
if (isRestoring) {
return <p>검색 중...</p>;
}
추가로 query 실행도 제어
useQuery({
...
enabled: !!q && !isRestoring,
});
8. 해결 이후 흐름
이전의 흐름
서버: 빈 결과 → <p>
클라이언트: 캐시 있음 → <div>
→ hydration mismatch ❌
useIsRestoring 훅 이용 후 흐름
서버: <p>
클라이언트 (복원 중): <p>
클라이언트 (복원 완료): <div>
→ 정상 동작 ⭕
9. 배운점
1. SSR과 CSR은 동일하지 않다
특히 브라우저 API(localStorage)는 서버에서 접근 불가합니다.
2, hydration은 '초기 상태 동기화' 과정이다
React는 기존 HTML + JS 상태를 맞추는 작업을 합니다. 여기에서 mismatch가 나면 에러가 발생합니다.
3. 캐시 도입은 SSR과 충돌할 수 있다
캐시는 클라이언트에만 존재할 수 도 있습니다. 초기 렌더 차이는 반드시 고려해야 합니다.
클라이언트 캐시를 도입할 때는 서버와 초기 렌더 상태를 반드시 동일하게 맞춰야 한다
4. 상태 제어의 위치가 중요하다
상위에서 렌더 흐름을 제어해야 안정적입니다.
'개인 프로젝트 MenuMate' 카테고리의 다른 글
| MenuMate 레시피 조회 로직 리팩토링 및 문제 해결 과정 정리 (0) | 2026.04.21 |
|---|---|
| MenuMate 배포 전략 고민 사항 정리 (0) | 2026.04.19 |
| MenuMate 검색 로직 리팩토링 작업 (상태 관리 -> 캐시 관리) (0) | 2026.04.15 |
| TypeScript가 css import를 타입으로 인식하지 못하는 문제 해결 과정 (0) | 2026.04.14 |
| Next.js 검색 API 500 에러 디버깅 및 문제 해결 과정 정리 (1) | 2026.04.09 |