Transparent 콘텐츠 모델이란?

a, ins, del 요소가 가진 Transparent 콘텐츠 모델의 특수성을 이해하고, 부모 요소의 콘텐츠 모델을 상속하는 동작 원리와 올바른 사용법을 익힙니다

심화 15분 Transparent 모델 a 요소 ins del 상속

HTML 명세에서 대부분의 요소는 자신이 어떤 자식 요소를 허용하는지 명확하게 정의하고 있습니다. 그런데 a, ins, del과 같은 일부 요소들은 스스로 콘텐츠 모델을 정의하지 않고, 자신의 부모 요소가 허용하는 콘텐츠 모델을 그대로 따르는 독특한 방식을 취합니다. 이것이 바로 Transparent 콘텐츠 모델입니다. Transparent 모델을 이해하지 못하면, 겉보기에는 문제없어 보이는 마크업이 실제로는 명세 위반인 경우를 놓치게 됩니다. 특히 a 요소는 웹 개발에서 가장 빈번하게 사용되는 요소 중 하나이기 때문에, 이 모델의 작동 원리를 정확히 파악하는 것이 올바른 HTML 작성의 핵심이 됩니다.

핵심 특징

  • 🔍 부모 콘텐츠 모델 상속: Transparent 요소는 자체 콘텐츠 규칙이 없으며, 부모가 허용하는 것만 자식으로 포함할 수 있습니다
  • 🔗 대표 요소 a 태그: 가장 널리 쓰이는 Transparent 요소로, 부모가 블록 컨텍스트이면 블록 요소를, 인라인 컨텍스트이면 인라인 요소만 감쌀 수 있습니다
  • 📝 ins와 del의 이중성: 수정 이력을 나타내는 이 요소들은 Transparent 모델을 따르면서도 Flow Content와 Phrasing Content 양쪽 카테고리에 동시에 속하는 특수한 위치를 가집니다
  • ⚠️ 중첩 제한의 전파: Transparent 요소 안에 또 다른 Interactive Content를 넣을 수 있는지는 부모의 규칙에 의해 결정되며, 특히 a 요소 안에 a 요소를 넣는 것은 항상 금지됩니다
  • 🧩 유효성 검사의 맹점: 브라우저가 관대하게 렌더링하더라도, Transparent 모델을 위반한 마크업은 접근성 도구나 파서에서 예기치 않은 동작을 유발할 수 있습니다

실무에서의 영향

Transparent 콘텐츠 모델은 실무에서 카드 UI, 링크 래핑, 수정 이력 표시 등 다양한 패턴에서 직접적인 영향을 미칩니다. 예를 들어, 카드 전체를 클릭 가능하게 만들기 위해 a 태그로 감싸는 패턴은 매우 흔하지만, 부모 요소가 Phrasing Content만 허용하는 맥락에서 블록 요소를 포함하면 명세 위반이 됩니다. insdel을 사용한 문서 변경 이력 관리에서도 Transparent 모델을 이해해야 어떤 위치에서 어떤 요소를 감쌀 수 있는지 정확히 판단할 수 있습니다. 이 원리를 모르면 HTML Validator 경고를 무시하거나 잘못된 구조를 반복하게 되고, 결국 스크린 리더 호환성 문제나 SEO 품질 저하로 이어질 수 있습니다. Transparent 모델을 정확히 이해하면, 마크업의 의미적 정확성과 접근성을 동시에 확보하면서도 유연한 UI 구조를 설계할 수 있습니다.


핵심 개념

Transparent 콘텐츠 모델의 정의

입문

HTML에서 어떤 요소들은 자기만의 규칙이 없고, 부모의 규칙을 그대로 따라요. 이걸 Transparent(투명한) 콘텐츠 모델이라고 해요.

