프론트엔드 접근성
<ui-packages-dir> 디렉토리의 컴포넌트와 <web-dir>/src/ 디렉토리의 페이지에 적용됩니다. 목표는 WCAG 2.1 Level AA이며, 이는 최소 기준이지 목표가 아닙니다.
프로젝트 규칙 확인
꺾쇠 괄호 안의 값은 프로젝트의 스택 프로파일에서 가져옵니다. 다음 순서로 확인하세요.
- 리포지토리 루트에 있는
STACK-PROFILE.md를 읽고 값을 그대로 사용합니다. - 파일이 없거나 키가 누락된 경우, 리포지토리에서 추론합니다 —
css-strategy와ui-framework는 웹 애플리케이션의 종속성에서 가져옵니다. - 여전히 해결되지 않은 경우, 리포지토리에 이미 존재하는 지배적인 규칙을 따릅니다.
- 리포지토리가 비어 있는 경우에만 이 스킬의 기본값을 사용합니다. 그런 다음 선택한 값을
STACK-PROFILE.md에 기록합니다.
리포지토리에 이미 규칙이 있는 경우 새로운 규칙을 절대 만들지 마세요.
엄격한 규칙
1. 의미론적 HTML 우선
<button>은 동작,<a>는 탐색에 사용합니다.<div>에 클릭 핸들러를 절대 사용하지 마세요.- 제목 수준은 건너뛰지 않고 내려갑니다.
- 모든 폼 필드에
<label>을 사용하고htmlFor로 연결합니다. - 그룹화된 컨트롤에는
<fieldset>과<legend>를 사용합니다. - 모든 페이지에
<nav>,<main>,<footer>,<aside>와 같은 랜드마크를 사용합니다.
2. HTML로 해결할 수 없는 경우에만 ARIA 사용
선호 순서는 의미론적 요소, 그다음 ARIA 속성, 마지막으로 사용자 정의 동작입니다. 이미 요소가 가지고 있는 의미를 덮어쓰기 위해 ARIA를 절대 사용하지 마세요. <div>에 role="button"을 사용하는 것은 <button>이 무료로 제공하는 것을 나쁘게 재구현하는 것입니다.
잘못된 ARIA는 없는 것보다 worse합니다. 잘못된 aria-label은 스크린 리더가 올바르게 계산했을 접근 가능한 이름을 조용히 대체합니다.
3. 키보드 동작은 필수
- 모든 대화형 요소는 Tab 키로 접근 가능해야 합니다.
- Tab 순서는 시각적 순서를 따릅니다.
- 포커스는 항상 표시되어야 합니다. 대체 수단 없이 아웃라인을 제거하지 마세요.
Escape는 대화상자와 팝오버를 닫습니다.Enter와Space는 컨트롤을 활성화합니다. 화살표 키는 복합 위젯 내에서 이동합니다.- 사용자 정의 위젯은 포커스를 명시적으로 관리합니다.
4. 색상과 대비
- 일반 텍스트는 4.5:1, 큰 텍스트(18px 또는 14px 굵게)와 의미 있는 UI 경계는 3:1을 유지합니다.
- 색상만으로 의미를 전달하지 마세요. 텍스트, 아이콘 또는 패턴과 함께 사용합니다.
- 기본 테마뿐만 아니라 두 테마 모두 확인합니다.
5. 이미지와 미디어
- 모든 이미지에
alt속성이 있어야 합니다. 콘텐츠 이미지는 설명을, 장식 이미지는alt=""를 사용합니다. - "이미지", "사진" 또는 파일 이름을 절대 사용하지 마세요.
- 복잡한 이미지는
aria-describedby또는 캡션을 통해 긴 설명을 제공합니다. - 동영상에는 자막이, 오디오에는 대본이 있어야 합니다.
6. 폼
- 모든 입력에 레이블이 있어야 합니다.
- 오류는
aria-describedby로 연결하고role="alert"로 알립니다. - 유효하지 않은 필드는
aria-invalid로 표시합니다. - 필수 필드는
required속성으로 표시합니다. 별표(*)를 표시하는 경우 그 의미가 어딘가에 텍스트로 명시되어야 합니다. - 제출 실패 시 포커스가 첫 번째 유효하지 않은 필드로 이동합니다.
7. 대상 크기
WCAG 2.1 AA에는 최소 대상 크기가 없습니다. (2.2 AA는 24×24 CSS px로 설정합니다. 44×44 수치는 2.1 AAA 및 플랫폼 HIG 규칙입니다.) 터치 표면의 기본 작업에 대해 44×44를 최소 기준으로 간주하고 다음과 같이 적용합니다.
- 기본 및 파괴적 작업, 그리고 휴대폰에서 사용자가 손을 뻗는 모든 것: 44×44.
- 조밀한 데스크톱 크롬(툴바, 테이블 행 작업, 컴팩트 폼 컨트롤)은 24×24까지 작아질 수 있습니다. 단, 대상이 다른 대상과 인접하지 않고 동등한 전체 크기 컨트롤이 다른 곳에 있어야 합니다.
- 간격도 중요합니다. 32px 컨트롤에 각 측면에 6px의 빈 공간이 있으면 2.2 기준을 충족하지만, 이웃과 맞닿은 32px 컨트롤은 충족하지 않습니다.
공급업체 컴포넌트 라이브러리는 일반적으로 이 기준 미만으로 제공됩니다. — 기본 버튼 높이가 32 또는 36 CSS px인 것이 일반적입니다. 공급업체 파일을 직접 수정하여 해결하지 말고, 불일치를 기본적으로 허용 가능한 것으로 간주하지 마세요. <wrappers-pkg>에서 기본값을 높여 모든 소비자가 규격을 준수하는 크기를 상속받도록 하고, 조밀한 컨텍스트를 위한 명시적 옵트인으로 더 작은 크기를 유지하세요. 이렇게 하면 접근 가능한 선택이 생각하지 않고 얻는 기본값이 됩니다.
프로젝트에서 어떤 해결 방안을 취했는지 STACK-PROFILE.md에 기록하세요. 명시되지 않은 면제는 6개월마다 버그로 재발견됩니다.
8. 모션
prefers-reduced-motion을 존중합니다.- 자동 재생 콘텐츠에는 일시 중지 컨트롤이 있어야 합니다.
- 초당 3회 이상 깜빡이지 않습니다.
9. 테스트는 작업의 일부입니다.
- 키보드만으로 기능을 작동시켜 보세요.
- 스크린 리더로 들어보세요. macOS에서는 VoiceOver, Windows에서는 NVDA를 사용합니다.
- 엔드투엔드 스위트에서 자동화된 스캔을 실행하세요.
자동화 도구는 실제 문제의 약 1/3만 발견합니다. 이는 최소 기준이지 합격 기준이 아닙니다.
패턴
전체 구현은 references/component-patterns.md에 있습니다. 각 패턴이 보장하는 것은 다음과 같습니다.
- 버튼 — 포커스 링이 키보드 사용자에게는 표시되지만 마우스 클릭 시에는 표시되지 않는(
focus-visible) 의미론적<button>, 모션 안전 전환, 아이콘 전용 사용을 위한ariaLabelprop. - 폼 입력 — 오류 텍스트가
aria-describedby로 프로그래밍 방식으로 연결되고,role="alert"로 알려지며,aria-invalid로 표시된 레이블이 있는 입력; 필수 별표는aria-hidden입니다. - 대화상자 — 네이티브
<dialog>를showModal()과 함께 사용하여 무료 포커스 트래핑,Escape처리, 비활성 배경 제공; 열릴 때 포커스가 안으로 이동하고 닫힐 때 트리거로 복원됩니다. - 리스트박스 — 로빙 화살표 키 탐색과 올바른 ARIA 연결(
aria-activedescendant,role="listbox")을 갖춘 사용자 정의 선택; 네이티브<select>가 적합하지 않을 때만 구축합니다. - 스킵 링크 — 키보드 사용자가 탐색을 건너뛸 수 있게 하며, 포커스될 때만 표시됨; 대상은
tabIndex={-1}을 가지므로 포커스가 실제로 그곳에 위치합니다.
WCAG 빠른 참조
| 원칙 | 요구 사항 | 실제 적용 |
|---|---|---|
| 지각 가능 | 모든 감각으로 콘텐츠 이용 가능 | 대체 텍스트, 자막, 색상 + 아이콘 |
| 작동 가능 | 모든 것을 키보드로 접근 가능 | Tab 순서, Escape로 닫기, 포커스 표시 |
| 이해 가능 | 예측 가능하고 명확하게 표현 | 일관된 탐색, 평이한 언어 |
| 견고함 | 보조 기술과 함께 작동 | 유효한 HTML, 올바른 ARIA, 의미론적 요소 |
| 문제 | 해결 방법 |
|---|---|
| 레이블이 없는 입력 | <label htmlFor> 추가 |
| 낮은 대비 | 일반 텍스트 4.5:1, 큰 텍스트 3:1 충족 |
| 키보드 경로 없음 | 실제 대화형 요소, 포커스 관리 |
| 대체 텍스트 누락 | 설명, 또는 장식용이면 alt="" |
| 색상만으로 의미 전달 | 텍스트 또는 아이콘 추가 |
| 아이콘 전용 버튼 | aria-label 추가 |
| 깜빡이는 콘텐츠 | 초당 3회 미만으로 유지 |
모션 환경 설정
순수 CSS로 축소 모션을 처리하세요. motion-safe:와 같은 유틸리티 클래스 변형은 모션이 허용될 때만 적용되므로, 축소 모션 쿼리 내부에 배치하면 절대 일치할 수 없는 규칙이 생성됩니다.
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
0에 가까운 값(none 대신)은 전환 종료 이벤트가 계속 발생하도록 하여, 이를 기다리는 컴포넌트가 멈추지 않도록 합니다.
컴포넌트별 제어를 위해, 애니메이션 유틸리티 자체에 motion-safe: 변형을 적용하세요. 위 버튼 예제의 motion-safe:transition-colors와 같이.
시각적으로 숨겨진 콘텐츠
대부분의 유틸리티 프레임워크는 이미 sr-only와 not-sr-only를 제공합니다. 그것들을 사용하고, 다시 정의하지 마세요. 프레임워크에 없다면 참조 구현이 references/component-patterns.md에 있습니다.
테스트 체크리스트
키보드
- 모든 대화형 요소가 Tab으로 접근 가능
- Tab 순서가 시각적 순서와 일치
- 포커스가 항상 표시됨
- Escape로 대화상자와 팝오버가 닫힘
- 복합 위젯에서 화살표 키가 작동
- 대화상자가 닫힐 때 포커스가 트리거로 돌아감
스크린 리더
- 모든 콘텐츠에 접근 가능
- 컨트롤의 목적이 주변 컨텍스트 없이 명확함
- 레이블이 해당 필드와 함께 알려짐
-
role="alert"를 통해 오류가 알려짐 - 랜드마크가 존재함
- 장식 요소가
aria-hidden으로 숨겨짐
색상
- 일반 텍스트 4.5:1, 큰 텍스트 및 UI 경계 3:1
- 색상만으로 의미 전달하지 않음
- 두 테마 모두 확인됨
모션
- 축소 모션이 존중됨
- 초당 3회 이상 깜빡이지 않음
- 자동 재생 콘텐츠를 일시 중지 가능
자동화
스캔은 Playwright 스위트 내에서 실행됩니다. 자세한 내용은 frontend-testing을 참조하세요. Axe-in-Playwright 코드 조각은 references/component-patterns.md에 있습니다.
참조 문서
| 심층 분석 | 참조 |
|---|---|
| 접근 가능한 컴포넌트 구현: 버튼, 폼 입력, 모달, 리스트박스, 스킵 링크 | references/component-patterns.md |
관련 스킬
frontend-architecture— 컴포넌트 계층화 및 테마 설정frontend-testing— 이러한 검사가 실행되는 곳frontend-component-docs— 컴포넌트의 접근성 계약 문서화