Next.js는 서버 사이드 렌더링(SSR)을 하며, React는 서버와 클라이언트 간에 렌더링된 콘텐츠가 동일하다고 예상한다.
이로 인해 클라이언트 측에서는 초기 HTML과 React 컴포넌트가 서버 측에서 생성한 것과 일치하도록 자동으로 조정된다.
이를 Hydration이라고 칭하고, 이를 위반하여 나는 에러를 발생시켰다.
하단의 예시를 매우 간단히 요약하자면,
로그인 시 쿠키에 저장하는 userId값을 이용하여 유효성을 판별하고
이에 따라 조건부 렌더링을 구현하고자 삼항연산자를 이용해 아래와 같은 코드를 구성했었다.
export default function RenderErrorModal() {
// 쿠키에 저장된 userId를 꺼내오고 변수에 저장
const cookie = new Cookies();
const userId = cookie.get('userId');
const [isRender, setIsRender] = useState(true);
useEffect(() => {
if (userId) setIsRender(false);
}, [userId]);
return (
<div>
/* isRender가 truthy하다면 비로그인 상태이므로 에러 모달을 띄운다 */
{isRender ? <ErrorModal /> : ''}
</div>
)
}
하지만 아래와 같은 Hydration Error를 발생시켰다.

서버와 클라이언트 렌더링의 Hydration Error라면, 작성한 삼항연산자가 에러를 일으키는 원인이라고 생각이 들었다.
즉시 나는 Next 공식 문서를 통해 문제를 파악했다.

이는 서버에서 pre-render한 React 트리와, 클라이언트에서 렌더링된 React 트리 사이에 차이가 발생했기 때문에 발생한 오류이다.
즉 에러 메시지를 통해 다시 해석해보자면, 서버 측에서 pre-render한 <div> 태그 안에 일치하는 <p> 태그가 클라이언트에서의 렌더링 결과에는 있는 것이 Hydration Error를 일으킨 것이다.
시나리오를 통해 더 자세히 알아보자
서버 측 pre-render에서는 isRender이 true이므로, 빈 문자열이 렌더된다. 이는 곧 서버 측에서 생성되는 HTML에는 해당 부분이 아예 나타나지 않는다.
클라이언트 측에서는 JavaScript가 활성화되면서 truthy 조건에 따라 빈 문자열을 렌더링하기 위해 <p> 태그가 렌더링 될 수 있다.
서버 측에서 생성된 HTML에는 <div> 태그 안에 <p> 태그가 없지만, 클라이언트 측에서 생성된 HTML에는 <p> 태그가 존재한 채로 렌더링되는 상황이 발생하게 되어 서버와 클라이언트 간에 HTML 구조가 일치하지 않게 된다.
truthy 조건을 서버와 클라이언트 간 불일치하지 않도록 수정해야한다!
export default function RenderErrorModal() {
// 쿠키에 저장된 userId를 꺼내오고 변수에 저장
const cookie = new Cookies();
const userId = cookie.get('userId');
const [isRender, setIsRender] = useState(true);
useEffect(() => {
if (userId) setIsRender(false);
}, [userId]);
return (
<div>
/* isRender가 truthy하다면 비로그인 상태이므로 에러 모달을 띄운다 */
{isRender && <ErrorModal />}
</div>
)
}
아주 간단했다. isRender가 truthy일 때, 빈 문자열이 아니라 아예 렌더하지 않았어야했다.
렌더링 로직을 삼항연산자로 구성하냐 AND 연산자로 구성하냐 이 차이일 뿐인데,
SSR을 통해 서버에서 처음에 내려주는 UI(pre-render)와 클라이언트에서 첫 번째로 렌더되는 UI 트리간에 차이가 발생하는 것이었다.
UI 렌더링 로직을 서버 사이드 렌더링과 클라이언트 렌더링 양쪽에서 일관성 있게 유지하면서 작성하는 것이 중요하다.
이를 위해 조건부 렌더링 시 방식이 서버와 클라이언트에서 똑같은 결과를 만들어내는지 항상 확인하는 것이 좋겠다.