🪟 투명한 상자란? 투명한 유리 상자를 생각해보세요. 이 상자는 자기만의 색깔이 없어서, 바깥에서 보면 마치 상자가 없는 것처럼 보여요. HTML에서 Transparent 요소도 마찬가지예요. 이 요소를 제거해도 안에 있는 내용이 부모 요소 안에서 여전히 유효하다면, 그 요소는 “투명하다”고 해요.

📦 어떤 요소들이 투명한가요? 대표적으로 링크를 만드는 a 태그, 그리고 내용이 추가되었다는 표시를 하는 ins, 삭제되었다는 표시를 하는 del이 투명한 요소예요. 이 요소들은 자기 안에 무엇을 넣을 수 있는지 스스로 정하지 않고, 자신의 부모가 정한 규칙을 그대로 따라요.

🎯 왜 투명해야 하나요? 링크(a 태그)는 글자 하나에도, 카드 전체에도 걸 수 있어야 해요. 만약 a 태그가 “나는 글자만 감쌀 수 있어!”라고 자기 규칙을 정해버리면, 카드 전체를 링크로 만들 수 없겠죠. 그래서 부모가 허용하는 범위 안에서 자유롭게 쓸 수 있도록 투명하게 설계된 거예요.

💡 쉽게 기억하는 방법 “투명한 요소를 벗겨내도 문서가 여전히 올바르면 OK”라고 기억하면 돼요. 투명한 포장지를 벗겨도 선물이 그대로인 것처럼, Transparent 요소를 제거해도 안의 내용이 부모 안에서 문제가 없어야 해요.

중급

Transparent 콘텐츠 모델은 HTML 명세에서 특정 요소가 자체적인 콘텐츠 모델을 정의하지 않고, 부모 요소의 콘텐츠 모델을 그대로 상속받는 방식입니다.

Transparent의 의미 “Transparent”라는 이름은 해당 요소가 콘텐츠 모델 관점에서 투명하다는 뜻입니다. 즉, 그 요소를 DOM 트리에서 제거했을 때 자식 요소들이 부모 요소 안에서 여전히 유효한 콘텐츠여야 합니다. 이것이 Transparent 모델의 핵심 검증 기준입니다.

Transparent 요소 목록 HTML 명세에서 Transparent 콘텐츠 모델을 가진 요소는 a, ins, del, object, video, audio, map, canvas, slot 등입니다. 이 중 실무에서 가장 자주 접하는 것은 a, ins, del입니다.

<!-- 원본: a 요소가 p 안에서 span을 감싸고 있음 -->
<p><a href="/link"><span>텍스트</span></a></p>

<!-- 검증: a를 제거해도 유효한가? -->
<p><span>텍스트</span></p>
<!-- ✅ p 안에 span은 유효 → 올바른 사용 -->

<!-- 원본: p 안에서 a가 div를 감싸고 있음 -->
<p><a href="/link"><div>카드</div></a></p>

<!-- 검증: a를 제거하면? -->
<p><div>카드</div></p>
<!-- ❌ p 안에 div는 유효하지 않음 → 명세 위반 -->

심화

Transparent 콘텐츠 모델은 HTML Living Standard에서 콘텐츠 모델의 유연성과 의미적 정확성을 동시에 달성하기 위해 설계된 간접 참조(Indirection) 패턴입니다.

WHATWG HTML Living Standard의 Transparent 정의 HTML Living Standard §3.2.5.3에서 Transparent 콘텐츠 모델은 다음과 같이 정의됩니다: “Some elements are described as transparent; they have ‘transparent’ in the description of their content model. When a content model includes a part that is ‘transparent’, those parts of the content model are replaced by the content model of the parent element.” 즉, Transparent는 콘텐츠 모델의 위임(Delegation)으로, 해당 요소의 허용 콘텐츠 규칙이 부모의 규칙으로 대체됩니다.

