키보드 탐색과 포커스 관리
한국어 해설 KO
Original Text EN
키보드만으로 모든 기능을 사용할 수 있도록 Tab 순서, 포커스 표시, 건너뛰기 링크, 포커스 트랩을 학습합니다.
Learn how to ensure all functionality is keyboard accessible through tab order, focus indicators, skip links, and focus traps.
학습 목표
tabindex 값(0, -1, 양수)의 차이와 사용 기준을 설명할 수 있다
포커스 표시의 WCAG 요구사항을 이해한다
건너뛰기 링크의 목적과 구현 방법을 설명할 수 있다
모달에서의 포커스 트랩 패턴을 이해한다
Learning Objectives
Explain the differences between tabindex values (0, -1, positive)
Understand WCAG requirements for focus indicators
Describe the purpose and implementation of skip links
Understand the focus trap pattern for modals
키보드 접근성의 원칙
Principles of Keyboard Accessibility
키보드 접근성은 마우스를 사용할 수 없는 사용자(운동 장애, 화면낭독기 사용자, 임시 부상자)에게 필수이며, 스위치·음성 제어 같은 보조기술도 내부적으로 키보드 인터페이스를 사용합니다. 모든 인터랙티브 기능은 키보드로 접근·조작할 수 있어야 하고(WCAG 2.1.1, Level A), 들어간 곳에서 빠져나올 수 있어야 합니다(2.1.2 키보드 트랩 없음).
Keyboard accessibility is essential for users who cannot use a mouse — motor disabilities, screen reader users, temporary injuries — and assistive technologies like switches and voice control operate through the keyboard interface internally. All interactive functionality must be keyboard accessible (WCAG 2.1.1, Level A), and users must be able to leave anything they enter (2.1.2 No Keyboard Trap).
기본 키 관례: Tab/Shift+Tab으로 컨트롤 간 이동, Enter로 링크·버튼 활성화, Space로 버튼·체크박스 토글, 화살표 키로 라디오 그룹·탭·메뉴 같은 복합 위젯 내부 이동, Esc로 모달·메뉴 닫기. 커스텀 위젯을 만들면 이 관례를 그대로 구현해야 사용자의 기대와 일치합니다.
Core key conventions: Tab/Shift+Tab move between controls; Enter activates links and buttons; Space toggles buttons and checkboxes; arrow keys move within composite widgets (radio groups, tabs, menus); Esc closes modals and menus. Custom widgets must implement these conventions to match user expectations.
Tab 순서와 tabindex
Tab Order and tabindex
Tab 키는 포커스 가능한 요소(링크, 버튼, 입력 필드)를 DOM 순서대로 이동합니다. tabindex="0"은 원래 포커스 불가능한 요소를 자연스러운 DOM 순서에 넣고, tabindex="-1"은 탭 순서에서 제외하되 JavaScript focus()로는 포커스 가능하게 합니다(프로그래밍 포커스 — 모달 진입, SPA 헤딩 포커스 등에 사용).
Tab moves through focusable elements (links, buttons, inputs) in DOM order. tabindex="0" adds a normally unfocusable element into the natural order; tabindex="-1" removes an element from the tab order while allowing programmatic focus() — used for modal entry and SPA heading focus.
양수 tabindex는 탭 순서를 DOM과 다르게 강제하므로 피해야 합니다 — 유지보수가 어렵고 예측 불가능한 순서를 만듭니다. 근본 해결은 DOM 순서 자체를 논리적으로 배치하는 것입니다. 포커스 순서는 의미와 조작 순서에 맞아야 한다는 것이 WCAG 2.4.3(포커스 순서)의 요구입니다. CSS로 시각 순서만 바꾸면(flex order 등) 시각과 포커스 순서가 어긋나는 결함이 생깁니다.
Avoid positive tabindex values: they force an order different from the DOM, are hard to maintain, and create unpredictable sequences. The real fix is arranging the DOM logically. WCAG 2.4.3 (Focus Order) requires focus order to match meaning and operation — reordering only visually with CSS (flex order) creates a mismatch defect between visual and focus order.
포커스 표시와 건너뛰기 링크
Focus Indicators and Skip Links
포커스된 요소는 시각적으로 명확히 표시되어야 합니다(WCAG 2.4.7 가시적 포커스, Level AA). CSS `outline: none`으로 포커스 표시를 제거하고 대안을 주지 않으면 위반입니다. `:focus-visible` 의사 클래스를 쓰면 키보드 사용 시에만 표시하고 마우스 클릭 시엔 숨길 수 있어 디자인과 접근성을 함께 만족합니다. WCAG 2.2는 포커스된 요소가 다른 콘텐츠에 가려지지 않을 것(2.4.11 포커스 가림 방지)도 요구합니다.
Focused elements must be clearly visible (WCAG 2.4.7 Focus Visible, Level AA). Removing indicators with `outline: none` without an alternative is a violation. The `:focus-visible` pseudo-class shows indicators for keyboard use while hiding them on mouse clicks — satisfying both design and accessibility. WCAG 2.2 adds that the focused element must not be hidden by other content (2.4.11 Focus Not Obscured).
건너뛰기 링크(Skip Link)는 페이지 첫 요소로 '본문으로 바로가기'를 제공해, 키보드 사용자가 반복되는 내비게이션을 건너뛰고 주요 콘텐츠로 즉시 이동하게 합니다(WCAG 2.4.1 블록 건너뛰기, Level A). 평소엔 화면 밖에 숨겼다가 포커스를 받으면 나타나는 패턴이 일반적이며, 대상(`#main-content`)이 tabindex="-1"로 포커스를 받을 수 있어야 완성됩니다.
A skip link — 'Skip to main content' as the first element — lets keyboard users bypass repeated navigation (WCAG 2.4.1 Bypass Blocks, Level A). The common pattern hides it off-screen until focused; it's complete only when the target (`#main-content`) can receive focus via tabindex="-1".
포커스 관리 — 모달과 SPA
Focus Management — Modals and SPAs
모달 다이얼로그가 열리면: 포커스를 모달 안으로 이동시키고, 포커스가 모달 내부에만 머물게 하며(마지막 요소에서 Tab → 첫 요소로 순환), 배경 콘텐츠는 inert 처리하고, Esc로 닫을 수 있어야 합니다. 모달이 닫히면 모달을 열었던 트리거 요소로 포커스를 되돌립니다. 네이티브 `<dialog>` 요소의 showModal()은 이 중 상당 부분을 기본 제공합니다.
When a modal opens: move focus into it, contain focus inside (Tab from the last element wraps to the first), make the background inert, and support Esc to close. When it closes, return focus to the trigger element. The native `<dialog>` element's showModal() provides much of this for free.
SPA(단일 페이지 앱)에서는 라우트가 바뀌어도 전체 페이지가 새로 로드되지 않아 포커스와 낭독이 이전 상태에 머뭅니다. 라우트 변경 시 새 화면의 주요 제목(h1)에 tabindex="-1"을 주고 포커스를 이동시키거나, 라이브 영역으로 페이지 전환을 알리는 패턴이 필요합니다. 동적으로 나타나는 콘텐츠(알림, 삭제 후 목록)도 포커스가 허공에 남지 않게 관리해야 합니다.
In an SPA, route changes don't reload the page, so focus and announcements linger in the previous state. On route change, give the new screen's main heading tabindex="-1" and move focus to it, or announce the transition via a live region. Dynamic content (toasts, lists after deletion) also needs focus management so focus never dangles on removed elements.
// 열기: 트리거를 기억하고 모달 안 첫 포커서블로 이동
const opener = document.activeElement;
dialog.showModal();
dialog.querySelector("button, [href], input")?.focus();
// 닫기: 원래 트리거로 포커스 반환 (사용자가 길을 잃지 않도록)
dialog.addEventListener("close", () => opener.focus());