WCAG 성공 기준 · Level A

WCAG 4.1.1: 구문 분석 (WCAG 2.2에서 사용 중단됨)

WCAG 4.1.1 구문 분석은 보조 기술이 페이지를 잘못 해석하거나 처리하지 못하게 만들 수 있는 중복 ID와 같은 중대한 HTML/XML 오류가 웹 콘텐츠에 없어야 한다고 요구합니다. WCAG 2.2에서 더 이상 권장되지 않게 되었지만, 기반이 되는 axe-core 규칙은 여전히 활성 상태이며, 위반 사항은 여전히 실제 접근성 위험을 나타냅니다.

이 규칙의 의미

WCAG 4.1.1 Parsing은 원래 브라우저와 보조 기술을 포함한 사용자 에이전트가 웹 콘텐츠를 정확하게 파싱하고 해석할 수 있도록 보장하기 위해 설계되었다. 이 기준은 HTML이나 XML과 같은 마크업 언어로 작성된 페이지가 네 가지 구조적 조건을 충족할 것을 요구했다. 요소는 완전한 시작 태그와 종료 태그를 가져야 하고, 요소는 명세에 따라 중첩되어야 하며, 요소는 중복된 속성을 포함해서는 안 되고, 콘텐츠에서 사용되는 모든 ID는 고유해야 한다.

WCAG 2.2에서 W3C는 이 기준을 공식적으로 폐지(deprecate)했다. 그 근거는 현대 브라우저가 잘못된 HTML에 대해 매우 탄력적으로 변해, 접근성 트리에 도달하기 전에 대부분의 구조적 오류를 자동으로 수정한다는 점이었다. 그 결과, 닫히지 않은 태그나 잘못 중첩된 요소와 같은 원래의 많은 우려 사항들은 더 이상 현대 환경에서 보조 기술에 실질적인 피해를 주지 않게 되었다.

그러나 폐지되었다고 해서 이 기준이 다루던 우려가 완전히 사라졌다는 의미는 아니다. W3C는 중복된 ID 속성이 여전히 의미 있는 접근성 문제로 남아 있다고 명시적으로 언급한다. 두 개 이상의 요소가 동일한 id 값을 공유할 때, 브라우저는 ARIA 참조, 레이블 연결, 또는 프래그먼트 링크를 어떤 요소와 연결할지 임의로 결정해야 한다. 이 모호성 때문에 스크린 리더가 잘못된 콘텐츠를 읽어 주거나, 인터랙티브 컨트롤을 건너뛰거나, 아예 폼 레이블을 노출하지 못할 수 있다. 따라서 오늘날 이 기준의 통과 조건은 DOM에 중복된 ID 값이 나타나지 않는 것으로 이해하는 것이 가장 적절하다. 보조 기술이 의존하는 프로그래밍적 연결을 깨뜨리는 방식으로 ID가 중복될 때 페이지는 이 기준을 실패하게 된다.

WCAG 명세에서의 공식적인 예외는 최소한에 그친다. 이 기준은 마크업 언어로 작성된 콘텐츠에 적용되며, 작성자가 출력 형식을 직접 제어할 수 없는 스크립팅 환경에서 생성된 콘텐츠에는 적용되지 않는다. 그러나 실제로는, 어떤 기술 스택을 사용해 DOM을 생성했는지와 관계없이 개발자는 최종 렌더링된 DOM에 대해 책임을 진다.

왜 중요한가

중복된 ID는 사소한 정리 문제처럼 보일 수 있지만, 보조 기술 사용자에게는 심각한 결과를 초래할 수 있다. JAWS, NVDA, VoiceOver와 같은 스크린 리더는 브라우저의 접근성 트리에 의존하며, 이 트리는 인터페이스 요소 간의 관계를 구축하기 위해 올바르게 해석된 ID 참조에 의존한다. ID가 중복되면 브라우저는 일반적으로 문서 순서에서 첫 번째로 일치하는 요소에만 참조를 연결하고, 동일한 ID를 가진 이후 요소들은 조용히 무시한다.

