하이드레이션 에러는 오직 "클라이언트 컴포넌트" 에서만 일어납니다.
리액트의 하이드레이션 에러를 이해하기 위해, 서버 컴포넌트와 클라이언트 컴포넌트 그리고 하이드레이션 기법에 대한 이해가 필요해요.

하이드레이션 기법
"물을 주다" - "촉촉하게 하다" 라는 의미로, 메마른 클라이언트 컴포넌트에 이벤트 등 JS 로직을 불어 넣어주는 과정이에요.
즉, 서버에서 만들어진 HTML에 클라이언트의 JS 로직을 붙이는 과정입니다. (이벤트 핸들러/ 혹은 리액트의 상태 등)
참고로 서버 환경에서는 브라우저가 존재하지 않으니, 브라우저에서만 실행될 수 있는 코드들은 사용할 수 없습니다.
이벤트핸들러, webAPI나, useState, useEffect 등의 코드들은 서버 컴포넌트에서는 사용할 수 없습니다.
서버 컴포넌트
서버에서 렌더링해서 보내주는 컴포넌트입니다.
서버에서 렌더링해서 보내주기 때문에, 브라우저 환경에서만 사용할 수 있는 로직을 사용할 수 없습니다.
정적인 HTML + CSS 형식의 코드만 클라이언트(브라우저)에 보내주기 때문이에요.
이때, 서버에서 렌더링해주는 컴포넌트도 2가지 방식으로 나누어 볼 수 있어요.
SSR과 SSG.
| SSR | 클라이언트의 요청이 올 때마다, 서버에서 렌더링해서 렌더링 된 결과물을 클라이언트에 보내주는 방식 |
| SSG | 빌드 타임에 한번 렌더링해두고, 그렇게 만들어진 HTML을 요청 때마다 보내주는 방식 (거의 고정적인 페이지에서 사용) |
참고로 서버 컴포넌트를 직접 사용하기 위해서는 Next.js 프레임워크를 사용하는 것을 추천합니다.
리액트에서 서버 컴포넌트를 지원해주지만, 이 서버 컴포넌트를 사용하기 위해서는 클라이언트에 보내주어야 할 번들링된 파일을 구분해야 하기 때문에 번들링된 JS 파일을 클라이언트와 서버용으로 나누어야하고, 서버 컴포넌트를 구현하기 위한 리액트만의 라이브러리를 사용해야 해요. 또한 이것저것 설정해줄게 많아요.
리액트 서버 컴포넌트(RSC)가 발표되기 이전부터 Next.js 는 SSR, SSG를 지원했는데 어떻게 가능했나요?
: 초기엔 Page Router 방식만 존재했는데, 이때에는 클라이언트 컴포넌트 기반 SSR/SSG를 지원했어요.
즉, 클라이언트 컴포넌트를 서버에서 한번 실행해 HTML로 렌더링하고, 클라이언트에게 보내주는 방식이었어요.
이를 지원하기 위해 getServerSideProps, getStaticProps 함수를 제공했고요.
Next.js 의 App Router 방식은요?
: 그건 2022년에 버전 13 이후부터 시작되었고, 이때부터 RSC를 공식적으로 지원해주기 시작했어요.
그러면서 자연스레 기본적으로 컴포넌트를 서버 컴포넌트로 간주하게 되었어요. 클라이언트 컴포넌트를 사용하고 싶으면 리액트에서 지원하는 "use client" 지시어를 사용해야 했고요.
그렇게 되면서 서버 컴포넌트 내부의 클라이언트 컴포넌트는 placeholder로 치환해버리고 클라이언트에 보내서 클라이언트에서 렌더링하도록 만들었습니다.
클라이언트 컴포넌트
클라이언트에서 렌더링을 "완료"해야하는 컴포넌트 입니다. 따라서 번들링된 JS 파일도 서버로부터 받아와야만 해요.
서버 컴포넌트는 오직 HTML만 받아오는데, 클라이언트 컴포넌트는 JS 파일도 받아오고 실행해야하니 확실히 초기 로딩이 느리죠.
(패킷으로 넘겨받아야하는 전송량의 차이가 있으니까요)
또한, 온전히 클라이언트에서만 렌더링하는 것은 아닐 수 있습니다. 즉, 서버에서 일부분 미리 렌더링한 것에 작업을 이어갈 수 있어요.
이 경우에 하이드레이션이 발생합니다.
하이드레이션의 과정은 다음과 같아요.
0. 서버에서 받아온 HTML
1. 클라이언트에서 컴포넌트를 직접 렌더하며 가상 DOM 생성
2. 서버에서 받아온 HTML과 비교함
3-1. DOM 트리 비교 결과, 일치하지 않으면 하이드레이션 에러 발생
3-2. 일치한다면, 재사용하고 그곳에 onClick, useState 등의 "기능"을 붙임
여기서 의문이 하나 들 수 있어요.
"어차피 클라이언트에서 가상 DOM을 만들어야 하면, SSR을 이용하는게 더욱 비용이 큰 거 아냐?
그냥 가상 DOM 만드는 김에 클라이언트에서 렌더링해버리면 끝인데,
하이드레이션 과정 때문에 더 많은 작업들이 수행되잖아."
맞는 말이에요. 하지만 SSR을 사용하는 이유는 명확해요.
1. SEO에 유리
2. 렌더링 된 초기 HTML을 받아오므로, 즉시 화면 표시가 가능(초기 로딩 빠름 - 단, 아주 잠깐은 정적인 화면 상태임)
3. 초기에 필요한 JS 번들 크기가 확실히 적을 수 있음 (서버 컴포넌트를 잘 활용하면 _ 클라이언트 컴포넌트는 해당 x)
실제로 전체 사용자 중 상당수는 서버에서 만든 HTML만 보고 떠나기도 하기 때문에 (e.g., 검색 유입 사용자),
CPU 비용을 다소 지불하더라도 속도 향상 및 UX 개선에 주안점을 둔 방식인겁니다.
하이드레이션 에러
하이드레이션 에러는 위에서 이미 언급해버렸지만, 다시 한번 말하자면
"서버에서 미리 렌더링해 받아온 클라이언트 컴포넌트의 HTML" 과 "클라이언트에서 초기 렌더링한 결과물"이 일치하지 않으면 발생하는 에러에요. 정확히 어느곳에 이벤트를 부여해야할 지 리액트에서 혼선을 겪기에 하이드레이션 에러를 터트리는거에요.
하이드레이션 에러가 발생하는 경우를 먼저 봅시다.
const [url] = useState<URL | null>(() => {
if (typeof window === undefined) {
return null;
}
return new URL(self.location.href);
});
이런 코드가 실제로 작성되어도, 당장은 하이드레이션 에러로 찍히지 않을 수 있습니다. (url이 렌더링에 쓰이지 않으면요!)
하지만 언젠가 url의 값이 화면을 구성하는데 쓰인다면, DOM 영역이 일치하지 않게 되고 하이드레이션 에러가 터집니다.
그때는 이미 이런 저런 코드가 쌓여있을테고, 하이드레이션의 원인을 찾기 위한 디버깅이 쉽지 않아질겁니다.
따라서 아래와 같이, 안전하게 useEffect를 사용하는 것이 좋습니다. 첫번째 렌더링 때는 서버의 HTML과 일치된 상태를 유지하고, useEffect 호출(오직 클라이언트에서만 일어남)이후에는 렌더링이 완료된 이후이므로 값이 변경되어도 상관 없으니까요.
(그래서 useEffect로 렌더링이 한번 더 일어나더라도, 안전성 측면에서 얻어갈 것이 훨씬 많습니다)
const [url, setUrl] = useState<URL | null>(null);
useEffect(() => {
setUrl(new URL(self.location.href));
}, []);
'Develop > Frontend' 카테고리의 다른 글
| 브라우저 캐싱과 헤더 필드 (0) | 2025.06.23 |
|---|---|
| Tanstack Query (React Query) 필수 지식 (0) | 2025.05.11 |
| css는 모듈로 뽑을 수 있을까? (1) | 2025.04.29 |
| React Reconciliation 심화 이해를 바탕으로 리액트 잘 활용하기 (2) | 2025.04.18 |
| 개발자도구 Element 탭 강조 표시의 기준 (1) | 2025.04.18 |