DOM 트리 제거 테스트(Removal Test) 명세는 Transparent 모델의 유효성 검증을 위해 “제거 테스트”를 정의합니다. Transparent 요소와 그 시작/종료 태그를 DOM에서 제거했을 때, 자식 노드들이 부모 요소의 콘텐츠 모델에 여전히 적합해야 합니다. 이 테스트는 재귀적(Recursive)으로 적용되어, 중첩된 Transparent 요소가 있을 경우 가장 가까운 비-Transparent 조상(Non-transparent Ancestor)까지 올라가서 검증합니다.

파서 동작과 Tree Construction HTML 파서의 Tree Construction 단계에서 Transparent 요소는 특별한 처리를 받지 않습니다. 파서는 Transparent 속성을 인식하지 않으며, 일반적인 삽입 모드(Insertion Mode) 규칙에 따라 동작합니다. 따라서 명세 위반 마크업이라도 파서가 에러 복구(Error Recovery)를 통해 렌더링할 수 있으며, 이것이 개발자가 위반을 인지하지 못하는 주요 원인입니다.

부모 콘텐츠 모델 상속 메커니즘

입문

Transparent 요소는 “나는 부모님이 허락하는 것만 할 수 있어요”라는 규칙을 따라요. 부모가 바뀌면 같은 요소라도 할 수 있는 것이 달라져요.

🏠 부모님 집의 규칙 여러분이 친구 집에 놀러 가면, 그 집의 규칙을 따라야 하죠? 어떤 집은 거실에서 뛰어도 되지만, 어떤 집은 안 돼요. Transparent 요소도 마찬가지예요. 같은 요소라도 어떤 부모 안에 있느냐에 따라 허용되는 것이 완전히 달라져요.

🔄 같은 요소, 다른 규칙? 링크(a 태그)가 문단(p) 안에 있으면 글자 관련 요소만 감쌀 수 있어요. 하지만 같은 링크가 div 안에 있으면 제목, 문단, 목록 같은 큰 요소도 감쌀 수 있어요. 부모가 달라졌기 때문이에요!

🪆 여러 겹으로 투명하면? 만약 투명한 요소 안에 또 투명한 요소가 있다면 어떻게 될까요? 투명한 유리를 여러 장 겹쳐도 결국 바깥이 보이듯이, 투명한 요소가 겹쳐도 결국 가장 가까운 “불투명한” 부모의 규칙을 따르게 돼요.

🚦 정리하면 Transparent 요소의 규칙은 고정된 것이 아니라 “맥락에 따라 변하는 규칙”이에요. 같은 교실이라도 수학 시간과 체육 시간에 할 수 있는 활동이 다른 것처럼, 같은 Transparent 요소도 부모에 따라 담을 수 있는 내용이 달라져요.

중급

Transparent 요소의 콘텐츠 모델은 고정되어 있지 않고 부모 요소에 의해 동적으로 결정됩니다. 같은 a 요소라도 부모가 p인지 div인지에 따라 허용하는 자식이 완전히 달라집니다.

부모에 따른 콘텐츠 모델 변화

  • p > a: p의 콘텐츠 모델은 Phrasing Content → a 안에 Phrasing Content만 허용
  • div > a: div의 콘텐츠 모델은 Flow Content → a 안에 Flow Content 허용
  • span > a: span의 콘텐츠 모델은 Phrasing Content → a 안에 Phrasing Content만 허용
<!-- 부모가 div (Flow Content 허용) -->
<div>
  <a href="/card">
    <h2>제목</h2>        <!-- ✅ h2는 Flow Content -->
    <p>설명 텍스트</p>    <!-- ✅ p는 Flow Content -->
  </a>
</div>

<!-- 부모가 p (Phrasing Content만 허용) -->
<p>
  <a href="/link">
    <strong>강조 텍스트</strong>  <!-- ✅ strong은 Phrasing -->
    <em>기울임</em>              <!-- ✅ em은 Phrasing -->
  </a>
</p>

<!-- 부모가 p인데 Flow Content를 넣으면? -->
<p>
  <a href="/link">
    <div>블록 요소</div>  <!-- ❌ p는 Phrasing만 허용 -->
  </a>
</p>