시각장애 및 저시력 사용자에게 이는 폼 필드가 레이블 없이 읽히거나, aria-describedby를 통해 입력 요소와 연결된 오류 메시지가 전혀 읽히지 않는다는 의미가 될 수 있다. 예를 들어, 한 전자상거래 사이트의 결제 폼에서 배송지 주소 필드와 청구지 주소 필드가 모두 city, zip, state와 같은 ID를 사용한다고 가정해 보자. 스크린 리더 사용자가 청구지 섹션을 작성할 때 배송지 섹션의 레이블을 대신 듣게 되어 혼란, 오류, 그리고 거래 포기로 이어질 수 있다.

인지 장애가 있는 사용자에게는 레이블 연결이 깨지면 화면에서 눈으로 읽는 가시 텍스트와 스크린 리더나 음성 제어 소프트웨어가 읽어 주는 내용이 일치하지 않게 되어, 인지 부하를 증가시키는 혼란스러운 단절이 발생한다.

Dragon NaturallySpeaking과 같은 음성 입력 소프트웨어에 의존하는 운동 장애가 있는 사용자의 경우, 중복된 ID 때문에 특정 컨트롤을 대상으로 하는 음성 명령이 잘못된 요소를 활성화할 수 있다. 이는 소프트웨어가 내부적으로 ID 기반 타기팅에 의존할 수 있기 때문이다.

장애 영향 외에도, 중복된 ID는 SEO에도 영향을 미친다. 특정 페이지 섹션을 인덱싱하기 위해 프래그먼트 식별자에 의존하는 검색 엔진 크롤러는 ID가 고유하지 않을 때 잘못된 콘텐츠를 인덱싱할 수 있다. 또한 페이지 내 앵커 링크가 페이지의 잘못된 위치로 이동하게 되어 모든 사용자에게 사용성이 저하된다.

세계보건기구에 따르면 전 세계적으로 약 22억 명이 어떤 형태로든 시각 장애를 가지고 있다. 이들 중 상당수는 ID 연결이 깨졌을 때 직접적인 영향을 받는 스크린 리더에 의존한다. 고유한 ID를 보장하는 것은 개발 팀이 구현할 수 있는 가장 적은 노력으로 가장 큰 효과를 얻을 수 있는 수정 사항 중 하나다.

관련 Axe-core 규칙

세 가지 axe-core 규칙이 WCAG 4.1.1이 제기한 우려와 직접적으로 매핑된다. 각 규칙은 중복 ID 문제의 특정 양상을 대상으로 한다.

  • duplicate-id: 이 규칙은 DOM 전체를 검사하여 두 개 이상의 요소에 나타나는 모든 id 속성 값을 찾는다. ARIA로 참조되었는지, 인터랙티브한 요소인지와 관계없이, 동일한 ID를 공유하는 첫 번째 요소 이후의 모든 요소를 플래그한다. 이는 세 규칙 중 가장 범위가 넓은 규칙으로, 명시적인 ARIA 관계가 없더라도 구조적 위반을 포착한다. 일반적인 트리거는 재사용 가능한 컴포넌트를 페이지에서 여러 번 렌더링하면서 각 인스턴스에 대해 고유한 ID를 생성하지 않는 컴포넌트 기반 프레임워크다.
  • duplicate-id-active: 이 규칙은 인터랙티브하거나 포커스를 받을 수 있는 요소—버튼, 링크, 입력 요소, 그리고 0 이상 tabindex를 가진 모든 요소—에 있는 중복 ID로 범위를 좁힌다. 여기서는 보조 기술과 키보드 내비게이션이 모두 활성 컨트롤을 명확하게 식별할 수 있는 능력에 의존하기 때문에 접근성 영향이 더 크다. 제출 버튼과 관련 없는 아이콘이 동일한 ID를 공유할 경우, 탭 순서 안내와 프로그래밍적 포커스 관리가 모두 깨질 수 있다.
  • duplicate-id-aria: 이는 세 규칙 중 가장 중요하다. 이 규칙은 해당 ID가 aria-labelledby, aria-describedby, aria-controls, aria-owns와 같은 ARIA 속성—관계 속성—에 의해 참조될 때 중복 ID를 플래그한다. 이러한 속성은 보조 기술이 요소 간 관계를 이해하는 주요 메커니즘이기 때문에, 이 영역에서의 중복은 접근 가능한 이름 계산과 역할 관계를 직접적으로 깨뜨린다. 실패 사례의 예로는 모달이 aria-labelledby='dialog-title'을 사용할 때, id='dialog-title'을 가진 두 개의 <div> 요소가 있는 경우를 들 수 있다. 스크린 리더는 DOM에서 먼저 나타나는 요소를 읽게 되는데, 이는 의도한 대화 상자 제목이 아닐 수 있다.

