HelloWook.life

'use client'는 CSR이 아닙니다

Next.js에 처음 입문했을 때였습니다. 저는 사용자 경험을 위해 토스트(Toast) 알림 컴포넌트를 구축하기로 했습니다.

이러한 전역 UI 요소들은 React 컴포넌트 트리의 구조와 관계없이 브라우저의 DOM 최상단(대개 document.body 또는 #portal-root)에 렌더링되는 것이 일반적입니다. 따라서 저는 ReactDOM.createPortal을 사용하여 컴포넌트를 구현했습니다.

하지만 개발 서버를 띄우자마자, 예상치 못한 에러에 직면했습니다. 바로 하이드레이션 불일치 에러(Hydration Error) 였습니다.

에러 메시지 요약: "서버에서 렌더링된 HTML 마크업이 클라이언트에서 생성된 마크업과 일치하지 않습니다."

이 문제를 해결하기 위해 가장 먼저 클라이언트 컴포넌트로 명시하는 지시자인 'use client'를 파일 상단에 추가했습니다. 이 컴포넌트는 명백히 브라우저의 windowdocument에 의존하고 있었기 때문이죠.

그러나 놀랍게도 에러는 사라지지 않았습니다. 분명 createPortal을 사용하는 코드를 클라이언트에서 실행하도록 지시했는데, 왜 Next.js는 계속해서 마크업 불일치를 이야기하는 걸까요?

원인

'use client' 지시자는 "이 컴포넌트의 최종 렌더링과 상호작용은 클라이언트에서 이루어진다" 고 선언하는 것입니다. 그러나 이는 페이지의 초기 로딩 방식, 즉 서버 렌더링(SSR) 단계까지 막는 것은 아닙니다.

Next.js에서 'use client' 컴포넌트도 성능 최적화를 위해 서버에서 한 번 렌더링(Pre-render) 됩니다. 이 초기 서버 렌더링을 통해 HTML 마크업이 클라이언트로 전달되죠.

클라이언트는 이 마크업을 받아 브라우저에 표시합니다. 이후 자바스크립트가 로드되면, React는 서버가 보낸 이 HTML 구조 위에 자바스크립트 로직과 이벤트 핸들러를 연결하는 과정, 즉 하이드레이션(Hydration) 을 시작합니다.

하이드레이션 에러는 이 서버 마크업과 클라이언트 컴포넌트가 생성하는 최종 마크업이 일치하지 않을 때 발생합니다.

렌더링 방식 비교

순수 CSR

초기 HTML이 비어 있고, 대규모 JS 번들이 로드된 후 화면 구성 (느린 TTI)

Next.js SSR

서버에서 마크업을 생성하여 전송. JS 로드 후, React가 마크업에 하이드레이션 진행

createPortaldocument 객체를 사용해 React 트리 밖의 DOM 노드에 접근하고 조작해야 하는데, 서버 환경(Node.js) 에는 document 객체가 존재하지 않습니다.

서버 렌더링 시점에 이 코드가 실행되거나 불완전하게 처리되면, 서버가 클라이언트에게 보낸 마크업과 클라이언트에서 JS가 기대하는 마크업 사이에 필연적인 불일치가 발생하게 됩니다.

해결 방법

그렇다면 어떻게 해결할 수 있을까요? 이 컴포넌트가 클라이언트 환경(DOM)에서만 실행됨을 보장하기 위해 useEffect 훅을 사용합니다.

useEffect는 컴포넌트가 마운트된 후, 즉 브라우저 환경에서만 실행되기 때문에 서버 렌더링 단계에서는 실행되지 않습니다.

'use client';
 
import { useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
 
export default function Toast() {
  const [mounted, setMounted] = useState(false);
 
  // 클라이언트에서만 실행됨을 보장
  useEffect(() => {
    setMounted(true);
  }, []);
 
  // 마운트되기 전에는 아무것도 렌더링하지 않음
  if (!mounted) return null;
 
  // 마운트 후에는 포털을 통해 렌더링
  return createPortal(<div className='toast'>토스트 내용</div>, document.body);
}

이렇게 하면 서버에서는 null을 렌더링하고, 클라이언트에서 하이드레이션이 완료된 후에만 포털을 생성하므로 마크업 불일치가 발생하지 않습니다.

정리

Next.js에서 기본적으로 모든 컴포넌트는 서버 컴포넌트입니다.

'use client' 지시자는 해당 컴포넌트가 클라이언트에서 사용될 것임을 나타내지만, 여전히 SSR을 통해 초기 HTML이 렌더링됩니다. 이후 하이드레이션을 통해 이벤트 핸들링 등의 인터랙티브 기능이 활성화됩니다.

반면 서버 컴포넌트는 순수 HTML 형태로만 클라이언트에 제공되어, JS 번들 크기를 줄이는 역할을 합니다.

번외: 'use server'는?

'use server' 지시자는 서버 컴포넌트와는 다른 개념으로, 서버 액션(Server Actions) 을 사용하겠다는 의미입니다. 폼 제출이나 데이터 수정 같은 서버 사이드 작업을 처리할 때 사용됩니다.