재귀적 Transparent 해석 Transparent 요소가 중첩된 경우, 가장 가까운 비-Transparent 조상의 콘텐츠 모델이 적용됩니다. 예를 들어 div > a > ins > del > [자식]에서 a, ins, del 모두 Transparent이므로, 최종적으로 div의 Flow Content 모델이 적용됩니다.

<div>
  <a href="/link">
    <ins datetime="2024-01-01">
      <h3>새로 추가된 제목</h3>  <!-- ✅ div까지 올라가서 Flow Content 확인 -->
    </ins>
  </a>
</div>

<span>
  <a href="/link">
    <del datetime="2024-01-01">
      <h3>삭제된 제목</h3>  <!-- ❌ span까지 올라가면 Phrasing만 허용 -->
    </del>
  </a>
</span>

심화

부모 콘텐츠 모델 상속은 HTML 명세의 콘텐츠 모델 해석기(Content Model Resolver)가 Transparent 요소를 만났을 때 재귀적으로 조상 체인을 탐색하는 알고리즘적 과정입니다.

콘텐츠 모델 해석 알고리즘 Transparent 요소의 콘텐츠 모델을 결정하는 과정은 다음과 같습니다: (1) 현재 요소가 Transparent인지 확인, (2) Transparent이면 부모 요소로 이동, (3) 부모도 Transparent이면 다시 상위로 이동 (재귀), (4) 비-Transparent 조상을 찾으면 해당 요소의 콘텐츠 모델을 반환. 이 알고리즘의 종료 조건(Termination Condition)은 반드시 비-Transparent 조상이 존재해야 한다는 것이며, 문서 루트인 html 요소가 이를 보장합니다.

고아 Transparent 요소(Orphan Transparent Element) 명세에 따르면, Transparent 요소가 부모를 가지지 않는 경우(예: DocumentFragment의 직접 자식) Transparent 부분의 콘텐츠 모델은 “아무것도 허용하지 않음(Nothing)“으로 처리됩니다. 이는 DOM 조작을 통해 요소를 임시로 분리했을 때 발생할 수 있는 엣지 케이스(Edge Case)입니다. 실제 브라우저 구현에서는 이 규칙을 엄격하게 적용하지 않지만, 명세 준수형 밸리데이터(Conformance Checker)에서는 경고를 발생시킵니다.

Content Model과 Category의 구분 Transparent는 콘텐츠 모델(Content Model)이지, 콘텐츠 카테고리(Content Category)가 아닙니다. a 요소 자체는 Flow Content이자 Phrasing Content, Interactive Content 카테고리에 속하지만, 그 내부에 무엇을 담을 수 있는지를 결정하는 콘텐츠 모델이 Transparent입니다. 이 구분은 명세 해석에서 매우 중요하며, 혼동할 경우 “a는 Phrasing이니까 Phrasing만 담을 수 있다”는 잘못된 결론에 도달하게 됩니다.

a 요소의 Transparent 동작과 제약

입문

링크를 만드는 a 태그는 HTML에서 가장 많이 쓰이는 Transparent 요소예요. 하지만 아무거나 다 감쌀 수 있는 건 아니에요!

🔗 a 태그는 만능 포장지? a 태그를 포장지라고 생각해보세요. 이 포장지는 투명해서 안에 뭐가 들었는지 다 보여요. 그리고 어떤 크기의 선물이든 감쌀 수 있을 것 같지만, 사실 선반(부모 요소)의 크기에 맞는 선물만 감쌀 수 있어요.

🃏 카드 전체를 링크로 만들기 요즘 웹사이트를 보면 카드를 클릭하면 다른 페이지로 이동하는 경우가 많죠? 이때 a 태그로 카드 전체를 감싸는데, 이게 가능한 이유는 카드의 부모가 보통 큰 영역(블록 요소)이기 때문이에요.