자동화 도구는 중복 ID를 잡는 데 매우 적합한데, 이 검사는 순전히 구문적이기 때문이다. 도구는 DOM을 읽고 ID 값을 비교하기만 하면 된다. 이 기준을 위해 엄밀히 요구되는 수동 테스트는 없다. 그러나 ID가 사용자 상호작용 이후 동적으로 생성되는 경우—예를 들어, 새로운 콘텐츠를 주입하면서 반복된 ID를 생성하는 인피니트 스크롤—페이지 로드 시점에 실행되는 자동화 스캔은 나중에야 나타나는 위반을 놓칠 수 있다. 이런 경우, 테스터는 스캔을 실행하기 전에 동적 동작을 트리거하거나, 상호작용 이후 브라우저 개발자 도구를 사용해 DOM을 모니터링해야 한다.

테스트 방법

  1. axe DevTools를 사용한 자동 스캔: Chrome 또는 Firefox에서 페이지를 연다. DevTools(F12)를 열고 axe DevTools 패널로 이동(또는 브라우저 확장 프로그램 설치)한 뒤 전체 페이지 스캔을 실행한다. 결과를 duplicate-id, duplicate-id-active, duplicate-id-aria 규칙으로 필터링한다. 각 위반 항목에는 영향을 받는 요소와 중복된 ID 값이 나열된다. 필요하다면 감사 문서화를 위해 보고서를 내보낸다. Lighthouse의 경우, DevTools의 Lighthouse 탭에서 Lighthouse 접근성 감사를 실행하고 "Document has multiple elements with the same id" 감사를 확인한다.
  2. 브라우저 DevTools 콘솔 검사: 브라우저 콘솔을 열고 현재 페이지의 모든 중복 ID를 찾기 위해 다음 JavaScript 스니펫을 실행한다. const ids = [...document.querySelectorAll('[id]')].map(el => el.id); const dupes = ids.filter((id, i) => ids.indexOf(id) !== i); console.log([...new Set(dupes)]); 이 코드는 두 번 이상 나타나는 모든 ID 값을 배열로 출력한다. 빈 배열이면 중복이 없다는 의미다.
  3. NVDA와 Firefox를 사용한 스크린 리더 테스트: NVDA를 실행한 상태에서 페이지를 로드한다. for/id 또는 aria-labelledby를 통해 레이블이 연결된 필드가 있는 폼으로 이동한다. 각 필드를 탭으로 이동하며 NVDA가 올바른 레이블을 읽는지 주의 깊게 듣는다. 필드가 레이블 없이 읽히거나, 다른 섹션의 잘못된 레이블과 함께 읽힌다면, 그 원인이 중복된 ID일 수 있다. aria-controlsaria-describedby를 사용하는 ARIA 랜드마크 영역, 모달 대화 상자, 위젯에 대해서도 이 과정을 반복한다.
  4. macOS의 VoiceOver와 Safari: VoiceOver(Command+F5)를 활성화한다. VoiceOver 로터(Control+Option+U)를 사용해 폼 컨트롤 또는 링크 목록을 열고 각 컨트롤이 고유하고 올바르게 읽히는 레이블을 가지고 있는지 확인한다. 모달 대화 상자로 이동해, 대화 상자가 열릴 때 제목이 올바르게 읽히는지도 확인한다.
  5. JAWS와 Chrome: JAWS를 실행한 상태에서 페이지를 열고, JAWS의 폼 필드 목록(Insert+F5)을 사용해 모든 폼 요소와 연결된 레이블을 검토한다. 서로 다른 필드가 서로 구분되어야 하는데도 동일한 레이블 텍스트를 공유하지 않는지 확인한다.
  6. 동적 콘텐츠 테스트: 페이지가 인피니트 스크롤, 단일 페이지 내비게이션, JavaScript로 주입되는 모달 대화 상자를 사용하는 경우, 이러한 기능과 상호작용해 DOM에 새로운 콘텐츠를 로드한 다음, 동적 콘텐츠로 인해 도입된 중복을 확인하기 위해 자동 스캔이나 콘솔 스니펫을 다시 실행한다.

