Next.js

Next.js & React HTML Nesting 완전 정복 가이드 - Hydration Mismatch

중년개발자
중년개발자

@loxo

25일 전

41

Next.js와 React 환경에서 HTML nesting(중첩) 규칙으로 인한 Hydration 오류는 많은 개발자들이 경험하는 문제입니다.


📘 Next.js & React HTML Nesting 완전 정복 가이드


1. 💡 왜 HTML Nesting 오류가 Hydration Mismatch를 일으킬까?

Next.js(SSR/SSG)는 Node.js 서버에서 먼저 HTML 문자열을 만들어서 브라우저로 전송합니다.

html
<!-- 서버가 생성한 raw HTML 예시 --> <p> <div>안녕하세요</div> </p>

하지만 브라우저의 HTML Parser는 HTML 표준 명세에 따라 다음과 같이 동작합니다:

⚠️ "표준 규칙: <p>(문단) 태그 안에 <div>(블록)가 들어오면, 브라우저는 <p>가 끝난 것으로 판단하고 즉시 </p>를 자동으로 닫아버린다."

결국 브라우저 DOM은 다음과 같이 변형됩니다:

html
<!-- 브라우저가 자동 정정한 DOM --> <p></p> <div>안녕하세요</div> <p></p>

이후 클라이언트에서 React가 JavaScript를 실행(Hydration)하며 자신이 기억하는 Virtual DOM (<p> 안에 <div>)과 브라우저 DOM (<p> 뒤에 <div>)을 비교할 때 구조가 달라졌음을 감지하고 경고를 발생시킵니다:

text
❌ Hydration failed because the initial UI does not match what was rendered on the server. Warning: In HTML, <div> cannot appear as a descendant of <p>.

2. 🧱 잊지 않는 HTML 태그 계급도 (3대 직업군)

복잡한 HTML 명세를 모두 외울 필요 없이, 모든 태그를 3가지 직업군으로 나누어 생각하면 자연스럽게 몸에 익습니다.

① 상자형 (Block / Container)

  • 대표 태그: <div>, <section>, <article>, <main>, <header>, <footer>, <aside>
  • 수용 범위: 거의 모든 태그(상자, 문장, 글자)를 담을 수 있습니다.
  • 규칙: 구조를 잡는 최외각 뼈대로 사용합니다.

② 문장형 (Paragraph / Inline)

  • 대표 태그: <p>, <h1>~<h6>, <span>, <strong>, <em>, <code>
  • 핵심 규칙 (Golden Rule):
    • <p> 태그는 "오직 순수한 글자(Inline)와 텍스트만 담는 프린터"입니다.
    • <p> 태그 안에는 절대로 상자형(<div>, <ul>, <form>)이나 다른 문장형(<p>, <h1>)을 넣을 수 없습니다.

③ 상호작용형 (Interactive Element)

  • 대표 태그: <button>, <a>, <input>, <select>
  • 핵심 규칙 (Golden Rule):
    • "클릭 유전자끼리는 중첩 금지!"
    • <button> 안에 <a>를 넣거나, <a> 안에 또 다른 <a><button>을 넣을 수 없습니다.

👑 엄격한 가문 (Direct Parent-Child Requirements)

  • <ul>, <ol>: 직속 자식은 무조건 <li> 만 허용!
  • <table>: <table><thead>/<tbody>/<tfoot><tr><th>/<td> 순서를 엄격히 준수. <table> 바로 아래에 <div><span> 배치 금지!

3. ⚠️ 자주 실수하는 TOP 5 사례 & 올바른 코드

❌ 1위: Typography/Card 컴포넌트의 <p> 내부 <div>

UI 라이브러리(Shadcn UI, MUI 등)나 직접 만든 <Text> 컴포넌트를 쓸 때 가장 흔한 실수입니다.

tsx
// ❌ 잘못된 코드: <p> 안에 <div> (Flex box나 Icon 래퍼) <p className="description"> 설명 텍스트입니다. <div className="badge">NEW</div> {/* 💥 Hydration Error! */} </p> // ⭕ 올바른 코드: 바깥을 <div>로 바꾸거나 내부를 <span>으로 변경 <div className="description"> 설명 텍스트입니다. <span className="badge">NEW</span> </div>

❌ 2위: 카드 전체 클릭 링크 (<a>) 안에 버튼/태그 링크

카드 전체를 <Link>로 감싸고, 카드 내부의 '프로필 버튼'이나 '태그 링크'를 또 클릭 가능하게 만든 경우입니다.

tsx
// ❌ 잘못된 코드: <a> 안에 <button> 또는 <a> <Link href="/posts/1"> <div className="card"> <h3>포스트 제목</h3> <button onClick={handleLike}>좋아요</button> {/* 💥 <a> 안에 <button> 중첩! */} </div> </Link> // ⭕ 올바른 코드: 클릭 이벤트를 분리하거나 CSS absolute/Event Delegation 활용 <div className="card-wrapper relative"> <Link href="/posts/1" className="absolute inset-0"> <span className="sr-only">포스트 읽기</span> </Link> <h3>포스트 제목</h3> <button onClick={handleLike} className="relative z-10">좋아요</button> </div>