🚫 절대 안 되는 것 a 태그 안에 또 다른 a 태그를 넣는 건 절대 안 돼요! 링크 안에 링크가 있으면 어떤 링크를 클릭한 건지 알 수 없잖아요. 또한 버튼이나 입력칸처럼 직접 조작하는 요소(인터랙티브 콘텐츠)도 a 안에 넣을 수 없어요.

🖱️ 왜 버튼은 안 되나요? 링크를 클릭하면 페이지가 이동해야 하는데, 안에 버튼이 있으면 “이 클릭이 링크인가요, 버튼인가요?”라는 혼란이 생겨요. 그래서 HTML 규칙에서 서로 충돌하는 조작 가능한 요소들은 겹쳐 넣지 못하게 막아놓은 거예요.

중급

a 요소는 가장 대표적인 Transparent 요소로, 부모의 콘텐츠 모델을 상속받아 매우 유연하게 사용할 수 있습니다. 하지만 Transparent 모델과 별개로, a 요소 고유의 제약 조건이 존재합니다.

a 요소의 콘텐츠 제약

  1. Interactive Content 금지: a 안에 a, button, input, select, textarea, details 등을 넣을 수 없습니다
  2. tabindex 속성을 가진 요소 금지: tabindex가 지정된 요소도 Interactive Content로 간주됩니다
  3. 위 제약은 부모의 콘텐츠 모델과 무관하게 항상 적용됩니다
<!-- ✅ 올바른 카드 링크 패턴 -->
<div class="card-list">
  <a href="/article/1" class="card">
    <article>
      <h3>기사 제목</h3>
      <p>기사 요약...</p>
      <time datetime="2024-01-15">2024년 1월 15일</time>
    </article>
  </a>
</div>

<!-- ❌ Interactive Content 중첩 위반 -->
<div>
  <a href="/page">
    <button>클릭</button>    <!-- ❌ button은 Interactive -->
  </a>
</div>

<!-- ❌ a 안에 a 중첩 금지 -->
<div>
  <a href="/outer">
    외부 링크
    <a href="/inner">내부 링크</a>  <!-- ❌ 항상 금지 -->
  </a>
</div>

브라우저의 에러 복구 동작 a 안에 a를 넣으면 브라우저 파서가 자동으로 바깥 a를 닫고 안쪽 a를 새로 엽니다. 이로 인해 의도한 DOM 구조와 실제 구조가 완전히 달라지며, 이벤트 핸들링이나 스타일링에서 예상치 못한 버그가 발생합니다.

심화

a 요소는 HTML Living Standard §4.5.1에서 정의되며, Transparent 콘텐츠 모델에 추가적인 제약이 결합된 대표적 사례입니다.

명세상 a 요소의 콘텐츠 모델 명세 원문: “Transparent, but there must be no interactive content or a element descendants.” 이 정의는 두 가지 규칙의 교집합(Intersection)을 의미합니다: (1) 부모의 콘텐츠 모델에 부합하는 요소만 허용 (Transparent), (2) Interactive Content 카테고리에 속하는 요소와 a 요소의 자손(Descendant)은 무조건 제외. 두 번째 규칙은 부모가 무엇이든 상관없이 적용되는 절대적 제약입니다.

HTML 파서의 Adoption Agency Algorithm a 요소가 중첩되었을 때 파서가 수행하는 에러 복구는 Adoption Agency Algorithm(AAA, HTML Living Standard §13.2.6.4.7)에 의해 처리됩니다. AAA는 서식 요소(Formatting Element)의 부적절한 중첩을 해결하기 위한 알고리즘으로, 액티브 포맷팅 엘리먼트 목록(List of Active Formatting Elements)과 오픈 엘리먼트 스택(Stack of Open Elements)을 조작합니다. a 안에 a가 나타나면 AAA가 외부 a를 암시적으로 닫고(Implicit Close) DOM 트리를 재구성하는데, 이 과정에서 원래 의도한 트리와 전혀 다른 구조가 만들어집니다.

