들어가며#
혹시 마우스 없이 인터넷을 쓴 경험이 있으신가요? 대부분의 사람들은 마우스를 당연하게 사용합니다. 하지만 세상에는 마우스를 쓸 수 없는 사람들이 있어요.
- 신체 장애로 인해 마우스를 조작할 수 없는 사람
- 손목터널증후군 같은 반복성 긴장 장애가 있는 사람
- 일시적으로 팔이 다친 사람
- 단순히 키보드가 더 효율적이라고 생각하는 파워 유저
이런 사용자들에게 “키보드로 이 사이트를 쓸 수 있나요?“라는 질문은 매우 중요합니다.
여기서 얘기하는 키보드 접근성은 단순히 Tab 키로 이동하는 것만을 의미하지 않습니다. 포커스 관리, 키보드 단축키, 드래그 앤 드롭 대체 수단, 모달 창 내비게이션 등 정말 많은 것들이 포함되어 있어요.
이번 글에서는 키보드 접근성의 A부터 Z까지 모든 것을 다루겠습니다. 이론적 배경도 있지만, 무엇보다 실무에서 바로 적용할 수 있는 코드 예제를 중심으로 진행할게요.

사진: Nanobanana AI로 생성
왜 키보드 접근성이 중요한가?#

사진: Nanobanana AI로 생성
표준과 실무 요구사항#
많은 접근성 기준이 키보드 접근성을 핵심 요구사항으로 다룹니다. 특히 WCAG 2.2의 Keyboard Accessible 성공 기준은 “키보드만으로 모든 기능을 사용할 수 있어야 한다”는 원칙을 명확히 합니다.
사용자 수요#
실제로 얼마나 많은 사람들이 키보드로 웹을 이용할까요?
- 운동장애로 마우스를 쓰기 어려운 사용자
- 일시적 부상이나 피로로 마우스 사용이 불편한 사용자
- 효율성 때문에 키보드를 선호하는 파워 유저
부수적 이점 (Co-benefits)#
키보드 접근성은 마치 건물의 경사로 같습니다.
처음엔 “휠체어 사용자를 위한 시설이겠지?” 생각하지만, 실제로는 유모차를 끄는 부모, 무거운 짐을 끄는 배달 기사, 계단이 불편한 어르신까지 모두가 편하게 이용하죠. 키보드 접근성도 마찬가지예요.
흥미롭게도, 키보드 접근성 개선은 다른 접근성도 함께 개선합니다:
- 스크린 리더: 키보드 접근성이 좋으면 스크린 리더 사용도 자동으로 좋아짐
- 음성 제어: 키보드 네비게이션이 가능하면 음성 제어도 가능
- 모바일: 모바일 터치는 일종의 포커스 기반 내비게이션
- 개발 효율: 명확한 포커스 관리는 코드 품질 향상
기본 개념: 포커스와 Tab 순서#
포커스란?#
포커스(Focus)는 현재 키보드 입력을 받을 준비가 된 요소를 의미합니다.
<!-- 포커스 가능한 요소들 -->
<button>클릭하세요</button>
<a href="#">링크</a>
<input type="text">
<select>
<option>선택</option>
</select>이런 요소들은 기본적으로 포커스 가능합니다. 하지만 <div>, <span> 같은 요소는 기본적으로 포커스 불가능이죠.
Tab 순서의 중요성#
사용자가 Tab 키를 누르면 웹사이트는 요소들을 순서대로 포커스 이동합니다. 이 순서가 논리적이어야 해요.
좋은 예시:
[1] 검색 입력창
↓
[2] 검색 버튼
↓
[3] 첫 번째 검색 결과
↓
[4] 두 번째 검색 결과나쁜 예시:
[1] 푸터의 링크
↓
[2] 왼쪽 사이드바
↓
[3] 메인 콘텐츠
↓
[4] 헤더이런 순서는 사용자를 혼란스럽게 만들어요.
tabindex 속성#
기본 tab 순서를 바꾸거나 포커스 불가능한 요소를 포커스 가능하게 할 수 있습니다:
<!-- tabindex 값에 따른 우선순위 -->
<!-- 1부터 32767까지 지정 가능 (작을수록 우선) -->
<button tabindex="1">첫 번째</button>
<button tabindex="2">두 번째</button>
<!-- 0: 자동 순서에 포함 (권장) -->
<div tabindex="0" role="button">포커스 가능한 div</div>
<!-- -1: 자동 순서에서 제외하되 프로그래매틱 포커스 가능 -->
<div tabindex="-1" id="modal-content">모달 콘텐츠</div>중요한 규칙:
tabindex값으로 1 이상의 숫자를 쓰는 것은 피해야 합니다 (물론 정말 특별히 필요한 경우도 있을수는 있어요.)- 항상 0 또는 -1을 사용하고, DOM 순서로 탭 순서를 관리하세요

