A11y Patterns

DOM 구조 권장사항

올바른 DOM 구조는 스크린 리더 탐색 효율, 포커스 순서, 접근성 트리 품질에 직접 영향을 준다.

시맨틱 마크업

<header>

header / footer / main

header, footer, main 같은 랜드마크 요소를 쓰면 스크린 리더 사용자가 단축키로 섹션 간을 빠르게 이동할 수 있다.

<nav>

nav

내비게이션 링크 묶음에 사용한다. 같은 페이지에 여러 개가 있다면 aria-label로 각각을 구분해야 한다.

<article>

article / section

독립적으로 의미가 통하는 콘텐츠에는 article을, 주제별 묶음에는 section을 쓴다. div보다 훨씬 많은 맥락을 전달한다.

<button>

button vs div

<div onclick>으로 버튼을 흉내 내려면 키보드 접근, role 지정, 상태 관리를 모두 직접 구현해야 한다. <button>을 쓰면 이 모든 게 기본으로 제공된다.

<ul>/<ol>

ul / ol / li

목록을 리스트 요소로 만들면 스크린 리더가 전체 항목 수와 현재 몇 번째인지를 자동으로 알려준다.

제목 계층 구조

h1 → h2 → h3 순서 유지

제목 레벨을 건너뛰면 스크린 리더 사용자가 페이지 구조를 잘못 파악할 수 있다. h1 바로 다음에 h3를 쓰는 것은 허용되지 않는다.

페이지당 h1 하나

페이지 전체의 주제를 나타내는 h1은 하나만 둔다. 이후 섹션에는 h2를, 그 안의 소제목에는 h3를 쓴다.

제목 건너뛰기 금지

스크린 리더 사용자는 헤딩 목록을 훑어보며 원하는 섹션으로 바로 이동한다. 레벨이 건너뛰어지면 목차가 엉킨 것처럼 느껴진다. 글자 크기를 바꾸고 싶다면 제목 레벨이 아닌 CSS로 조절하면 된다.

중첩 깊이

권장 중첩 깊이 ≤ 7~8단계

중첩이 너무 깊어지면 접근성 트리가 복잡해지고, 스크린 리더 사용자가 현재 어느 맥락에 있는지 파악하기 어려워진다.

레이아웃 전용 div 최소화

CSS Grid나 Flexbox로 해결할 수 있는 레이아웃이라면 래퍼 div를 따로 추가하지 않아도 된다.

컴포넌트 추상화 중첩 주의

컴포넌트를 여러 겹 쌓다 보면 실제 DOM에 불필요한 div가 쌓인다. 래퍼가 필요 없다면 Fragment를 활용하자.

DOM 크기

1500

Lighthouse 권장 ≤ 1,500 노드

DOM 노드가 많아질수록 접근성 트리를 만드는 비용도 커지고, 스크린 리더가 페이지를 처음 읽는 데 걸리는 시간도 늘어난다.

트리

접근성 트리 = DOM의 서브셋

aria-hidden이나 display:none으로 숨긴 요소는 접근성 트리에서 제외된다. 반대로 숨기지 않은 요소는 모두 트리에 포함된다는 뜻이기도 하다.

가상화

긴 목록은 가상화 고려

아이템이 수백 개 이상인 목록은 react-virtual 같은 라이브러리로 가상화하면 DOM 크기와 접근성 트리를 동시에 줄일 수 있다.

포커스 순서

DOM 순서 = 포커스 순서

Tab 키 포커스는 화면에 보이는 순서가 아니라 HTML에 작성된 DOM 순서를 따른다. CSS로 시각적 배치를 바꿔도 포커스 순서는 변하지 않기 때문에, 시각 순서와 탭 순서가 어긋나면 키보드 사용자가 혼란을 겪는다.

tabindex 양수 사용 금지

tabindex="0"은 자연스러운 탭 순서에 포함시키고, tabindex="-1"은 JavaScript로만 포커스가 가능하게 한다. 양수 값을 쓰면 해당 숫자 기준으로 페이지 전체 탭 순서가 재편되어, 포커스가 엉뚱한 요소로 튀어버린다. 컴포넌트가 하나라도 추가되면 전체 숫자를 다시 맞춰야 해서 사실상 유지보수가 불가능하다.

모달 내 포커스 트랩 필수

모달이 열린 동안에는 포커스가 모달 내부에서만 순환해야 한다. 모달을 닫으면 포커스는 모달을 열었던 버튼으로 돌아와야 한다.

용어 참고

프로그래매틱 포커스

JavaScript에서 .focus()를 직접 호출해 특정 요소로 포커스를 옮기는 것을 말한다. 사용자 조작 없이 코드가 포커스를 제어한다. 모달을 열거나 닫을 때, 폼 오류 발생 시 해당 필드로 이동할 때 주로 쓰인다.

접근성 트리 (Accessibility Tree)

브라우저가 DOM을 바탕으로 만드는 별도의 트리 구조다. 스크린 리더 같은 보조 기술은 DOM이 아닌 이 트리를 읽는다. role, aria-* 속성, 텍스트 내용이 담기며, aria-hidden이나 display:none으로 숨긴 요소는 여기서 제외된다.

포커스 트랩 (Focus Trap)

Tab 키가 특정 영역, 주로 모달 안에서만 순환하도록 막는 패턴이다. 모달이 열린 동안 배경 콘텐츠로 포커스가 넘어가지 않게 한다. @radix-ui나 react-focus-lock 같은 라이브러리가 이 기능을 제공한다.

랜드마크 (Landmark)

header, nav, main, aside, footer처럼 페이지의 구조를 나타내는 시맨틱 HTML 요소나 ARIA role을 가리킨다. 스크린 리더 사용자는 단축키로 랜드마크 사이를 점프 탐색할 수 있어, 긴 페이지에서도 원하는 영역으로 빠르게 이동할 수 있다.

가상화 (Virtualization)

화면에 보이는 항목만 실제 DOM에 렌더링하고 나머지는 메모리에서 제거하는 기법이다. 1,000개 목록이 있어도 화면에 보이는 20개만 DOM에 존재한다. DOM 크기와 접근성 트리를 줄여 성능과 보조 기술의 응답 속도를 모두 개선할 수 있다.