접근성 트리(Accessibility Tree)에서의 영향 a 요소는 접근성 트리에서 link 역할(Role)로 매핑됩니다. Interactive Content가 중첩되면 보조 기술(Assistive Technology)이 포커스 순서(Focus Order)와 활성화 동작(Activation Behavior)을 올바르게 결정할 수 없습니다. WCAG 2.1 SC 4.1.2 “Name, Role, Value”와 SC 2.1.1 “Keyboard”를 동시에 위반하게 되어 접근성 감사에서 심각도 높은 위반으로 분류됩니다.

ins와 del의 이중 카테고리

입문

insdel은 문서에서 “이 부분이 새로 추가됐어요” 또는 “이 부분이 삭제됐어요”라고 표시하는 요소예요. 이 요소들은 아주 특별한 위치를 가지고 있어요.

📝 수정 표시 스티커 학교에서 시험지를 돌려받으면, 선생님이 빨간 펜으로 틀린 부분에 줄을 긋고(삭제 표시) 올바른 답을 옆에 써주시죠(추가 표시)? del은 빨간 줄, ins는 올바른 답을 써넣은 것과 같아요.

🎭 두 가지 얼굴을 가진 요소 insdel이 특별한 이유는 두 가지 역할을 동시에 할 수 있기 때문이에요. 작은 글자 사이에 끼워 넣을 수도 있고(인라인처럼), 문단이나 목록 같은 큰 덩어리를 감쌀 수도 있어요(블록처럼). 마치 변신 로봇이 자동차도 되고 로봇도 되는 것과 비슷해요!

🔍 어디에 있느냐에 따라 달라져요 ins가 문단(p) 안에 있으면 글자 수준의 내용만 감쌀 수 있고, div 같은 큰 영역 안에 있으면 문단이나 목록 같은 큰 내용도 감쌀 수 있어요. 부모의 규칙을 따르는 Transparent 특성 덕분이에요.

⚠️ 주의할 점 insdel을 사용할 때는 자신이 어디에 위치해 있는지를 꼭 확인해야 해요. 같은 요소라도 위치에 따라 담을 수 있는 내용이 달라지기 때문에, 잘못 사용하면 문서의 구조가 깨질 수 있어요.

중급

insdel 요소는 Transparent 콘텐츠 모델을 가지면서 동시에 Flow Content와 Phrasing Content 두 카테고리에 모두 속하는 독특한 요소입니다.

이중 카테고리의 의미

  • Flow Content에 속함 → body, div, section 등의 자식으로 사용 가능
  • Phrasing Content에 속함 → p, span, h1 등의 자식으로도 사용 가능
  • Transparent 콘텐츠 모델 → 내부에 허용되는 콘텐츠는 부모에 의해 결정

이 조합 덕분에 insdel은 거의 모든 위치에서 사용할 수 있으면서도, 각 위치에서 적절한 콘텐츠만 담을 수 있는 유연성을 갖습니다.

<!-- Flow Content 맥락: div 안에서 블록 요소를 감쌈 -->
<div>
  <ins datetime="2024-01-15">
    <p>새로 추가된 문단입니다.</p>
    <ul>
      <li>추가된 항목 1</li>
      <li>추가된 항목 2</li>
    </ul>
  </ins>
</div>

<!-- Phrasing Content 맥락: p 안에서 인라인 요소만 감쌈 -->
<p>
  원래 가격은 <del>30,000원</del>이었으나
  현재 <ins>19,900원</ins>으로 할인 중입니다.
</p>

<!-- ❌ 잘못된 사용: p 안에서 블록 요소를 감쌈 -->
<p>
  <del>
    <h3>삭제된 제목</h3>   <!-- ❌ p는 Phrasing만 허용 -->
  </del>
</p>

datetime 속성의 중요성 insdel에는 datetime 속성으로 변경 시점을, cite 속성으로 변경 사유 문서의 URL을 기록할 수 있습니다. 이 속성들은 단순 렌더링에는 영향을 주지 않지만, 문서의 변경 이력을 기계 판독 가능하게 만들어 접근성과 SEO에 도움을 줍니다.

