키보드만으로 쓸 수 있는 사이트 만들기
마우스를 쓰지 못하는 사람은 생각보다 많습니다. 손 떨림·반복성 긴장 손상으로 정밀한 포인팅이 어려운 사용자, 화면낭독기 사용자(대부분 키보드로 조작), 보조 스위치 사용자, 그리고 그냥 키보드가 빠른 개발자까지. 키보드 접근성은 "모든 기능을 Tab·Enter·Space·방향키로 쓸 수 있는가" 한 문장으로 요약됩니다. 그리고 대부분의 문제는 "포커스"를 다루는 방식에서 나옵니다.
1. 기본기 — 시맨틱 요소를 쓰면 절반은 끝난다
<a>, <button>, <input>, <select>,
<textarea>는 브라우저가 알아서 포커스를 받고, Enter·Space 동작을 제공합니다.
문제는 이걸 <div>로 흉내 낼 때 시작됩니다.
<!-- 키보드로 도달 불가, Enter로 실행 불가 -->
<div class="btn" onclick="submit()">보내기</div>
<!-- 이렇게 -->
<button type="button" onclick="submit()">보내기</button>
꼭 div여야 한다면 tabindex="0", role="button",
Enter/Space 키 핸들러를 직접 다 붙여야 합니다.
이 세 가지를 손으로 관리하느니 <button>을 쓰고 CSS로 모양을 잡는 편이 훨씬 안전합니다.
2. 포커스 표시를 지우지 마세요
가장 흔하고, 가장 치명적인 실수입니다. 아래 한 줄이 전역 CSS에 있으면 키보드 사용자는 자기가 지금 화면 어디에 있는지 전혀 알 수 없습니다.
/* 이러지 마세요 */
*:focus { outline: none; }
기본 아웃라인의 모양이 마음에 안 든다면 지우지 말고 바꾸세요.
:focus-visible를 쓰면 마우스 클릭 때는 표시가 안 뜨고 키보드 이동 때만 뜹니다.
:focus-visible {
outline: 2px solid #1a73e8;
outline-offset: 2px;
border-radius: 3px;
}
/* 마우스 클릭 시의 잔상만 정리 (표시는 위에서 이미 담당) */
:focus:not(:focus-visible) { outline: none; }
표시는 대비 3:1 이상이어야 하고(WCAG 1.4.11), 요소를 최소 2px 두께로 감싸는 것이 좋습니다. 배경색만 살짝 바꾸는 방식은 색약 사용자에게 안 보일 수 있습니다.
3. 탭 순서는 화면 순서와 같아야 한다
Tab을 눌렀을 때 포커스가 이동하는 순서는 DOM 순서를 따릅니다.
CSS order, flex-direction: row-reverse, position 등으로
보이는 순서만 바꾸면 탭 순서와 어긋나 사용자가 혼란스러워집니다(WCAG 2.4.3).
- 시각적 순서를 바꿔야 한다면 DOM 자체를 그 순서로 작성하세요.
tabindex에 양수를 쓰지 마세요.tabindex="1"하나가 페이지 전체의 탭 순서를 망칩니다.- 쓸 일이 있는 값은 두 개뿐입니다:
tabindex="0"(자연스러운 순서에 포함),tabindex="-1"(스크립트로만.focus()가능, 탭으로는 도달 안 됨).
4. 스킵 링크 — 반복 영역 건너뛰기
페이지마다 상단에 헤더·GNB가 20~30개 링크로 깔려 있으면, 키보드 사용자는 본문에 닿기 위해 매번 그걸 다 지나야 합니다. 페이지 첫 요소로 "본문 바로가기" 링크를 두면 해결됩니다(WCAG 2.4.1).
<body>
<a href="#main" class="skip-link">본문 바로가기</a>
<header>...</header>
<main id="main" tabindex="-1">...</main>
<style>
.skip-link {
position: absolute; left: 8px; top: -50px;
background: #fff; color: #1a1a1a; padding: 10px 14px; border-radius: 6px;
transition: top .15s;
}
.skip-link:focus { top: 8px; } /* 포커스 받을 때만 화면에 등장 */
</style>
display: none으로 숨기면 포커스를 못 받으니, 화면 밖으로 밀어 두었다가
:focus에서 끌어오는 방식을 씁니다. 이 페이지 맨 위에서 Tab을 눌러 직접 확인해 보세요.
5. 모달·팝업 — 포커스 3원칙
대화상자를 열 때 지켜야 할 것은 세 가지입니다.
- 열면 포커스를 대화상자 안으로 옮긴다(대개 첫 요소나 제목).
- 열려 있는 동안 Tab이 대화상자 밖으로 나가지 않는다(포커스 가두기).
- 닫으면 포커스를 열기 전에 눌렀던 그 버튼으로 되돌린다.
let lastFocused;
function openDialog(dialog) {
lastFocused = document.activeElement;
dialog.hidden = false;
const focusables = dialog.querySelectorAll(
'a[href], button:not([disabled]), input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusables[0], last = focusables[focusables.length - 1];
first.focus();
dialog.addEventListener('keydown', (e) => {
if (e.key === 'Escape') { closeDialog(dialog); return; }
if (e.key !== 'Tab') return;
if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
});
}
function closeDialog(dialog) {
dialog.hidden = true;
lastFocused && lastFocused.focus();
}
마크업에는 role="dialog", aria-modal="true",
제목과 연결하는 aria-labelledby가 필요합니다. 네이티브 <dialog> 요소를
showModal()로 열면 포커스 가두기와 Esc 닫기를 브라우저가 처리해 주므로,
새로 만든다면 이쪽을 먼저 검토하세요.
6. 복합 위젯 — roving tabindex
탭 목록, 툴바, 메뉴, 라디오 그룹 같은 위젯은 위젯 전체가 Tab 한 번, 그 안의 이동은 방향키로 하는 것이 표준 패턴입니다(ARIA Authoring Practices). 자식 10개가 전부 탭 정지점이면 사용자가 피곤합니다.
<div role="tablist" aria-label="설정">
<button role="tab" aria-selected="true" tabindex="0">일반</button>
<button role="tab" aria-selected="false" tabindex="-1">알림</button>
<button role="tab" aria-selected="false" tabindex="-1">보안</button>
</div>
// 방향키로 이동하며 tabindex를 "굴린다"
tablist.addEventListener('keydown', (e) => {
if (!['ArrowRight', 'ArrowLeft'].includes(e.key)) return;
const tabs = [...tablist.querySelectorAll('[role=tab]')];
const i = tabs.indexOf(document.activeElement);
const next = e.key === 'ArrowRight'
? (i + 1) % tabs.length
: (i - 1 + tabs.length) % tabs.length;
tabs.forEach(t => t.tabIndex = -1);
tabs[next].tabIndex = 0;
tabs[next].focus();
});
7. 그 밖에 자주 걸리는 것
- 호버로만 열리는 메뉴 —
:hover에서만 나타나는 드롭다운은 키보드로 못 엽니다.:focus-within이나 클릭 토글을 함께 제공하세요(WCAG 2.1.1). - 무한 스크롤·캐러셀 — 자동으로 넘어가는 슬라이드는 멈춤 버튼이 필요하고(2.2.2), 각 슬라이드로 키보드 접근이 돼야 합니다.
- 커스텀 셀렉트·날짜 선택기 — 네이티브
<select>·<input type="date">가 가능한 상황이면 그게 가장 접근성이 좋습니다. - 포커스가 화면 밖 요소로 — 오프스크린 메뉴가 닫혀 있는데 그 안 링크에 포커스가 가면,
화면이 엉뚱한 곳으로 스크롤되거나 "사라진 포커스"가 됩니다. 닫힌 영역은
hidden또는inert처리하세요. - 키보드 함정 — 들어갔는데 Tab으로 못 나오는 위젯(옛 임베드 플러그인 등)은 2.1.2 위반입니다. 반드시 빠져나올 방법이 있어야 합니다.
8. 5분 점검법
- 마우스에서 손을 떼고 Tab으로 페이지를 처음부터 끝까지 통과한다.
- 지금 포커스가 어디 있는지 항상 눈에 보이는가?
- 탭 순서가 읽는 순서와 같은가?
- 메뉴·모달·탭·슬라이더 등 모든 기능이 실행되고, 빠져나올 수 있는가?
- 모달을 닫았을 때 포커스가 열었던 버튼으로 돌아오는가?
이 도구의 자동 검사는 div onclick, 양수 tabindex,
스킵 링크 부재, outline:none 패턴을 잡아 줍니다.
실제 탭 순서와 포커스 흐름은 위 5분 점검으로 직접 확인하는 것이 가장 확실합니다.