stack-analysis
CHAPTER 25 / 90
읽기 약 2분
FUNCTION
접근성(a11y): WCAG 2.2 + ARIA
핵심 개념
aria-label·focus management·키보드 네비·screen reader — Lighthouse a11y 100점.
본문
시맨틱 HTML 우선
// ❌ div 남발
<div onClick={handleClick}>버튼</div>
// ✅ 시맨틱
<button onClick={handleClick}>버튼</button>
// ❌
<div className="header">
<div className="logo">로고</div>
<div className="nav">
<div onClick={...}>메뉴1</div>
</div>
</div>
// ✅
<header>
<a href="/" aria-label="홈">로고</a>
<nav aria-label="주요 메뉴">
<ul>
<li><a href="/about">메뉴1</a></li>
</ul>
</nav>
</header>ARIA 속성
// 모달
<div
role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
aria-describedby="dialog-desc"
>
<h2 id="dialog-title">삭제 확인</h2>
<p id="dialog-desc">정말 삭제하시겠습니까?</p>
<button>취소</button>
<button>확인</button>
</div>
// 토글 버튼
<button
aria-pressed={isPressed}
aria-label={isPressed ? '음소거 해제' : '음소거'}
onClick={() => setIsPressed(!isPressed)}
>
{isPressed ? '🔇' : '🔊'}
</button>
// 진행 상태
<div
role="progressbar"
aria-valuenow={progress}
aria-valuemin={0}
aria-valuemax={100}
aria-label="업로드 진행률"
>
<div style={{ width: `${progress}%` }} />
</div>Focus Management
import { useEffect, useRef } from 'react';
function Modal({ isOpen, onClose }: Props) {
const closeBtnRef = useRef<HTMLButtonElement>(null);
const previousFocus = useRef<HTMLElement | null>(null);
useEffect(() => {
if (isOpen) {
previousFocus.current = document.activeElement as HTMLElement;
closeBtnRef.current?.focus();
} else {
previousFocus.current?.focus(); // 모달 닫고 원래 위치로
}
}, [isOpen]);
// ESC로 닫기
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
if (isOpen) document.addEventListener('keydown', handler);
return () => document.removeEventListener('keydown', handler);
}, [isOpen, onClose]);
if (!isOpen) return null;
return (
<FocusTrap>
<div role="dialog" aria-modal="true">
<button ref={closeBtnRef} onClick={onClose} aria-label="닫기">×</button>
{/* ... */}
</div>
</FocusTrap>
);
}Skip Link
// Tab 첫 번째 — 본문 바로가기
<a
href="#main-content"
className="
sr-only focus:not-sr-only
focus:absolute focus:top-4 focus:left-4
focus:z-50 focus:bg-white focus:px-4 focus:py-2
"
>
본문 바로가기
</a>
<main id="main-content">
{children}
</main>키보드 네비게이션
// 드롭다운 메뉴
function Menu() {
const [activeIndex, setActiveIndex] = useState(0);
const items = ['프로필', '설정', '로그아웃'];
const handleKey = (e: React.KeyboardEvent) => {
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
setActiveIndex(i => Math.min(i + 1, items.length - 1));
break;
case 'ArrowUp':
e.preventDefault();
setActiveIndex(i => Math.max(i - 1, 0));
break;
case 'Home': setActiveIndex(0); break;
case 'End': setActiveIndex(items.length - 1); break;
case 'Enter': handleSelect(items[activeIndex]); break;
}
};
return (
<ul role="menu" onKeyDown={handleKey}>
{items.map((item, i) => (
<li
key={item}
role="menuitem"
tabIndex={i === activeIndex ? 0 : -1}
aria-current={i === activeIndex}
>
{item}
</li>
))}
</ul>
);
}색상 대비 (WCAG AA)
일반 텍스트: 4.5:1
큰 텍스트(18pt+): 3:1
UI 컴포넌트: 3:1
// 검증
import { getContrast } from 'polished';
const ratio = getContrast('#6366f1', '#ffffff'); // 4.71 ✅
// Tailwind 안전 조합
text-gray-900 + bg-white → 통과
text-gray-500 + bg-white → 5.85 통과
text-gray-400 + bg-white → 3.94 ❌ (4.5 미만)다음 챕터
CH.26 "스토리북 디자인 시스템" — 컴포넌트 카탈로그.
AI 프롬프트
🤖 AI에게 잘 물어보는 법 — 모델·전략별 프롬프트
Claude
무료: Sonnet 4.6 / Pro $20/mo: Opus 4.6
내 코드의 웹 접근성 부분을 분석해서 실전 분석 + 개선 우선순위를 알려줘.
ChatGPT
무료: GPT-5.5 / Plus $20/mo: GPT-5.5 Pro
웹 접근성 관련 인기 라이브러리/패턴 5개를 비교 분석해서 패턴 추출를 알려줘.
Gemini
무료: 2.5 Flash / Pro $19.99/mo: 3.1 Pro
내 프로젝트 전체에서 웹 접근성 최적화 가능 위치를 보고해줘.
Grok
무료: Grok 4.1 / SuperGrok $30/mo
2026년 한국 프론트엔드 시장의 웹 접근성 트렌드를 솔직히 알려줘.
⭐ 이것만 기억하세요
접근성(a11y): WCAG 2.2 + ARIA는 이 3가지만 확실히 잡으세요
1.시맨틱 HTML 우선 — div 대신 button/nav/header 등
2.aria-* 보조 + role + focus management = 스크린리더 친화
3.키보드만으로 모든 기능 사용 가능 — Tab/화살표/ESC
공유하기
진행도 25 / 90