Skip to Content
FoundationsFoundationAccessibility (접근성)

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>

테스트 도구

키보드 네비게이션

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 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