Interactive 콘텐츠는 HTML 콘텐츠 모델에서 사용자가 직접 상호작용할 수 있는 요소들의 집합입니다. 링크를 클릭하거나, 버튼을 누르거나, 텍스트를 입력하거나, 옵션을 선택하는 등 사용자의 행동에 반응하는 요소들이 여기에 해당합니다. 이 카테고리에는 명확한 중첩 금지 규칙이 존재하며, 이 규칙을 위반하면 브라우저가 마크업을 예측 불가능하게 수정하거나 접근성 보조 도구가 올바르게 동작하지 않는 심각한 문제로 이어질 수 있습니다. 단순히 “작동하는 것처럼 보인다”는 것이 “올바르다”는 의미가 아님을 이해하는 것이 이 주제의 핵심입니다.
🔑 핵심 문제점
- Interactive 콘텐츠 요소(
a,button,input,select,textarea,details등)는 다른 Interactive 콘텐츠 요소 안에 중첩될 수 없습니다 - 가장 흔한 실수는
<a>안에<button>을 넣거나,<button>안에<a>를 넣는 패턴으로, HTML 명세상 모두 유효하지 않습니다 - 브라우저는 잘못된 중첩을 만나면 자체적인 오류 복구 알고리즘으로 DOM을 재구성하며, 이 결과는 브라우저마다 달라질 수 있습니다
- 스크린 리더와 키보드 네비게이션은 Interactive 요소의 중첩을 전제하지 않고 설계되어 있어, 잘못된 중첩은 접근성 기기 사용자에게 직접적인 불편을 줍니다
- 시각적으로 “정상적으로 보이는” 레이아웃이더라도 내부 DOM 구조가 의도와 다르게 구성될 수 있어 JavaScript 이벤트 처리에서 예상치 못한 버그가 발생할 수 있습니다
왜 중요한가?
실무에서 UI 컴포넌트를 조합하다 보면 클릭 가능한 카드, 버튼처럼 보이는 링크, 드롭다운이 내장된 버튼 등 Interactive 요소를 중첩하고 싶은 상황이 자주 생깁니다. 이때 브라우저가 화면에 문제없이 렌더링해 주기 때문에 규칙 위반 사실을 인지하지 못하는 경우가 많습니다. 그러나 잘못된 중첩은 키보드만 사용하는 사용자나 스크린 리더 사용자에게 포커스 순서가 뒤틀리거나 요소가 아예 누락되는 심각한 접근성 문제를 일으킬 수 있습니다. 또한 React나 Vue와 같은 컴포넌트 기반 프레임워크에서는 부모-자식 컴포넌트 각각이 Interactive 요소를 렌더링할 경우 합성 결과가 명세를 위반할 수 있어, 컴포넌트 경계에서도 이 규칙을 의식적으로 확인해야 합니다. Interactive 콘텐츠의 중첩 제약을 이해하면 시각적으로만 검증하는 습관에서 벗어나, 마크업의 의미론적 구조와 접근성을 함께 고려하는 개발 역량을 키울 수 있습니다.
핵심 개념
Interactive 콘텐츠의 정의와 범위
입문
웹 페이지에서 우리가 직접 손대거나 클릭할 수 있는 것들이 있어요. HTML은 이런 요소들을 따로 묶어서 ‘Interactive 콘텐츠’라고 불러요!
🖱️ Interactive 콘텐츠가 뭔가요? ‘Interactive(인터랙티브)‘는 ‘상호작용하는’이라는 뜻이에요. 웹 페이지에서 그냥 읽기만 하는 글이나 이미지가 아니라, 클릭하거나 입력하거나 선택할 수 있는 요소들이 여기에 해당해요. 마치 책과 게임기의 차이처럼, 책은 읽기만 하지만 게임기는 버튼을 눌러서 조작할 수 있잖아요.
📋 어떤 요소들이 포함되나요?
가장 대표적인 것들은 링크(a), 버튼(button), 텍스트 입력창(input, textarea), 드롭다운 선택(select), 접기/펼치기(details) 등이에요. 공통점은 모두 사용자가 마우스나 키보드로 직접 다룰 수 있다는 거예요. 마치 리모컨의 버튼들처럼, 각각 다른 역할을 하지만 모두 ‘누르면 반응한다’는 공통점이 있어요.
🎯 조건부로 Interactive가 되는 요소도 있나요?
맞아요! 어떤 요소는 특별한 설정을 해줬을 때만 Interactive 콘텐츠가 돼요. 예를 들어 input 태그도 type="hidden"이면 화면에 보이지도 않고 클릭할 수도 없어서 Interactive 콘텐츠가 아니에요. tabindex 속성이 있는 요소도 키보드로 이동할 수 있게 되어 Interactive 콘텐츠로 분류돼요. 기능이 있어야 Interactive인 거예요!
💡 왜 이렇게 따로 분류하나요? 이 분류는 단순한 정리가 아니에요. HTML은 이 분류를 기준으로 “같은 종류끼리는 안에 넣으면 안 된다”는 규칙을 만들었어요. 마치 전기 콘센트를 콘센트 안에 꽂을 수 없는 것처럼요. 이 규칙이 왜 존재하는지는 다음 개념에서 배울 거예요!
중급
HTML Living Standard는 콘텐츠를 역할에 따라 여러 카테고리로 분류하며, Interactive 콘텐츠는 그 중 하나로 사용자와 직접 상호작용할 수 있는 요소들의 집합입니다.
무조건 Interactive 콘텐츠인 요소
a(href 속성 있을 때), button, details, embed, iframe, label, select, textarea가 해당합니다. 이 요소들은 별도 조건 없이 항상 Interactive 콘텐츠로 분류됩니다.
조건부로 Interactive 콘텐츠인 요소
audio(controls 속성 있을 때), img(usemap 속성 있을 때), input(type이 hidden이 아닐 때), video(controls 속성 있을 때), tabindex 속성이 명시된 모든 요소가 해당합니다.
<!-- 항상 Interactive 콘텐츠 -->
<a href="/page">링크</a>
<button>버튼</button>
<select><option>옵션</option></select>
<textarea></textarea>
<details><summary>접기/펼치기</summary></details>
<!-- 조건부 Interactive 콘텐츠 -->
<input type="text" /> <!-- Interactive (hidden이 아니므로) -->
<input type="hidden" /> <!-- Interactive 아님 -->
<audio controls></audio> <!-- Interactive (controls 있으므로) -->
<audio></audio> <!-- Interactive 아님 -->
<div tabindex="0">포커스 가능</div> <!-- tabindex로 Interactive가 됨 -->
다른 콘텐츠 카테고리와의 관계
Interactive 콘텐츠는 Flow 콘텐츠, Phrasing 콘텐츠와 중복 소속될 수 있습니다. 예를 들어 button은 Phrasing 콘텐츠이면서 동시에 Interactive 콘텐츠입니다. 콘텐츠 모델 규칙은 각 카테고리별로 독립적으로 적용됩니다.
심화
HTML Living Standard의 콘텐츠 카테고리 시스템은 파싱 알고리즘, 명세 유효성 검사, 접근성 맵핑의 세 레이어에서 동시에 역할을 수행하며, Interactive 콘텐츠 카테고리는 그 중 사용자 에이전트(User Agent)의 포인팅 디바이스 및 키보드 포커스 관리와 가장 밀접하게 연결된 분류입니다.
WHATWG 명세 기반 정의 HTML Living Standard, Section 3.2.5.2.7 (Interactive content)에 따르면, Interactive 콘텐츠는 사용자 상호작용을 위해 특별히 설계된 콘텐츠입니다. 명세는 각 요소에 대해 “if the element has an X attribute” 형태의 조건부 소속을 명시하며, 이는 요소의 콘텐츠 카테고리가 정적이 아닌 속성 상태에 따라 동적으로 결정됨을 의미합니다.
tabindex IDL 속성을 통한 Dynamic Interactive 분류는 특히 주목할 만합니다. 명세에 따르면 tabindex 속성이 명시된 모든 요소는 Interactive 콘텐츠로 분류되며, 이는 커스텀 위젯 구현 시 콘텐츠 모델 위반 위험을 내포합니다.
ARIA와의 교차점
WAI-ARIA 1.2 명세에서 Interactive 콘텐츠 요소들은 대부분 암묵적 ARIA 역할(Implicit ARIA Role)을 가집니다. button은 role="button", a[href]는 role="link", select는 role="listbox"에 매핑됩니다. 이 암묵적 역할은 접근성 트리(Accessibility Tree)에서 해당 요소의 의미론적 의미를 결정하며, 중첩 규칙 위반 시 접근성 트리 구조가 파괴되어 보조 기술(Assistive Technology)의 예측 기반 탐색이 실패하게 됩니다.
중첩 금지 규칙
입문
같은 종류의 Interactive 요소는 서로 안에 넣을 수 없어요. 버튼 안에 버튼, 링크 안에 버튼처럼요. 이게 왜 안 되는지 알아볼게요!
🪆 마트료시카 인형의 한계 러시아 전통 인형인 마트료시카는 인형 안에 더 작은 인형을 계속 넣을 수 있어요. 그런데 만약 버튼(리모컨 버튼)을 버튼 안에 넣으면 어떻게 될까요? 어떤 버튼을 눌러야 하는지, 두 버튼 중 어느 게 작동하는 건지 아무도 알 수 없게 돼요. 클릭 한 번이 두 버튼 중 어디에 전달되는지 컴퓨터도 혼란스러워요.
🚪 한 번에 하나의 문 집에서 문 안에 또 다른 문이 있다고 생각해보세요. 문을 “열었다”고 할 때 바깥 문을 연 건지, 안쪽 문을 연 건지 모호해져요. Interactive 요소도 마찬가지예요. 링크 안에 버튼이 있으면 사용자가 그 영역을 클릭했을 때 링크로 이동해야 할지, 버튼 동작을 실행해야 할지 명확하지 않아요. HTML은 이 모호함을 원천 차단하기 위해 중첩을 금지해요.
📏 HTML 명세의 규칙
HTML을 만든 사람들(WHATWG)은 “Interactive 콘텐츠는 Interactive 콘텐츠를 자식으로 가질 수 없다”는 규칙을 명세에 명시했어요. 예를 들어 button의 콘텐츠 모델은 “Phrasing 콘텐츠 중에서 Interactive 콘텐츠가 아닌 것”으로 정해져 있어요. 즉, 버튼 안에는 텍스트나 이미지는 넣을 수 있지만 다른 버튼이나 링크는 넣을 수 없어요.
🎭 가장 자주 하는 실수들
실제 코드를 짜다 보면 자연스럽게 이 규칙을 어기게 되는 상황이 생겨요. 클릭 가능한 카드 전체를 <a> 태그로 감싸고 그 안에 <button>을 넣거나, 드롭다운 버튼 안에 링크를 넣는 경우가 대표적이에요. 화면에서는 잘 보이는데 실제로는 규칙 위반이에요!
중급
HTML Living Standard는 Interactive 콘텐츠 요소의 콘텐츠 모델에 명시적으로 Interactive 콘텐츠 자식을 금지합니다. 이 규칙은 브라우저 파서가 강제하는 구문 제약이며, 단순한 권고 사항이 아닙니다.
주요 요소별 콘텐츠 모델 제약
button: “Phrasing content, but there must be no interactive content descendant”a: “Transparent, but there must be no interactive content descendant”label: “Phrasing content, but there must be no descendant labelable elements”details:summary이후 Flow 콘텐츠 허용, 단 Interactive 콘텐츠 중첩 불가
<!-- 잘못된 예시: a 안에 button (명세 위반) -->
<a href="/page">
<button>클릭</button> <!-- Interactive 안에 Interactive -->
</a>
<!-- 잘못된 예시: button 안에 a (명세 위반) -->
<button>
<a href="/page">링크</a> <!-- Interactive 안에 Interactive -->
</button>
<!-- 올바른 예시: 중첩 없이 역할 분리 -->
<a href="/page" role="button">버튼처럼 스타일된 링크</a>
<button onclick="location.href='/page'">이동 버튼</button>
<!-- 잘못된 패턴: a로 카드 전체 감싸고 내부에 button -->
<a href="/detail">
<div class="card">
<h3>제목</h3>
<button>상세 보기</button> <!-- 명세 위반 -->
</div>
</a>
<!-- 올바른 패턴: CSS로 카드 전체 클릭 구현 -->
<div class="card">
<h3>제목</h3>
<a href="/detail" class="card-link">상세 보기</a>
</div>
심화
Interactive 콘텐츠 중첩 금지 규칙은 HTML 파싱 알고리즘의 “Adoption Agency Algorithm”과 직접 연결되어 있으며, 단순한 콘텐츠 모델 제약을 넘어 브라우저의 토큰화(Tokenization) 및 트리 구성(Tree Construction) 단계에서 강제됩니다.
WHATWG 명세의 명시적 금지 조항
HTML Living Standard, Section 4.10.6 (button element)의 Content model 항목은 “Phrasing content, but there must be no interactive content descendant and there must be no descendant with the tabindex attribute specified”로 명시합니다. a 요소(Section 4.5.1)는 “Transparent; however, there must be no interactive content or a element descendants”로 정의됩니다.
여기서 a의 “transparent” 콘텐츠 모델은 특히 중요합니다. Transparent 모델은 부모 요소의 콘텐츠 모델을 상속받는다는 의미인데, Interactive 콘텐츠 제외 조항이 transparent 상속을 오버라이드합니다. 이는 a 안에 div를 넣는 것은 허용되지만, a 안에 button을 넣는 것은 부모가 무엇이든 불허됨을 의미합니다.
파싱 알고리즘 레벨의 강제
HTML 파서의 Tree Construction 단계에서 “in body” 삽입 모드(Insertion Mode)는 Interactive 요소를 열 때 스택(Open Elements Stack)에 이미 동일 카테고리 요소가 있으면 해당 요소를 강제 닫습니다. 이 동작은 Section 8.2.6.4.7에 정의된 각 요소별 시작 태그 처리 규칙에 내장되어 있으며, generate implied end tags와 clear the stack back to a button context 등의 알고리즘이 연쇄 실행됩니다.
Transparency와 중첩 규칙의 상호작용
React/Vue 컴포넌트 합성 시, 부모 컴포넌트가 <a>를 렌더링하고 자식 컴포넌트가 내부적으로 <button>을 렌더링하는 구조는 컴포넌트 경계에서 명세 위반이 숨겨집니다. 정적 분석 도구(eslint-plugin-jsx-a11y의 anchor-has-content, interactive-supports-focus 규칙)와 HTML 유효성 검사기만이 이를 감지할 수 있으며, 런타임에서는 DOM이 이미 재구성된 이후입니다.
브라우저 오류 복구와 DOM 재구성
입문
명세를 어긴 HTML을 브라우저에 보내도 브라우저는 그냥 오류를 뱉지 않아요. 대신 스스로 고쳐서 보여줘요. 그런데 이 ‘자동 수정’이 우리가 원하는 대로가 아닐 수도 있어요!
🔧 자동으로 수리하는 브라우저 브라우저는 잘못된 HTML을 만나면 오류 메시지를 보여주는 대신 “아, 이렇게 하려는 거겠지?”라고 추측해서 스스로 고쳐요. 마치 글씨가 틀린 문자 메시지를 받았을 때 문맥으로 의미를 파악하는 것처럼요. 이걸 ‘오류 복구(Error Recovery)‘라고 해요.
🤔 어떻게 고치나요?
예를 들어 링크(a) 안에 버튼(button)을 넣으면 브라우저는 이렇게 생각해요: “a 안에 button은 안 되는데… button을 a 밖으로 꺼내야겠다!” 그래서 실제 화면에 그려지는 HTML(DOM)은 우리가 작성한 코드와 달라져요. 링크가 버튼 앞과 뒤로 쪼개지거나, 버튼이 완전히 링크 밖으로 이동할 수 있어요.
😱 왜 이게 문제인가요? 우리 눈에는 화면이 멀쩡해 보여요. 그래서 문제가 없다고 착각하기 쉬워요. 하지만 실제 DOM 구조는 우리가 의도한 것과 달라요. 예를 들어 “링크 전체 영역을 클릭하면 이동한다”고 만들었는데, 브라우저가 DOM을 재구성하면서 버튼 부분은 링크 영역 밖이 되어버릴 수 있어요. JavaScript로 이벤트를 걸어놓은 부분도 엉뚱한 곳에 붙어서 작동이 이상해질 수 있고요.
🌐 브라우저마다 다르게 고쳐요 가장 무서운 건 Chrome, Firefox, Safari가 각자 다른 방식으로 고칠 수 있다는 거예요. 개발할 때는 Chrome에서 확인했는데, 다른 브라우저에서는 전혀 다른 모양이 될 수 있어요. 이런 불예측성을 막으려면 처음부터 올바른 HTML을 작성하는 것이 최선이에요.
중급
HTML5 파서는 오류 허용(Error Tolerance) 설계를 채택하여, 명세 위반 마크업을 파싱 오류로 중단하지 않고 정해진 오류 복구 알고리즘에 따라 DOM을 재구성합니다. Interactive 콘텐츠 중첩은 이 알고리즘의 주요 트리거 중 하나입니다.
a 안에 button 중첩 시 브라우저 처리
파서가 <a> 열린 상태에서 <button> 토큰을 만나면, 현재 열린 <a>를 강제로 닫고 <button>을 <a> 바깥에 배치합니다. 결과적으로 원래 하나였던 링크가 버튼 앞뒤로 분리된 두 개의 링크로 쪼개집니다.
<!-- 작성한 HTML -->
<a href="/page">
앞 텍스트
<button>버튼</button>
뒷 텍스트
</a>
<!-- 브라우저가 실제로 구성하는 DOM (Chrome 기준) -->
<a href="/page">앞 텍스트</a>
<button>버튼</button>
<a href="/page">뒷 텍스트</a>
// 의도: a 요소 클릭 시 이벤트 처리
document.querySelector('a').addEventListener('click', (e) => {
// 버튼이 a 밖으로 이동했으므로 버튼 클릭은 이 핸들러에 도달하지 않음
console.log('링크 클릭됨');
});
// DOM 재구성 후 실제 구조 확인
console.log(document.body.innerHTML);
// 예상: <a><button></button></a>
// 실제: <a></a><button></button><a></a>
브라우저별 재구성 차이 HTML 파서 명세(Section 8.2)는 오류 복구 알고리즘을 상세히 정의하지만, 모든 엣지 케이스를 포괄하지는 않아 구현체마다 미묘한 차이가 발생할 수 있습니다. 특히 중첩이 복잡할수록 브라우저 간 DOM 구조 차이가 커집니다.
심화
HTML 파서의 오류 복구 메커니즘은 WHATWG HTML Living Standard Section 8.2 (Parsing HTML documents)에 상세 정의된 상태 기계(State Machine) 기반 알고리즘으로, Interactive 콘텐츠 중첩 충돌 시 “Adoption Agency Algorithm”과 “Implied End Tag” 생성 규칙이 복합적으로 작동합니다.
Adoption Agency Algorithm의 동작
Adoption Agency Algorithm(Section 8.2.6.4.7)은 포매팅 요소(formatting element)가 블록 요소에 의해 ‘분리’될 때 적용됩니다. a 요소는 포매팅 요소로 분류되므로, a 내부에서 button 같은 Interactive 콘텐츠를 만나면 이 알고리즘이 실행됩니다.
알고리즘의 핵심은 “outer loop”와 “inner loop”의 두 단계로 구성됩니다. Outer loop는 포매팅 요소를 스택에서 찾고, inner loop는 최후 포매팅 요소와 furthest block 사이의 요소들을 재배치합니다. 이 결과로 a 요소가 복제(clone)되어 충돌 요소 전후에 각각 배치되는 DOM 구조가 생성됩니다.
구현체별 준수 현황 주요 브라우저 엔진별 파서 구현 위치:
- Blink (Chrome/Edge):
HTMLDocumentParser,HTMLTreeBuilder클래스 - Gecko (Firefox):
nsHtml5TreeBuilder - WebKit (Safari):
HTMLTreeBuilder
세 엔진 모두 WHATWG 명세의 파서 알고리즘을 준수하지만, 명세가 미정의한 엣지 케이스(3중 이상 Interactive 중첩, tabindex 요소 간 충돌 등)에서 구현체 차이가 발생합니다. 이는 Browserling, BrowserStack 등의 크로스 브라우저 테스트에서 관찰 가능합니다.
DOM 재구성이 JavaScript 이벤트 위임에 미치는 영향
이벤트 위임(Event Delegation) 패턴에서 재구성된 DOM은 버블링 경로를 변경합니다. 원래 a > button으로 버블링되어야 할 이벤트가 재구성 후 button에서 body로 직접 버블링됩니다. React의 합성 이벤트(Synthetic Event) 시스템은 실제 DOM 구조를 기반으로 이벤트 위임을 구현하므로, 재구성된 DOM에서 onClick 핸들러 실행 순서와 stopPropagation 효과가 예상과 달라질 수 있습니다.
접근성과 포커스 관리
입문
Interactive 콘텐츠를 잘못 중첩하면 눈으로 보기에는 멀쩡해 보여도, 키보드만 사용하는 사람이나 화면 낭독 프로그램을 쓰는 시각 장애인에게는 심각한 문제가 생길 수 있어요.
⌨️ 키보드만 쓰는 사람들 컴퓨터를 마우스 없이 키보드의 Tab 키만으로 사용하는 사람들이 있어요. 손이 불편한 분들이나 마우스를 선호하지 않는 개발자들도 많이 사용해요. 이 분들은 Tab 키를 눌러서 웹 페이지의 버튼, 링크, 입력창을 순서대로 이동해요. 그런데 Interactive 요소가 잘못 중첩되면 이 순서가 뒤틀리거나, 어떤 요소는 아예 건너뛰어지기도 해요.
🔊 화면 낭독 프로그램 시각 장애인은 ‘스크린 리더’라는 프로그램을 써서 컴퓨터를 사용해요. 이 프로그램은 화면을 읽어서 목소리로 들려줘요. “링크: 홈으로 이동”, “버튼: 주문하기” 같은 식으로요. 그런데 링크 안에 버튼을 넣으면 스크린 리더가 “이게 링크인지 버튼인지” 혼란스러워져요. 어떤 스크린 리더는 아예 그 요소를 없는 것처럼 건너뛰기도 해요!
🗺️ 포커스 순서의 중요성 우리가 웹 페이지를 만들 때 논리적인 순서로 요소를 배치하잖아요. 키보드 Tab 이동도 이 순서대로 되어야 해요. 그런데 브라우저가 잘못된 중첩 때문에 DOM을 재구성하면, 포커스 이동 순서가 화면에 보이는 순서와 달라질 수 있어요. 사용자가 Tab을 눌렀는데 갑자기 엉뚱한 곳으로 점프하는 느낌이 들어요.
✅ 올바르게 만드는 방법
중첩 없이 같은 시각적 효과를 낼 수 있어요! 버튼처럼 보이는 링크가 필요하면 CSS로 스타일만 버튼처럼 만들면 돼요. 클릭 가능한 카드를 만들 때는 CSS position 속성을 이용해서 링크가 전체 카드를 덮게 만들 수 있어요. 이렇게 하면 화면도 예쁘고 접근성도 지킬 수 있어요!
중급
Interactive 콘텐츠 중첩 금지 규칙의 핵심 이유 중 하나는 접근성(Accessibility)입니다. 보조 기술(Assistive Technology)은 HTML 명세를 따른 올바른 DOM 구조를 전제로 설계되어 있습니다.
포커스 관리 문제 브라우저의 오류 복구로 DOM이 재구성되면, 포커스 순서(Tab Order)가 의도한 시각적 순서와 달라집니다. WCAG 2.1의 Success Criterion 2.4.3 (Focus Order)은 포커스 순서가 의미 있고 맥락에 맞아야 한다고 요구합니다.
스크린 리더 동작 문제
a 안에 button이 있을 경우, 스크린 리더는 역할(role)을 결정할 수 없어 요소를 건너뛰거나 잘못된 역할로 읽을 수 있습니다. NVDA, JAWS, VoiceOver 등 주요 스크린 리더의 동작이 각각 달라져 예측 불가능한 접근성 경험이 발생합니다.
<!-- 잘못된 패턴: a 안에 button (접근성 파괴) -->
<a href="/product/1">
<div class="card">
<img src="product.jpg" alt="상품명" />
<h3>상품명</h3>
<button>장바구니 담기</button> <!-- 명세 위반 + 접근성 문제 -->
</div>
</a>
<!-- 올바른 패턴: 중첩 없이 각 역할 분리 -->
<div class="card">
<a href="/product/1">
<img src="product.jpg" alt="상품명" />
<h3>상품명</h3>
</a>
<button onclick="addToCart(1)">장바구니 담기</button>
</div>
<!-- CSS로 전체 카드 클릭 구현 (의미론적으로 올바름) -->
<div class="card" style="position: relative;">
<img src="product.jpg" alt="상품명" />
<h3>상품명</h3>
<!-- 전체 카드를 덮는 링크 (스크린 리더에 명확) -->
<a href="/product/1" class="card-overlay-link">
<span class="visually-hidden">상품명 상세 보기</span>
</a>
<button class="add-to-cart">장바구니 담기</button>
</div>
WCAG 준수와의 연결 잘못된 Interactive 중첩은 WCAG 2.1의 여러 기준을 동시에 위반할 수 있습니다: 1.3.1 (Info and Relationships), 2.1.1 (Keyboard), 2.4.3 (Focus Order), 4.1.2 (Name, Role, Value). 이는 법적 접근성 요건이 있는 서비스에서 컴플라이언스 리스크로 이어집니다.
심화
Interactive 콘텐츠 중첩 금지 규칙은 접근성 트리(Accessibility Tree)의 구조적 무결성을 보장하는 핵심 제약이며, WAI-ARIA 명세의 역할 모델(Role Model)과 HTML 콘텐츠 모델의 교차점에서 이해되어야 합니다.
접근성 트리와 DOM의 분리
브라우저는 DOM과 별도로 접근성 트리를 유지합니다. 접근성 트리는 플랫폼 접근성 API(Windows: UIA/MSAA, macOS: NSAccessibility, Linux: AT-SPI)를 통해 보조 기술에 노출됩니다. DOM 재구성이 발생하면 접근성 트리도 재구성되지만, 그 결과는 DOM 재구성 결과와 항상 일치하지 않습니다. 특히 a 요소가 분리 복제될 때 aria-label이나 aria-describedby 같은 참조 속성은 복제된 요소에 올바르게 전파되지 않아 접근성 트리에서 레이블 없는 인터랙티브 요소가 생성됩니다.
Focus Management API와의 충돌
HTML 명세의 Sequential Focus Navigation(Section 6.6.3)은 tabindex와 DOM 순서를 기반으로 포커스 순서를 결정합니다. DOM 재구성 후 복제된 a 요소들은 원래 tabindex 값을 상속하지만, 포커스 관리 관점에서는 별개의 포커스 대상으로 처리됩니다. 이로 인해 document.activeElement 추적, focus()/blur() 이벤트 처리, FocusTrap 구현이 예상과 다르게 동작합니다.
ARIA 역할 충돌과 접근성 명세 위반
WAI-ARIA 1.2 명세는 상위/하위 역할 제약(Required Context Role, Required Owned Elements)을 정의합니다. role="button"은 role="link" 내부에 포함될 수 없으며, 이를 위반하면 접근성 검증 도구(axe-core, Lighthouse Accessibility)가 “aria-required-context” 위반으로 보고합니다. 특히 NVDA + Chrome 조합에서는 중첩된 Interactive 요소를 발견하면 가상 커서(Virtual Cursor)의 이동 알고리즘이 해당 요소를 건너뛰는 현상이 관찰됩니다.
WCAG 2.2 컴플라이언스 관점 WCAG 2.2 SC 2.4.11 (Focus Not Obscured) 및 SC 2.4.12 (Focus Not Obscured Enhanced)는 포커스 요소가 완전히 가려지지 않아야 함을 요구합니다. DOM 재구성으로 시각적 위치와 DOM 위치가 어긋난 경우, 포커스 인디케이터가 엉뚱한 위치에 렌더링되어 이 기준을 자동으로 위반할 수 있습니다. EU의 European Accessibility Act(EAA) 2025 시행 이후 접근성 컴플라이언스는 법적 의무가 되었으며, Interactive 콘텐츠 중첩 위반은 자동화된 접근성 감사 도구에서 즉시 검출되는 항목입니다.