도입 스토리
"마우스를 뺄게요." 박멘토가 마우스를 서랍 속에 넣었습니다.
"Tab 키만으로 회원가입까지 완료해봐요."
김개발이 Tab을 눌렀습니다. 포커스가 메뉴로 이동했습니다. 다시 Tab. 포커스가... 사라졌습니다. 어디에 있는지 전혀 보이지 않았습니다.
"포커스가 어디 있는지 안 보여요."
"맞아요. outline: none 처리를 해놨거든요." 박멘토가 말했습니다. "키보드 사용자는 지금 이 페이지를 쓸 수 없어요. 운동 장애가 있어서 마우스를 못 쓰는 분들, 손이 부상 중인 분들, 파워 유저들까지 전부요."
핵심 개념 설명
키보드 접근성의 기본
WCAG 성공 기준 2.1.1은 마우스로 할 수 있는 모든 기능은 키보드로도 가능해야 한다고 명시합니다. 단, 드로잉처럼 경로 자체가 중요한 경우는 예외입니다.
기본 키보드 탐색 키:
- Tab: 포커스를 다음 인터랙티브 요소로 이동
- Shift+Tab: 이전 요소로 이동
- Enter / Space: 활성화 (버튼, 링크, 체크박스 등)
- 방향키: 라디오 버튼, 메뉴, 슬라이더 등 그룹 내 이동
- Escape: 모달, 드롭다운 닫기
포커스 가시성 — WCAG 2.4.7
포커스가 어디 있는지 반드시 보여야 합니다.
/* 절대 하지 말 것 */* { outline: none; /* 키보드 사용자의 시야를 빼앗음 */}/* 올바른 포커스 스타일 */:focus-visible { outline: 3px solid #005FCC; outline-offset: 2px; border-radius: 2px;}
:focus-visible은 키보드 탐색 시에만 포커스 링을 표시하고, 마우스 클릭 시에는 숨깁니다. 디자인과 접근성을 동시에 만족합니다.
포커스 순서 — WCAG 2.4.3
Tab 이동 순서가 논리적이어야 합니다. 일반적으로 DOM 순서를 따릅니다.
<!-- 잘못된 예: tabindex가 시각적 순서를 망가뜨림 --><button tabindex="3">세 번째</button><button tabindex="1">첫 번째</button><button tabindex="2">두 번째</button><!-- 올바른 예: DOM 순서가 곧 Tab 순서 --><button>첫 번째</button><button>두 번째</button><button>세 번째</button>
tabindex="0"은 기본 Tab 순서에 요소를 포함시킵니다. tabindex="-1"은 Tab으로 접근은 안 되지만 JavaScript로 포커스를 보낼 수 있습니다.
// tabindex="-1" 요소에 프로그래밍 방식으로 포커스 보내기document.getElementById('modal-title').focus();
포커스 함정 — WCAG 2.1.2
모달 창 안에서 Tab을 누르면 모달 안에서만 이동해야 합니다. 배경 페이지로 빠져나가면 안 됩니다. 이를 포커스 트랩(Focus Trap)이라고 합니다.
function trapFocus(element) { const focusableElements = element.querySelectorAll( 'a[href], button:not([disabled]), input, select, textarea, [tabindex]:not([tabindex="-1"])' ); const firstElement = focusableElements[0]; const lastElement = focusableElements[focusableElements.length - 1]; element.addEventListener('keydown', (e) => { if (e.key !== 'Tab') return; if (e.shiftKey) { // Shift+Tab: 첫 번째 요소에서 마지막으로 if (document.activeElement === firstElement) { e.preventDefault(); lastElement.focus(); } } else { // Tab: 마지막 요소에서 첫 번째로 if (document.activeElement === lastElement) { e.preventDefault(); firstElement.focus(); } } });}
커스텀 컴포넌트의 키보드 지원
<div>, <span>으로 만든 버튼에는 기본 키보드 동작이 없습니다.
<!-- 잘못된 예: div 버튼은 키보드 접근 불가 --><div class="btn" onclick="submit()">제출</div><!-- 방법 1: 시맨틱 요소 사용 (권장) --><button type="button" onclick="submit()">제출</button><!-- 방법 2: role과 tabindex로 보완 --><div role="button" tabindex="0" onclick="submit()" onkeydown="if(event.key==='Enter'||event.key===' ')submit()"> 제출</div>
가능하면 <button>, <a>, <input> 같은 네이티브 요소를 사용하는 것이 훨씬 간단하고 안전합니다.
단계별 실습
따라하기: 키보드만으로 사이트 탐색하기
마우스를 치우고 Tab 키만으로 웹사이트를 탐색합니다.
체크리스트:
- 포커스 위치가 항상 보이는가.
- 모든 링크와 버튼에 Tab으로 접근 가능한가.
- Tab 순서가 왼쪽에서 오른쪽, 위에서 아래로 논리적인가.
- 모달을 열었을 때 포커스가 모달 안으로 이동하는가.
- Escape로 모달을 닫을 수 있는가.
- 모달을 닫으면 포커스가 열었던 버튼으로 돌아오는가.
변형하기: 포커스 스타일 개선하기
현재 프로젝트의 포커스 스타일을 찾아 개선합니다.
/* Chrome DevTools에서 포커스 강제 표시 *//* Elements 탭 → :hov → :focus 체크 *//* 브랜드 색상을 활용한 포커스 스타일 */:focus-visible { outline: 3px solid var(--color-primary, #005FCC); outline-offset: 3px; border-radius: 4px;}/* 어두운 배경 위 포커스 */.dark-background :focus-visible { outline-color: #ffffff;}
응용하기: axe로 키보드 접근성 검사
axe DevTools에서 다음 규칙을 확인합니다.
scrollable-region-focusable: 스크롤 영역 포커스 가능 여부focus-order-semantics: 포커스 순서 의미론적 정확성tabindex: tabindex 과다 사용 여부
정리와 확인
핵심 내용 요약
- WCAG 2.1.1: 모든 기능은 키보드로 사용 가능해야 함
:focus-visible: 키보드 탐색 시 포커스 링 표시, 마우스 클릭 시 숨김- DOM 순서 = Tab 순서:
tabindex값 남용 금지 - 포커스 트랩: 모달 안에서 Tab이 순환해야 함
- 네이티브 요소 우선:
<div>버튼보다<button>사용
확인 문제
문제 1. 모달을 열었을 때 포커스를 모달 안으로 이동시키는 코드는?
// 모달의 첫 번째 포커스 가능 요소로 포커스 이동document.querySelector('#modal button, #modal [href], #modal input').focus();
문제 2. tabindex="2"를 사용하면 왜 문제가 될 수 있는가?
양수 tabindex는 Tab 순서를 강제 변경하여
DOM 순서와 시각적 순서가 불일치할 때 예측 불가능한 탐색 흐름을 만듭니다.
유지보수 중 다른 요소를 추가하면 순서가 더 복잡해집니다.
다음 챕터에서는 시간 제한과 자동 재생 콘텐츠의 접근성을 다룹니다. 카운트다운 타이머, 자동 슬라이더 — 서두르지 않아도 되는 웹을 만드는 방법을 알아봅니다.