심화

insdel은 HTML Living Standard §4.7.1, §4.7.2에서 정의되며, 콘텐츠 카테고리(Content Category)와 콘텐츠 모델(Content Model)의 교차점에서 독특한 위치를 차지합니다.

명세상 카테고리와 콘텐츠 모델 insdel의 명세 정의를 분해하면: Categories는 Flow Content, Phrasing Content이고, Content Model은 Transparent입니다. 카테고리는 “이 요소가 어디에 위치할 수 있는가”를 결정하고, 콘텐츠 모델은 “이 요소 안에 무엇을 넣을 수 있는가”를 결정합니다. 이중 카테고리와 Transparent 모델의 조합은 ins/del이 거의 모든 맥락에서 사용 가능하면서도, 각 맥락에서의 콘텐츠 규칙을 자동으로 조정하는 효과를 만듭니다.

Implicit Paragraph(암시적 문단) 경계와의 상호작용 insdel이 Flow Content 맥락에서 블록 레벨 콘텐츠를 포함할 때, HTML 파서의 암시적 문단(Implicit Paragraph) 규칙과 상호작용합니다. 명세 §3.2.5.4에 따르면 “When ins or del elements are used to mark up paragraphs or other block-level content, the elements should span the entire block.” 이는 ins/del이 문단의 일부만 감싸면서 동시에 블록 요소를 포함하는 것이 명세적으로 부적절함을 의미합니다.

스크린 리더와 변경 이력 인식 접근성 관점에서 insinsertion 역할, deldeletion 역할로 매핑됩니다. 그러나 주요 스크린 리더(NVDA, JAWS, VoiceOver)의 지원 수준이 상이합니다. VoiceOver는 ins/del의 역할을 음성으로 알려주지만, 일부 스크린 리더는 기본 설정에서 이 역할을 무시합니다. datetime 속성의 값은 스크린 리더에서 직접 노출되지 않으며, aria-label이나 시각적으로 숨겨진 텍스트(Visually Hidden Text)를 통해 보충 정보를 제공하는 것이 권장됩니다.

Interactive Content 중첩 제한

입문

HTML에는 클릭하거나 입력할 수 있는 특별한 요소들이 있어요. 이런 요소들을 서로 겹쳐서 넣으면 큰 문제가 생겨요.

🖱️ 인터랙티브 콘텐츠란? 버튼, 링크, 입력칸, 드롭다운 메뉴 같은 것들을 “인터랙티브 콘텐츠”라고 해요. 이들의 공통점은 사용자가 직접 클릭하거나 키보드로 조작할 수 있다는 거예요.

🎮 게임 컨트롤러 비유 게임 컨트롤러의 버튼 위에 또 다른 버튼을 올려놓으면 어떻게 될까요? A 버튼을 누르고 싶었는데 위에 B 버튼이 올려져 있으면, 내가 누른 게 A인지 B인지 알 수 없어요. 인터랙티브 요소를 겹치는 것도 똑같은 문제를 일으켜요.

🔗 링크 안에 버튼은 왜 안 되나요? 링크(a)를 클릭하면 다른 페이지로 이동해야 하는데, 안에 버튼(button)이 있으면 “이 클릭은 이동인가요, 버튼 동작인가요?”라는 혼란이 생겨요. 그래서 HTML은 이런 상황 자체를 규칙으로 막아놓았어요.

👁️ 보이지 않는 사용자를 위해 눈이 불편한 분들은 스크린 리더라는 프로그램으로 웹을 사용해요. 인터랙티브 요소가 겹쳐 있으면 스크린 리더가 “여기에 링크가 있고, 그 안에 버튼이 있다”고 알려주는데, 사용자는 어떤 것을 활성화해야 할지 혼란스러워해요.

중급

HTML 명세에서 Interactive Content는 사용자 상호작용을 위한 요소 카테고리입니다. Transparent 요소, 특히 a 요소에서 Interactive Content의 중첩은 엄격하게 금지됩니다.