수정 방법

반복 섹션 간 중복된 폼 필드 ID — 잘못된 예

<!-- Shipping Address -->
<label for='city'>City</label>
<input type='text' id='city' name='shipping-city'>

<!-- Billing Address -->
<label for='city'>City</label>
<input type='text' id='city' name='billing-city'>
<!-- FAIL: Both inputs share id='city'. The second label's 'for' attribute
     resolves to the first input, so screen readers announce the wrong field. -->

반복 섹션 간 중복된 폼 필드 ID — 올바른 예

<!-- Shipping Address -->
<label for='shipping-city'>City</label>
<input type='text' id='shipping-city' name='shipping-city'>

<!-- Billing Address -->
<label for='billing-city'>City</label>
<input type='text' id='billing-city' name='billing-city'>
<!-- PASS: Each input has a unique ID scoped to its section.
     Screen readers correctly announce each field's label. -->

여러 번 렌더링되는 재사용 컴포넌트 — 잘못된 예

<!-- Product Card 1 -->
<div class='product-card'>
  <img id='product-img' src='shoe.jpg' alt='Running Shoe'>
  <button id='add-to-cart' aria-describedby='product-desc'>Add to Cart</button>
  <p id='product-desc'>Free shipping on orders over 500 TL.</p>
</div>

<!-- Product Card 2 (same template, duplicate IDs) -->
<div class='product-card'>
  <img id='product-img' src='boot.jpg' alt='Hiking Boot'>
  <button id='add-to-cart' aria-describedby='product-desc'>Add to Cart</button>
  <p id='product-desc'>Free shipping on orders over 500 TL.</p>
</div>
<!-- FAIL: IDs duplicated across cards. aria-describedby on the second button
     resolves to the <p> in the first card, not the second. -->

여러 번 렌더링되는 재사용 컴포넌트 — 올바른 예

<!-- Product Card 1 -->
<div class='product-card'>
  <img id='product-img-1' src='shoe.jpg' alt='Running Shoe'>
  <button id='add-to-cart-1' aria-describedby='product-desc-1'>Add to Cart</button>
  <p id='product-desc-1'>Free shipping on orders over 500 TL.</p>
</div>

<!-- Product Card 2 -->
<div class='product-card'>
  <img id='product-img-2' src='boot.jpg' alt='Hiking Boot'>
  <button id='add-to-cart-2' aria-describedby='product-desc-2'>Add to Cart</button>
  <p id='product-desc-2'>Free shipping on orders over 500 TL.</p>
</div>
<!-- PASS: Each card's IDs are unique. ARIA references resolve correctly
     within their own card. Use a counter, UUID, or slug-based strategy
     to generate IDs in your component framework. -->

중복된 ARIA 레이블 참조를 가진 모달 대화 상자 — 잘못된 예

<!-- A generic heading used as a reusable ID -->
<h1 id='dialog-title'>Welcome</h1>

<div role='dialog' aria-modal='true' aria-labelledby='dialog-title'>
  <h2 id='dialog-title'>Confirm Your Order</h2>
  <p>Are you sure you want to place this order?</p>
  <button>Confirm</button>
  <button>Cancel</button>
</div>
<!-- FAIL: Two elements share id='dialog-title'. The dialog's
     aria-labelledby resolves to the page <h1>, not the dialog heading.
     Screen readers will announce 'Welcome' as the dialog name. -->

중복된 ARIA 레이블 참조를 가진 모달 대화 상자 — 올바른 예

<h1>Welcome</h1>

