Accessibility (접근성)
Foundation은 WCAG 2.1 AA 기준을 준수하여 모든 사용자가 접근 가능한 UI를 구축할 수 있도록 설계되었습니다.
접근성 철학
Foundation의 접근성은 다음 원칙을 따릅니다:
- Inclusive by Default: 기본적으로 모든 사용자를 포함
- WCAG 2.1 AA: 국제 웹 접근성 표준 준수
- Semantic HTML: 의미있는 HTML 요소 사용
- Keyboard Navigation: 키보드만으로 모든 기능 접근 가능
WCAG 2.1 AA 기준
POUR 원칙
| 원칙 | 설명 |
|---|---|
| Perceivable | 사용자가 정보를 인지할 수 있어야 함 |
| Operable | 사용자가 UI 컴포넌트를 조작할 수 있어야 함 |
| Understandable | 정보와 UI 조작이 이해 가능해야 함 |
| Robust | 다양한 기술(보조 기술 포함)로 접근 가능해야 함 |
색상 대비 (Color Contrast)
WCAG 2.1 AA 요구사항
| 콘텐츠 유형 | 최소 대비율 | Foundation |
|---|---|---|
| 일반 텍스트 | 4.5:1 | ✅ 준수 |
| 큰 텍스트 (18px+) | 3:1 | ✅ 준수 |
| UI 요소 | 3:1 | ✅ 준수 |
권장 색상 조합
// ✅ Good: 19.6:1 대비 (AAA)
<p className="text-gray-900 bg-white">
High contrast text
</p>
// ✅ Good: 4.7:1 대비 (AA)
<p className="text-gray-500 bg-white">
Sufficient contrast
</p>
// ❌ Bad: 2.1:1 대비 (FAIL)
<p className="text-gray-300 bg-white">
Insufficient contrast
</p>테스트 도구
- WebAIM Contrast Checker
- Stark
- Chrome DevTools: Lighthouse Accessibility Audit
키보드 네비게이션
Focus Indicator
모든 인터랙티브 요소에는 명확한 Focus Indicator가 필요합니다.
<button
className="
px-md py-sm bg-primary text-white rounded
focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2
"
>
Accessible Button
</button>Tab 순서
논리적인 Tab 순서를 유지하세요.
// ✅ Good: 자연스러운 DOM 순서
<div>
<input tabIndex={0} />
<button tabIndex={0}>Submit</button>
</div>
// ❌ Bad: tabIndex로 순서 변경
<div>
<button tabIndex={2}>Submit</button>
<input tabIndex={1} />
</div>Skip Links
페이지 상단에 Skip Link를 추가하세요.
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:absolute focus:top-md focus:left-md focus:z-50 focus:px-md focus:py-sm focus:bg-primary focus:text-white"
>
Skip to main content
</a>ARIA (Accessible Rich Internet Applications)
ARIA Labels
의미있는 레이블을 제공하세요.
import { Trash2 } from 'lucide-react'
// ✅ Good: aria-label 제공
<button aria-label="Delete item">
<Trash2 size={20} />
</button>
// ❌ Bad: 레이블 없음
<button>
<Trash2 size={20} />
</button>ARIA Live Regions
동적 콘텐츠 변경을 알려주세요.
export default function Toast() {
return (
<div
role="alert"
aria-live="polite"
className="p-md bg-green-50 border-green-200 rounded"
>
<p>Operation completed successfully</p>
</div>
);
}ARIA States
인터랙티브 요소의 상태를 표시하세요.
export default function Dropdown({ isOpen }) {
return (
<div>
<button aria-expanded={isOpen} aria-haspopup="true">
Open Menu
</button>
{isOpen && (
<div role="menu">
<a role="menuitem" href="#">
Item 1
</a>
<a role="menuitem" href="#">
Item 2
</a>
</div>
)}
</div>
);
}스크린 리더 지원
Semantic HTML
의미있는 HTML 요소를 사용하세요.
// ✅ Good: Semantic HTML
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>
// ❌ Bad: Div Soup
<div className="navigation">
<div className="menu">
<div className="item">
<div className="link">Home</div>
</div>
</div>
</div>Heading Hierarchy
올바른 Heading 계층을 유지하세요.
// ✅ Good: 논리적 계층
<h1>Page Title</h1>
<h2>Section Title</h2>
<h3>Subsection Title</h3>
// ❌ Bad: 계층 건너뛰기
<h1>Page Title</h1>
<h3>Subsection</h3>sr-only 유틸리티
시각적으로 숨기되 스크린 리더에는 표시하세요.
// Tailwind sr-only 클래스
<span className="sr-only">Loading...</span>
// 또는 CSS로 정의
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}Form 접근성
Label 연결
모든 Input에 Label을 연결하세요.
// ✅ Good: for/id 연결
<div>
<label htmlFor="email" className="block mb-xs">Email</label>
<input id="email" type="email" className="px-md py-sm border rounded" />
</div>
// ❌ Bad: Label 없음
<input type="email" placeholder="Email" />Error Messages
에러 메시지를 명확하게 전달하세요.
export default function Form() {
const [error, setError] = useState("");
return (
<div>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
aria-invalid={error ? "true" : "false"}
aria-describedby={error ? "email-error" : undefined}
className={error ? "border-red-500" : "border-gray-300"}
/>
{error && (
<p id="email-error" className="text-sm text-red-600 mt-xs" role="alert">
{error}
</p>
)}
</div>
);
}Required Fields
필수 필드를 표시하세요.
<div>
<label htmlFor="name">
Name{" "}
<span className="text-red-500" aria-label="required">
*
</span>
</label>
<input id="name" required aria-required="true" />
</div>이미지 접근성
Alt Text
의미있는 Alt 텍스트를 제공하세요.
// ✅ Good: 설명적인 alt
<img src="/product.jpg" alt="Blue ceramic coffee mug with handle" />
// ❌ Bad: 불명확한 alt
<img src="/product.jpg" alt="image1" />
// ✅ Good: 장식용 이미지 (alt 빈 문자열)
<img src="/decoration.jpg" alt="" aria-hidden="true" />모바일 접근성
Touch Target Size
터치 타겟은 최소 44x44px이어야 합니다.
<button className="min-h-[44px] min-w-[44px] px-md py-sm">
Touch Friendly
</button>Zoom
브라우저 확대/축소를 비활성화하지 마세요.
<!-- ✅ Good -->
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- ❌ Bad -->
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
/>실전 예제
Accessible Modal
export default function Modal({ isOpen, onClose, title, children }) {
return (
<>
{isOpen && (
<div
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
className="fixed inset-0 z-50"
>
<div
className="fixed inset-0 bg-black bg-opacity-50"
onClick={onClose}
/>
<div className="fixed inset-0 flex items-center justify-center p-md">
<div className="bg-white rounded-lg shadow-xl max-w-md w-full p-lg">
<h2 id="modal-title" className="text-2xl font-bold mb-md">
{title}
</h2>
<div>{children}</div>
<button
onClick={onClose}
className="mt-lg px-md py-sm bg-gray-100 rounded focus:ring-2 focus:ring-primary"
>
Close
</button>
</div>
</div>
</div>
)}
</>
);
}Accessible Tabs
export default function Tabs() {
const [activeTab, setActiveTab] = useState(0);
const tabs = ["Tab 1", "Tab 2", "Tab 3"];
return (
<div>
<div role="tablist" className="flex gap-sm border-b">
{tabs.map((tab, index) => (
<button
key={index}
role="tab"
aria-selected={activeTab === index}
aria-controls={`panel-${index}`}
id={`tab-${index}`}
onClick={() => setActiveTab(index)}
className={
activeTab === index ? "border-primary" : "border-transparent"
}
>
{tab}
</button>
))}
</div>
{tabs.map((tab, index) => (
<div
key={index}
role="tabpanel"
id={`panel-${index}`}
aria-labelledby={`tab-${index}`}
hidden={activeTab !== index}
className="mt-md"
>
{tab} Content
</div>
))}
</div>
);
}테스트 도구
자동화 도구
- Lighthouse: Chrome DevTools 내장
- axe DevTools: Chrome/Firefox Extension
- WAVE: 웹 접근성 평가 도구
수동 테스트
- 키보드만으로 네비게이션
- 스크린 리더 테스트 (NVDA, JAWS, VoiceOver)
- 색상 대비 검사
- 브라우저 확대/축소 (200%)
다음 단계
Last updated on