개인 프로젝트 MenuMate

MenuMate 검색 로직 리팩토링 이후 Recoverable Error 해결

dev_in 2026. 4. 15. 16:14

 

에러 상황 스크린샷

 

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. 상태 제어의 위치가 중요하다

상위에서 렌더 흐름을 제어해야 안정적입니다.