<div role='dialog' aria-modal='true' aria-labelledby='confirm-dialog-title'>
  <h2 id='confirm-dialog-title'>Confirm Your Order</h2>
  <p>Are you sure you want to place this order?</p>
  <button>Confirm</button>
  <button>Cancel</button>
</div>
<!-- PASS: The dialog heading has a unique, descriptive ID.
     aria-labelledby correctly identifies the dialog to screen readers
     as 'Confirm Your Order'. -->

자주 발생하는 실수

  • ID를 업데이트하지 않고 컴포넌트 마크업을 복사-붙여넣기: 개발자는 두 번째 인스턴스(두 번째 주소 블록, 두 번째 탭 패널, 두 번째 아코디언 아이템)를 위해 잘 동작하는 HTML 섹션을 복제하면서 모든 ID 값을 고유하게 업데이트하는 것을 잊는 경우가 많다. component-name-index(예: accordion-panel-1, accordion-panel-2)와 같은 네이밍 규칙을 정하고 코드 리뷰에서 이를 강제하라.
  • 고유 키 전략 없이 프레임워크 컴포넌트에서 정적 ID 사용: React, Vue, Angular와 같은 프레임워크는 동일한 컴포넌트를 한 페이지에서 수십 번 렌더링할 수 있다. 재사용 컴포넌트 내부에서 하드코딩된 id='search-input'을 사용하면 인스턴스 수만큼 중복이 생성된다. 항상 props, 카운터, 또는 React 18+의 useId()와 같은 유틸리티에서 ID를 파생하라.
  • HTML을 수정하는 대신 CSS 클래스 타기팅에 의존: 일부 개발자는 중복 ID 문제를 해결하기 위해 JavaScript 셀렉터를 getElementById에서 클래스 기반 querySelector로 바꾸면서, 중복된 ID는 그대로 두기도 한다. 이는 시각적 동작은 고칠 수 있지만, 접근성 트리의 연결이 깨진 문제는 전혀 해결하지 못한다.
  • 서버 사이드 템플릿 루프가 매 반복마다 동일한 ID를 생성: {% for item in items %} 루프 안에서 id='item-title'을 렌더링하는 Jinja2, Blade, Twig 템플릿은 목록의 항목마다 하나의 중복을 생성한다. 항상 루프 인덱스나 항목 식별자를 ID에 덧붙여라. 예: id='item-title-{{ loop.index }}'.
  • 숨김 또는 화면 밖 요소의 중복 ID를 무시: display: none이나 visibility: hidden이 적용된 요소도 여전히 DOM에 존재하며, 그 ID 역시 등록된다. 보이는 요소와 ID를 공유하는 숨겨진 모달 템플릿은 동일한 파싱 실패를 일으킨다. hidden 속성을 사용하거나, 숨겨진 템플릿이 고유한 ID를 사용하도록 하라.
  • Shadow DOM으로 스코프가 분리되었다고 가정: 네이티브 Shadow DOM 내부의 ID는 스코프가 분리되어 라이트 DOM이나 다른 shadow root의 ID와 충돌하지 않는다. 그러나 많은 컴포넌트 라이브러리는 진정한 스코프 분리를 제공하지 않는 폴리필이나 비표준 방식을 사용한다. 프레임워크 동작을 가정하지 말고 실제 DOM 출력 결과를 확인하라.
  • 사용자 제공 콘텐츠를 기반으로 ID를 생성하면서 정규화나 중복 제거를 하지 않음: 상품명, 기사 제목, 기타 동적 텍스트에서 ID를 생성하면 두 항목이 동일한 이름을 가질 때 충돌이 발생할 수 있다(예: 두 상품이 모두 "Classic"이라는 이름을 가져 둘 다 id='classic'을 생성하는 경우). 항상 고유한 데이터베이스 키나 인덱스를 콘텐츠 기반 ID에 덧붙여라.
  • 단일 페이지 애플리케이션에서 클라이언트 사이드 내비게이션 이후 테스트를 하지 않음: 전체 페이지 리로드 없이 새로운 라우트 콘텐츠를 DOM에 주입하는 SPA는 이전에 방문한 라우트의 콘텐츠가 제대로 언마운트되지 않으면 ID를 누적시킬 수 있다. 초기 로드뿐 아니라 라우트 간 이동 후에도 axe 스캔을 실행하라.
  • SVG <defs><use> 요소에서 생성된 ID를 잊어버림: <defs> 내부에서 ID를 가진 심볼을 정의하고 <use href='#icon-arrow'>로 참조하는 SVG 스프라이트 패턴은 동일한 심볼 정의가 페이지에 여러 번 포함되면 중복 ID를 생성할 수 있다. SVG 스프라이트 정의를 중앙집중화하고 한 번만 포함하라.
  • 서드파티 위젯, 채팅 플러그인, 분석 스크립트가 생성하는 ID를 간과: 서드파티 스크립트는 때때로 하드코딩된 ID를 가진 요소를 주입한다. 자체 코드가 동일한 ID를 사용할 경우, 개발 중에는 눈치채지 못할 수 있는 충돌이 발생한다. 서드파티 콘텐츠를 포함한 전체 렌더링 DOM을 감사하고, 충돌을 벤더에 보고하거나 자체 ID에 네임스페이스를 적용해 충돌을 피하라.