사진: Nanobanana AI로 생성
스킵 링크와 포커스 복귀#
키보드 사용자는 페이지를 열 때마다 헤더의 메뉴 링크를 전부 Tab으로 지나야 본문에 닿습니다. 메뉴가 스무 개면 스무 번이죠. 스킵 링크는 그걸 건너뛰게 해주는 링크예요. 평소에는 화면 밖에 숨어 있다가 첫 Tab을 누르는 순간 나타납니다.
<!-- 문서 최상단: 첫 Tab에 잡히도록 배치 -->
<a class="skip-link" href="#main">본문 바로가기</a>
<main id="main" tabindex="-1">
...
</main>
<style>
.skip-link {
position: absolute;
left: -999px;
}
.skip-link:focus {
left: 1rem;
top: 1rem;
background: #fff;
outline: 3px solid #4A90E2;
}
</style>- 모달·드롭다운을 닫을 때는 열기 버튼으로 포커스를 되돌리는 것이 좋습니다. 이는 키보드 사용자가 흐름을 잃지 않도록 돕고, 포커스 순서를 안정적으로 유지하는 데 도움이 됩니다.
WCAG 2.2#
- SC 2.5.7 Dragging Movements: 드래그 동작이 있다면 드래그 없이도 단일 포인터로 같은 기능을 수행할 수 있어야 합니다. (드래그가 필수인 경우는 예외)
- SC 2.5.8 Target Size (Minimum): 대화형 타깃을 24px 이상(또는 최소 24×24 영역 확보)으로 설계하세요.
키 처리: 이벤트와 단축키#
키 이벤트 기초#
어떤 키가 눌렸는지 알아내는 것부터 시작합니다. 눌린 키는 두 가지 이름으로 들어와요. event.key는 “실제로 입력된 글자”(Enter, a, ㄱ)이고, event.code는 “물리적으로 어느 자리의 키인가”(KeyA)입니다. 단축키 처리에는 보통 key만 봐도 충분합니다.
// 기본적인 키 이벤트 처리
element.addEventListener('keydown', (event) => {
console.log('키 코드:', event.code);
console.log('키 값:', event.key);
// 특정 키 확인
if (event.key === 'Enter') {
// Enter 처리
}
});주의사항: keyCode는 deprecated되었습니다. key 또는 code를 사용하세요.
주요 키와 역할#
사용자가 어떤 키를 누르면 무엇을 기대하는지는 이미 관습으로 굳어져 있습니다. 이 관습을 깨면 “왜 안 되지?” 하는 순간이 생겨요. 아래 표는 그 관습을 정리한 것인데, 지금 외울 필요는 없습니다. 커스텀 위젯을 만들 때 다시 펴보시면 됩니다.
| 키 | 용도 | 예제 |
|---|---|---|
Tab | 다음 포커스 이동 | 모든 요소에서 자동 |
Shift+Tab | 이전 포커스 이동 | 모든 요소에서 자동 |
Enter | 버튼 클릭, 폼 제출 | <button>, <a> |
Space | 버튼 활성화, 체크박스 토글 | <button>, <input type="checkbox"> |
Escape | 모달 닫기, 메뉴 닫기 | 커스텀 구현 필요 |
Arrow Keys | 목록 네비게이션, 라디오 버튼 선택 | 커스텀 위젯 |
기본 동작 방지와 키 핸들링#
키에는 브라우저가 이미 정해둔 동작이 있습니다. Space는 페이지를 아래로 스크롤하죠. 커스텀 버튼에서 Space로 활성화를 처리하면서 이걸 그냥 두면, 눌릴 때마다 화면이 같이 툭 내려갑니다. preventDefault()가 그 기본 동작을 막는 역할이에요.
// 올바른 키 핸들링 예제
button.addEventListener('keydown', (event) => {
// 기본 동작 확인 (브라우저 기본 동작 방지 필요할 때만)
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
handleButtonClick();
}
});중요: 표준 HTML 요소(<button>, <a>)를 사용하면 브라우저가 자동으로 키 처리를 해줍니다. 커스텀 요소만 직접 처리하면 됩니다.
포커스 관리: 언제 어디로 이동할까?#
자동 포커스 vs 수동 포커스#
포커스를 코드로 옮기는 일은 조심스럽습니다. 사용자가 원하지 않았는데 커서가 튀면, 키보드 사용자는 자기가 어디 있는지 순식간에 잃어버리거든요. 기준은 하나예요. 사용자가 방금 한 행동의 결과로 화면이 바뀌었을 때만 포커스를 옮깁니다.
// ❌ 나쁜 예: 무분별한 자동 포커스
input.addEventListener('blur', () => {
// 사용자가 의도하지 않은 포커스 이동
someOtherElement.focus();
});
// ✅ 좋은 예: 사용자 의도에 따른 포커스 이동
button.addEventListener('click', () => {
// 모달 열린 후 첫 포커스 가능 요소로 이동
modal.showModal();
modal.querySelector('input').focus();
});포커스 함정(Focus Trap) 구현#
모달 다이얼로그처럼 배경과 상호작용을 막는 UI에서만 포커스 함정을 적용합니다. 일반 패널이나 드롭다운에서는 사용자가 자유롭게 이동할 수 있어야 합니다.
function createFocusTrap(modalElement) {
const focusableElements = modalElement.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
modalElement.addEventListener('keydown', (event) => {
if (event.key !== 'Tab') return;
if (event.shiftKey) {
// Shift+Tab: 뒤로 이동
if (document.activeElement === firstElement) {
event.preventDefault();
lastElement.focus();
}
} else {
// Tab: 앞으로 이동
if (document.activeElement === lastElement) {
event.preventDefault();
firstElement.focus();
}
}
});
}포커스 복원#
다이얼로그나 오버레이가 닫힐 때, 원래 포커스 위치로 돌아가야 합니다:
class Modal {
constructor(element) {
this.element = element;
this.previouslyFocusedElement = null;
}
open() {
// 열기 전 현재 포커스 저장
this.previouslyFocusedElement = document.activeElement;
this.element.showModal();
this.element.querySelector('input').focus();
}
close() {
this.element.close();
// 원래 포커스로 복원
if (this.previouslyFocusedElement && this.previouslyFocusedElement.focus) {
this.previouslyFocusedElement.focus();
}
}
}
사진: Nanobanana AI로 생성
커스텀 위젯: 표준이 없을 때의 키보드 처리#
많은 개발자들이 표준 HTML 요소를 무시하고 커스텀 위젯을 만듭니다. 이런 경우 모든 키 처리를 직접 구현해야 합니다.
드롭다운 메뉴#
<select> 대신 직접 만든 드롭다운이라면, 브라우저가 공짜로 주던 키 동작이 전부 사라집니다. 아래 코드는 그걸 하나씩 되살린 것이에요. 위아래 화살표로 항목 이동, Home·End로 처음과 끝, Enter로 선택, Escape로 닫고 트리거 버튼으로 포커스 복귀까지. 길어 보이지만 하는 일은 이 다섯 가지가 전부입니다.
class Dropdown {
constructor(triggerButton, menu) {
this.trigger = triggerButton;
this.menu = menu;
this.items = menu.querySelectorAll('[role="menuitem"]');
this.currentIndex = -1;
this.setupListeners();
}
setupListeners() {
// 트리거 키 처리
this.trigger.addEventListener('keydown', (event) => {
if (event.key === 'Enter' || event.key === ' ' || event.key === 'ArrowDown') {
event.preventDefault();
this.open();
this.focusItem(0);
}
});
// 메뉴 키 처리
this.menu.addEventListener('keydown', (event) => {
switch (event.key) {
case 'ArrowDown':
event.preventDefault();
this.focusNext();
break;
case 'ArrowUp':
event.preventDefault();
this.focusPrev();
break;
case 'Home':
event.preventDefault();
this.focusItem(0);
break;
case 'End':
event.preventDefault();
this.focusItem(this.items.length - 1);
break;
case 'Enter':
event.preventDefault();
this.selectCurrent();
break;
case 'Escape':
event.preventDefault();
this.close();
this.trigger.focus();
break;
}
});
}
focusItem(index) {
if (index < 0 || index >= this.items.length) return;
this.currentIndex = index;
this.items[index].focus();
// 시각적 강조
this.items.forEach((item, i) => {
item.setAttribute('aria-selected', i === index);
});
}
focusNext() {
const nextIndex = this.currentIndex + 1;
if (nextIndex < this.items.length) {
this.focusItem(nextIndex);
}
}
focusPrev() {
const prevIndex = this.currentIndex - 1;
if (prevIndex >= 0) {
this.focusItem(prevIndex);
}
}
open() {
this.menu.classList.add('visible');
}
close() {
this.menu.classList.remove('visible');
this.currentIndex = -1;
}
selectCurrent() {
if (this.currentIndex >= 0) {
this.items[this.currentIndex].click();
}
this.close();
}
}트리 위젯 (Tree Widget)#
파일 탐색기 같은 계층 구조를 가진 위젯:
class TreeWidget {
constructor(rootElement) {
this.root = rootElement;
this.setupListeners();
}
setupListeners() {
this.root.addEventListener('keydown', (event) => {
const item = event.target.closest('[role="treeitem"]');
if (!item) return;
switch (event.key) {
case 'ArrowRight':
event.preventDefault();
this.expandItem(item);
break;
case 'ArrowLeft':
event.preventDefault();
this.collapseItem(item);
break;
case 'ArrowDown':
event.preventDefault();
this.focusNextItem(item);
break;
case 'ArrowUp':
event.preventDefault();
this.focusPrevItem(item);
break;
case 'Home':
event.preventDefault();
this.focusFirstItem();
break;
case 'End':
event.preventDefault();
this.focusLastItem();
break;
case '*': // 모든 항목 확장
event.preventDefault();
this.expandAll();
break;
}
});
}
expandItem(item) {
item.setAttribute('aria-expanded', 'true');
const children = item.nextElementSibling;
if (children) {
children.style.display = 'block';
}
}
collapseItem(item) {
item.setAttribute('aria-expanded', 'false');
const children = item.nextElementSibling;
if (children) {
children.style.display = 'none';
}
}
// ... 기타 메서드
}
사진: Nanobanana AI로 생성
포커스 스타일: 보이지 않으면 접근할 수 없다#