Interactive Content에 해당하는 요소

  • a (href 속성이 있을 때)
  • button
  • input (type=“hidden”이 아닐 때)
  • select, textarea
  • details, embed
  • iframe, label
  • audio, video (controls 속성이 있을 때)
  • tabindex 속성이 있는 모든 요소

이 제약은 Transparent 모델의 부모 상속 규칙보다 우선합니다.

<!-- ❌ 위반: a 안에 button -->
<a href="/page">
  <div class="card">
    <h3>제목</h3>
    <button>좋아요</button>  <!-- Interactive Content 중첩 -->
  </div>
</a>

<!-- ✅ 해결: 카드 링크와 버튼을 분리 -->
<div class="card">
  <a href="/page">
    <h3>제목</h3>
  </a>
  <button>좋아요</button>  <!-- a 바깥에 배치 -->
</div>

<!-- ✅ 대안: CSS로 카드 전체 클릭 영역 구현 -->
<div class="card" style="position: relative;">
  <h3><a href="/page" class="stretched-link">제목</a></h3>
  <p>설명 텍스트</p>
  <button style="position: relative; z-index: 1;">좋아요</button>
</div>

왜 CSS 해결이 권장되는가 카드 전체를 클릭 가능하게 하면서 내부에 별도 인터랙티브 요소를 두어야 할 때, a로 전체를 감싸는 대신 CSS의 ::after 의사 요소나 stretched-link 패턴을 사용하는 것이 명세 준수와 접근성 모두를 만족하는 방법입니다.

심화

Interactive Content 중첩 제한은 HTML 명세의 콘텐츠 모델 제약과 사용자 에이전트의 활성화 동작(Activation Behavior) 모델이 결합된 규칙입니다.

활성화 동작(Activation Behavior) 충돌 모델 HTML Living Standard §6.6.3에서 정의하는 활성화 동작은 요소가 클릭 이벤트를 받았을 때 수행하는 기본 동작입니다. a 요소는 내비게이션(Navigation), button은 폼 제출(Form Submission) 또는 커스텀 동작을 수행합니다. 두 활성화 동작이 동일 이벤트 전파 경로(Event Propagation Path)에 존재하면, 이벤트 버블링(Event Bubbling) 과정에서 어떤 활성화 동작이 우선하는지 명세적으로 정의되지 않습니다. 이것이 중첩 금지의 근본적 이유입니다.

포커스 관리(Focus Management)와 탭 순서(Tab Order) Interactive Content가 중첩되면 Sequential Focus Navigation(탭 키로 이동하는 포커스 순서)에서 문제가 발생합니다. a 요소 안에 button이 있으면 탭 순서에서 두 요소가 모두 포커스 가능(Focusable)하게 되는데, 외부 a에 포커스가 있을 때 Enter 키를 누르면 내비게이션이 발생하고, 내부 button에 포커스가 있을 때는 버튼 활성화가 발생합니다. 이는 사용자 관점에서 동일 시각적 영역에서 다른 동작이 발생하는 혼란을 야기합니다. ARIA in HTML 명세에서도 link 역할 내부에 button 역할의 중첩을 금지하고 있습니다.

브라우저별 에러 복구 차이 a 안에 a가 중첩된 경우 HTML 파서 명세(§13.2.6.4.7, Adoption Agency Algorithm)에 의해 일관된 에러 복구가 이루어지지만, a 안에 button이 중첩된 경우는 파서 수준의 에러가 아닌 콘텐츠 모델 위반이므로 파서가 DOM을 재구성하지 않습니다. 결과적으로 위반된 DOM이 그대로 생성되어 브라우저마다 이벤트 처리 방식이 다를 수 있습니다. Chromium은 가장 안쪽 Interactive 요소의 활성화 동작을 우선하고, Gecko(Firefox)는 이벤트 전파 순서에 따라 처리합니다.