터키 접근성 규정과의 관계

터키의 대통령령 2025/10은 2025년 6월 21일자, 관보 번호 32933에 게재되었으며, 터키에서 운영되는 광범위한 공공 및 민간 기관에 대해 의무적인 웹 접근성 요구 사항을 수립한다. 이 대통령령은 WCAG 2.2를 기술적 참조 표준으로 채택하며, 모든 적용 대상 기관에 대해 최소 법적 기준을 Level A 준수로 설정한다.

WCAG 4.1.1 Parsing은 Level A 기준이다. W3C가 WCAG 2.2에서 이를 폐지했음에도 불구하고, 고유 ID라는 이 기준의 주요 우려를 집행하는 axe-core 규칙은 여전히 활성 상태이며 WCAG 2.2를 기준으로 수행되는 접근성 감사에서 계속 플래그된다. 자동화 스캐닝 도구를 사용하는 터키의 규제 감사와 준수 검토는, 명세 수준에서 이 기준이 폐지되었는지 여부와 관계없이 duplicate-id 위반을 잠재적인 Level A 실패로 표시할 것이다. 준수를 입증하려는 조직은 중복 ID 위반을 차단 이슈로 취급해야 한다.

대통령령 2025/10의 적용 대상에는 광범위한 공공 기관과 민간 부문 조직이 포함된다. 모든 중앙 및 지방 정부 기관과 그 산하기관, 터키 은행법에 따라 규제되는 은행 및 금융 기관, 병원과 민간 의료 제공자, 200,000명 이상의 가입자를 보유한 통신 사업자, 전자상거래 플랫폼과 온라인 마켓플레이스, 여행사와 투어 운영사, 공공 컨세션 하에 운영되는 민간 운송 회사, 그리고 교육부(MoNE)의 인가를 받은 민간 학교와 교육 기관이 이에 해당한다.

이 대통령령은 단계적 준수 일정을 수립한다. 공공 기관은 대통령령 공포일로부터 1년 이내에 Level A 완전 준수를 달성해야 한다. 해당 범주의 민간 부문 기관은 동일한 기준에 도달하기 위해 2년의 기간을 가진다. 준수하지 않을 경우, 적용 대상 기관은 규제 당국의 심사, 잠재적인 행정 제재, 그리고 접근성 인식이 높아지는 시장에서의 평판 리스크에 노출된다.

터키 조직의 경우, 중복 ID 위반을 해결하는 것은 특히 디지털 폼, 온라인 결제 흐름, 정부 서비스 포털, 의료 예약 시스템과 같이 반복된 폼 섹션, 재사용 컴포넌트, 서드파티 위젯 통합을 많이 사용하는 인터페이스에서 중요하다. 이러한 요소들은 중복 ID를 도입하기 가장 쉬운 유형이다. 개발 프로세스의 일부로 axe-core를 CI/CD 파이프라인에 통합하는 등 자동화된 접근성 테스트를 구축하는 것은 기술적 모범 사례이자, 대통령령 요구 사항 하에서 지속적인 규제 준수를 유지하기 위한 실용적인 전략이다.