사진: Nanobanana AI로 생성
기본 포커스 스타일의 문제#
많은 개발자들이 기본 포커스 스타일(파란색 테두리)을 없애고 대체 스타일을 제공하지 않습니다. 때로는 디자인 때문에 그런 경우도 있고, 때로는 관행상 그렇게 처리하는 경우도 있죠. 그리고 떄로는… 아웃라인 혹은 보더로 인해 레이아웃이 깨지는 경우에 대해 대응하지 못하고 없애는 경우도 보긴 했습니다… 그러면 안되겠죠?
/* ❌ 절대 금지: 포커스 아웃라인 제거 후 대체 없음 */
button:focus {
outline: none;
}
/* ✅ 권장: 명확한 포커스 스타일 */
button:focus {
outline: 3px solid #4A90E2;
outline-offset: 2px;
}좋은 포커스 스타일의 특징#
그럼 어떻게 그려야 “잘 보이는” 포커스일까요. 요소마다 따로 손대는 대신, 전역 규칙 하나를 두고 다크 모드와 고대비 모드에서만 값을 조정하는 방식을 추천합니다.
/* 포괄적 포커스 스타일 */
:focus-visible {
outline: 3px solid var(--primary);
outline-offset: 2px;
border-radius: 3px;
}
/* 다크 모드 지원 */
@media (prefers-color-scheme: dark) {
:focus-visible {
outline-color: #58A6FF;
}
}
/* 고대비 모드 지원 */
@media (prefers-contrast: more) {
:focus-visible {
outline-width: 4px;
outline-offset: 3px;
}
}포커스 스타일의 핵심 요건:
- ✅ 배경과 충분한 대비 확보 (비텍스트 대비 3:1 이상을 목표)
- ✅ 작은 요소에서도 명확하게 인지될 정도의 면적
- ✅ 모든 포커스 가능한 요소에 일관되게 적용
- ✅ 다크/라이트 모드 모두에서 시인성 유지
:focus-visible vs :focus#
디자이너가 포커스 링을 지우고 싶어 하는 이유는 대개 하나입니다. 마우스로 버튼을 눌렀을 뿐인데 테두리가 남거든요. :focus-visible이 정확히 그 문제를 풉니다. 브라우저가 “이번 포커스는 키보드로 온 것"이라고 판단할 때만 걸리는 선택자예요. 마우스 클릭에는 반응하지 않습니다.
/* :focus - 모든 포커스 상태 */
button:focus {
background: blue;
}
/* :focus-visible - 키보드 포커스만 */
button:focus-visible {
outline: 3px solid blue;
}
/* 결과: 마우스 클릭은 파란색 배경만, 키보드 Tab은 파란색 아웃라인 표시 */드래그 앤 드롭 대체 수단#
드래그 앤 드롭은 마우스 전용 상호작용입니다. 키보드 사용자도 같은 기능을 할 수 있어야 해요.
접근성 있는 드래그 앤 드롭#
class AccessibleDragDrop {
constructor(items, container) {
this.items = items;
this.container = container;
this.selectedItem = null;
this.sourceIndex = null;
this.setupListeners();
}
setupListeners() {
// 각 항목에 대해 위치 변경 메뉴 제공
this.items.forEach((item, index) => {
item.setAttribute('draggable', 'true');
item.setAttribute('role', 'button');
item.setAttribute('tabindex', '0');
// 마우스 드래그
item.addEventListener('dragstart', (e) => {
this.sourceIndex = index;
});
item.addEventListener('drop', (e) => {
e.preventDefault();
if (this.sourceIndex !== null) {
this.moveItem(this.sourceIndex, index);
}
});
// 키보드 내비게이션
item.addEventListener('keydown', (e) => {
const currentIndex = Array.from(this.items).indexOf(item);
switch (e.key) {
case 'ArrowUp':
case 'ArrowLeft':
if (currentIndex > 0) {
e.preventDefault();
this.moveItem(currentIndex, currentIndex - 1);
this.items[currentIndex - 1].focus();
}
break;
case 'ArrowDown':
case 'ArrowRight':
if (currentIndex < this.items.length - 1) {
e.preventDefault();
this.moveItem(currentIndex, currentIndex + 1);
this.items[currentIndex + 1].focus();
}
break;
}
});
});
}
moveItem(fromIndex, toIndex) {
const items = Array.from(this.items);
const [movedItem] = items.splice(fromIndex, 1);
items.splice(toIndex, 0, movedItem);
// DOM 업데이트
this.container.innerHTML = '';
items.forEach(item => this.container.appendChild(item));
this.items = this.container.querySelectorAll('[draggable]');
this.setupListeners();
}
}더 간단한 대안:
<!-- 각 항목마다 이동 버튼 제공 -->
<div class="list-item">
<span>항목 1</span>
<button aria-label="위로 이동">▲</button>
<button aria-label="아래로 이동">▼</button>
</div>
사진: Nanobanana AI로 생성
자주 실수하는 패턴들#
“이거 금방 고치겠지?” 하고 시작한 포커스 스타일 작업이 3시간째 CSS와 씨름 중이라면… 축하합니다, 정상입니다. 😅
실수 1: 포커스 스타일 제거#
많은 개발자가 기본 포커스 스타일(파란색 테두리)이 “못생겼다"고 생각합니다. 그래서 outline: none을 넣고 대체 스타일을 만들려다가… 자꾸 잊어버리죠. 결과적으로 키보드 사용자는 자신이 어디에 있는지 알 수 없게 됩니다.
/* ❌ 절대 금지 */
* {
outline: none;
}
/* 또는 */
button:focus {
outline: 0;
}실수 2: click 이벤트만 걸고 키보드는 빠뜨리기#
<div>에 click 핸들러를 붙이면 마우스로는 잘 동작합니다. 그래서 다 됐다고 착각하기 쉬워요. 하지만 <div>는 Tab으로 포커스가 가지 않고, Enter나 Space를 눌러도 click이 발생하지 않습니다. 키보드 사용자에게는 그냥 글자 덩어리인 셈이죠.
// ❌ 문제: div에 click만 있으면 키보드 미지원
div.addEventListener('click', handleClick);
// ✅ 올바름: button이나 keydown도 처리
button.addEventListener('click', handleClick);
button.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
handleClick();
}
});실수 3: 포커스 가능해야 할 요소를 숨김#
스킵 링크처럼 “평소엔 안 보이지만 포커스가 오면 나타나야 하는” 요소가 있습니다. 이때 display: none을 쓰면 요소가 접근성 트리에서 통째로 빠져서 포커스 자체가 불가능해져요. 시각적으로만 숨기는 다른 방법이 필요합니다.
/* ❌ 문제: display: none하면 포커스도 불가능 */
.hidden {
display: none;
}
/* ✅ 시각적으로는 숨기되 포커스는 가능 */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
}실수 4: 시간 제한 없는 상호작용#
정확히는 연장할 방법이 없는 시간 제한이 문제입니다. 일정 시간 뒤 폼이 자동으로 닫히도록 만들었다면, 그 시간은 마우스로 빠르게 클릭하는 사람 기준으로 잡혔을 가능성이 큽니다. 키보드로 한 칸씩 이동하며 입력하는 사람에게는 턱없이 짧아요.
// ❌ 문제: 사용자가 천천히 키보드로 입력하면 타임아웃
const timeout = setTimeout(() => {
closeForm();
}, 5000);
// ✅ 사용자가 활동 중이면 타임아웃 연장
document.addEventListener('keydown', () => {
clearTimeout(timeout);
timeout = setTimeout(() => closeForm(), 5000);
});테스트하기: 직접 경험해보기#
수동 테스트 (가장 중요)#
도구를 켜기 전에 5분만 직접 해보시길 권합니다. 자동화 도구가 잡아내지 못하는 문제 — 포커스가 어디로 튀는지, 순서가 어색한지 — 는 결국 사람이 써봐야 보이거든요.
1. 마우스를 치워둔다
2. 키보드만으로 웹사이트를 탐색한다:
- Tab으로 모든 기능에 접근할 수 있는가?
- 포커스 위치가 보이는가?
- 탭 순서가 논리적인가?
- 타이핑 속도가 느려도 작동하는가?
3. 스크린 리더와 함께 사용해본다자동화 테스트#
// axe DevTools, WAVE, Lighthouse 사용
// 또는 프로그래매틱 테스트:
test('모든 버튼이 키보드로 활성화 가능', () => {
const buttons = document.querySelectorAll('button');
buttons.forEach(button => {
const keyboardActivated = new KeyboardEvent('keydown', {
key: 'Enter'
});
button.dispatchEvent(keyboardActivated);
// 활성화 확인
});
});브라우저 별 확인 포인트#
키보드 동작은 브라우저마다 조금씩 다릅니다. 특히 Safari는 기본 설정에서 Tab이 링크와 입력창만 훑고 버튼을 건너뛰기 때문에, 시스템 설정에서 전체 키보드 접근을 켜지 않으면 “우리 사이트는 잘 된다"고 착각하기 쉬워요.
| 브라우저 | 확인 항목 |
|---|---|
| Chrome | 포커스 순서, 아웃라인 렌더링 |
| Safari | Mac 키보드 네비게이션 (전체 키보드 접근 켜기) |
| Firefox | ARIA 준수 |
| Edge | Windows 고대비 모드 |

사진: Nanobanana AI로 생성
정리하며#
포스트를 작성하다보니, 예시 코드를 너무 많이 작성한게 아닌가 싶네요. 이럴려고 이 주제의 글 작성을 시작한게 아닌데… 보시기 어땠나요? 코드가 너무 많아서 보시기 불편하진 않으셨나요?
어찌되었던 키보드 접근성은 기술적 요구사항일 뿐만 아니라 사용자 경험의 질을 좌우합니다.
체크리스트:
- ✅ Tab으로 모든 기능 접근 가능
- ✅ 포커스 순서가 논리적
- ✅ 포커스 스타일이 명확 (3px 이상, 충분한 색상 대비)
- ✅ 모달/팝업에서 포커스 함정 구현
- ✅ Escape로 닫을 수 있음
- ✅ 드래그 앤 드롭 등에 키보드 대체 수단 제공
- ✅ 커스텀 위젯에서 WAI-ARIA 패턴 따름
- ✅ 수동 테스트 완료
이 모든 것들이 “당연해 보이지만” 실무에서 자주 놓치는 부분들입니다. 하나씩 체크해가며 구현한다면, 훨씬 더 포용적인 웹사이트를 만들 수 있을 거예요.