❌ 3위: 드롭다운/리스트 UI 구현 시 <ul> 바로 아래 <div>

리스트 정렬이나 애니메이션 래퍼를 <ul> 바로 밑에 둘 때 발생합니다.

tsx
// ❌ 잘못된 코드: <ul> 직속 자식으로 <div> 사용 <ul> <div className="scroll-wrapper"> {/* 💥 <ul> 직속 자식은 <li>만 가능! */} <li>아이템 1</li> <li>아이템 2</li> </div> </ul> // ⭕ 올바른 코드: <div>를 <ul> 바깥으로 이동 <div className="scroll-wrapper"> <ul> <li>아이템 1</li> <li>아이템 2</li> </ul> </div>

❌ 4위: <table> 구조 단순화 시도

테이블을 만들 때 <tbody>를 생략하거나 <tr> 없이 <td>를 넣는 경우입니다.

tsx
// ❌ 잘못된 코드 <table> <tr> <div> {/* 💥 <tr> 안에는 <th> 또는 <td>만 가능 */} <td>내용</td> </div> </tr> </table> // ⭕ 올바른 코드 <table> <tbody> <tr> <td> <div>내용</div> {/* <td> 내부에는 <div> 가능 */} </td> </tr> </tbody> </table>

❌ 5위: <p> 안에 <p> (부모-자식 컴포넌트 조합)

부모 컴포넌트가 <p> 태그로 감싸고 있는데, 자식 컴포넌트가 내부에서 또 <p>를 반환할 때 발생합니다.

tsx
// SubText.tsx const SubText = () => <p>부연 설명입니다.</p>; // Page.tsx // ❌ 잘못된 코드: <p> 안에 <p>가 중첩됨 <p> 기본 문장입니다. <SubText /> {/* 💥 <p><p>부연 설명입니다.</p></p> */} </p> // ⭕ 올바른 코드: 부모 요소를 <div>로 변경 <div> <p>기본 문장입니다.</p> <SubText /> </div>

4. 🔍 에러 발생 시 3초 만에 원인 찾는 디버깅 노하우

Hydration 오류가 났을 때 다음 단계로 원인을 직관적으로 파악할 수 있습니다.

1️⃣ Next.js Dev Overlay / Console Log 읽기

오류 메시지에서 명시하는 태그 조합을 먼저 확인합니다.

  • In HTML, <p> cannot appear as a descendant of <p>.
  • Expected server HTML to contain a matching <div> in <p>.

2️⃣ 브라우저 F12 (Elements 탭) vs 소스 보기 (Ctrl + U) 비교

  • Ctrl + U (서버 응답): 서버가 보낸 원본 HTML을 확인합니다.
  • F12 Elements (브라우저 DOM): 브라우저가 파싱 후 자동 정정한 DOM을 확인합니다.
  • 꿀팁: Elements 탭에서 <p></p>처럼 내용 없이 빈 상태로 바로 닫혀있는 <p> 태그가 있다면, 100% 그 지점에 <div>나 다른 블록 태그가 잘못 들어가 브라우저가 강제로 닫은 것입니다.

5. 🧠 자연스럽게 몸에 익히는 「3초 코딩 체크 공식」

코딩할 때 매번 명세서를 찾아보지 않고 자연스럽게 몸으로 체득하는 3단계 자가 점검법입니다.

text
[1초] 🏷️ "내가 지금 여는 태그가 <p> 인가?" └─ YES ──> "내부에 div, ul, p, h1~6이 들어가는가?" └─ YES ──> 즉시 outer 태그를 <div>로 교체! [2초] 👆 "내가 지금 만드는 요소가 클릭 가능한 버튼/링크인가?" └─ YES ──> "이 안이나 밖에 또 다른 button/Link가 있는가?" └─ YES ──> 이벤트 구조 분리! (절대 겹치지 않기) [3초] 👪 "이 태그가 <ul>, <ol>, <table> 가문인가?" └─ YES ──> "직속 자식이 li, tr/tbody 인가?" └─ NO ──> 직속 자식 규칙 재정립!

💡 요약 한 줄 리마인더

"문장을 쓸 때는 <p>, 상자를 만들 때는 <div>, 클릭 요소(a/button)끼리는 절대 겹치지 않는다!"

#Next.js#React#Hydration Mismatch#HTML Nesting#SSR

댓글 0

Ctrl + Enter를 눌러 등록할 수 있습니다
※ AI 다듬기는 내용을 정제하는 보조 기능이며, 최종 내용은 사용자가 확인해야 합니다.