도입 스토리
"탭 컴포넌트를 만들었어요." 김개발이 화면을 보여주었습니다.
Tab 키로 첫 번째 탭에 포커스가 갔습니다. Enter를 눌렀더니 탭이 활성화됐습니다. 다시 Tab을 눌렀더니 두 번째 탭으로 이동했습니다.
"좋아 보이는데요?" 박멘토가 말했습니다.
"감사합니다." 김개발이 웃었습니다.
"APG 탭 패턴을 봐요." 박멘토가 화면을 열었습니다. "탭 안에서의 이동은 방향키로 해야 해요. Tab 키는 탭 목록 전체를 하나의 그룹으로 보고 건너뛰어야 하고요. 이게 roving tabindex 패턴이에요."
핵심 개념 설명
탭 컴포넌트 — APG Tabs 패턴
<!-- 탭 구조 --><div class="tabs"> <!-- 탭 목록: role="tablist" --> <div role="tablist" aria-label="상품 정보"> <!-- 첫 번째 탭 (기본 선택) --> <button id="tab-1" role="tab" aria-selected="true" aria-controls="panel-1" tabindex="0" > 기본 정보 </button> <!-- 비선택 탭: tabindex="-1" --> <button id="tab-2" role="tab" aria-selected="false" aria-controls="panel-2" tabindex="-1" > 리뷰 </button> <button id="tab-3" role="tab" aria-selected="false" aria-controls="panel-3" tabindex="-1" > Q&A </button> </div> <!-- 탭 패널 --> <div id="panel-1" role="tabpanel" aria-labelledby="tab-1" tabindex="0" > <h3>기본 정보</h3> <p>상품의 기본 정보입니다.</p> </div> <div id="panel-2" role="tabpanel" aria-labelledby="tab-2" tabindex="0" hidden > <h3>리뷰</h3> <p>사용자 리뷰입니다.</p> </div> <div id="panel-3" role="tabpanel" aria-labelledby="tab-3" tabindex="0" hidden > <h3>Q&A</h3> <p>자주 묻는 질문입니다.</p> </div></div>
Roving Tabindex 패턴
탭 그룹 내에서 선택된 탭만 tabindex="0", 나머지는 tabindex="-1".
방향키로 탭 간 이동, Tab 키로 탭 패널로 이동합니다.
class Tabs { constructor(container) { this.container = container; this.tabs = [...container.querySelectorAll('[role="tab"]')]; this.panels = [...container.querySelectorAll('[role="tabpanel"]')]; this.tabs.forEach(tab => { tab.addEventListener('click', () => this.select(tab)); tab.addEventListener('keydown', (e) => this.handleKeydown(e)); }); } select(selectedTab) { this.tabs.forEach(tab => { const isSelected = tab === selectedTab; tab.setAttribute('aria-selected', isSelected); tab.setAttribute('tabindex', isSelected ? '0' : '-1'); }); this.panels.forEach(panel => { const isActive = panel.id === selectedTab.getAttribute('aria-controls'); panel.hidden = !isActive; }); selectedTab.focus(); } handleKeydown(e) { const currentIndex = this.tabs.indexOf(e.currentTarget); let nextIndex; switch (e.key) { case 'ArrowRight': nextIndex = (currentIndex + 1) % this.tabs.length; e.preventDefault(); break; case 'ArrowLeft': nextIndex = (currentIndex - 1 + this.tabs.length) % this.tabs.length; e.preventDefault(); break; case 'Home': nextIndex = 0; e.preventDefault(); break; case 'End': nextIndex = this.tabs.length - 1; e.preventDefault(); break; default: return; } this.select(this.tabs[nextIndex]); }}// 초기화document.querySelectorAll('.tabs').forEach(tabs => new Tabs(tabs));
아코디언 컴포넌트
아코디언은 여러 섹션을 접고 펼칠 수 있는 컴포넌트입니다.
<div class="accordion"> <!-- 아코디언 항목 1 --> <h3> <button type="button" aria-expanded="false" aria-controls="section-1" id="accordion-btn-1" > 배송 정보 </button> </h3> <div id="section-1" role="region" aria-labelledby="accordion-btn-1" hidden > <p>배송은 주문 후 2-3일 소요됩니다.</p> </div> <!-- 아코디언 항목 2 --> <h3> <button type="button" aria-expanded="false" aria-controls="section-2" id="accordion-btn-2" > 교환/반품 </button> </h3> <div id="section-2" role="region" aria-labelledby="accordion-btn-2" hidden > <p>구매 후 7일 이내 교환/반품이 가능합니다.</p> </div></div>
document.querySelectorAll('.accordion button').forEach(btn => { btn.addEventListener('click', () => { const isExpanded = btn.getAttribute('aria-expanded') === 'true'; const panel = document.getElementById(btn.getAttribute('aria-controls')); btn.setAttribute('aria-expanded', !isExpanded); panel.hidden = isExpanded; });});
탭과 달리 아코디언은 방향키 이동이 APG에서 필수가 아닙니다. 각 버튼이 독립적이어서 Tab으로 이동하는 것이 자연스럽습니다.
단계별 실습
따라하기: 탭 키보드 동작 테스트
접근성 있는 탭 컴포넌트의 키보드 동작을 확인합니다.
- Tab 키로 탭 목록의 첫 번째 탭에 포커스.
- 오른쪽 방향키로 다음 탭으로 이동 (선택).
- 왼쪽 방향키로 이전 탭으로 이동.
- Home/End로 첫/마지막 탭으로 이동.
- Tab으로 탭 패널 안으로 이동.
- Shift+Tab으로 탭 패널에서 탭 목록으로 복귀.
변형하기: 수직 탭 방향키 변경
세로형 탭에서는 좌우 방향키 대신 상하 방향키를 사용합니다.
// 수직 탭: aria-orientation 설정tablist.setAttribute('aria-orientation', 'vertical');// 수직 탭의 키보드 핸들러case 'ArrowDown': nextIndex = (currentIndex + 1) % this.tabs.length; e.preventDefault(); break;case 'ArrowUp': nextIndex = (currentIndex - 1 + this.tabs.length) % this.tabs.length; e.preventDefault(); break;
정리와 확인
핵심 내용 요약
- 탭 구조:
tablist>tab+tabpanel - Roving tabindex: 선택 탭
tabindex="0", 나머지tabindex="-1" - 탭 방향키: 좌우(수평) 또는 상하(수직) 방향키로 탭 간 이동
aria-selected: 현재 선택된 탭 표시- 아코디언:
aria-expanded+hidden패널, Tab으로 버튼 간 이동
확인 문제
문제 1. 탭 컴포넌트에서 비선택 탭의 tabindex 값은?
tabindex="-1"
Tab 키로 그룹 전체를 건너뛰되, 방향키로 탭 간 이동을 지원합니다.
문제 2. aria-controls가 하는 역할은?
탭 버튼이 어떤 패널을 제어하는지 연결합니다.
aria-controls="panel-1"이면 id="panel-1" 요소를 가리킵니다.
다음 챕터에서는 자동완성 입력(Combobox)과 드롭다운 메뉴를 다룹니다. ARIA 패턴 중 가장 복잡한 컴포넌트들의 구현을 알아봅니다.