접근성 감사 (Accessibility Audit)
이름: a11y-audit 티어: STANDARD 카테고리: 엔지니어링 - 프론트엔드 품질 의존성: Python 3.8+ (표준 라이브러리만 사용) 작성자: Alireza Rezvani 버전: 2.1.2 최종 업데이트: 2026-03-18 라이선스: MIT
이름
a11y-audit -- WCAG 2.2 접근성 감사 및 수정 스킬
설명
a11y-audit 스킬은 현대 웹 애플리케이션을 위한 완전한 접근성 감사 파이프라인을 제공합니다. 스캔(Scan), 수정(Fix), 검증(Verify)의 3단계 워크플로를 구현하여 WCAG 2.2 레벨 A 및 AA 위반 사항을 식별하고, 프레임워크별 정확한 수정 코드를 생성하며, 이해관계자에게 바로 제공할 수 있는 규정 준수 보고서를 생성합니다.
이 스킬은 단순한 감지를 뛰어넘습니다. 발견된 모든 위반에 대해 프레임워크(React, Next.js, Vue, Angular, Svelte 또는 일반 HTML)에 맞춤화된 정확한 before/after 수정 코드를 제공합니다. React JSX의 <img>에 누락된 alt 속성은 Vue SFC나 Angular 템플릿의 동일한 문제와 다른 수정 패턴이 필요하다는 점을 이해합니다.
이 스킬이 수행하는 작업:
- 스캔: 심각도(치명적, 주요, 경미)별로 분류된 모든 WCAG 2.2 레벨 A 및 AA 위반 사항에 대해 코드베이스를 스캔합니다.
- 수정: 각 위반에 대해 프레임워크별 before/after 코드 패턴을 제공합니다.
- 검증: 수정 사항이 원래 위반을 해결하고 회귀를 발생시키지 않는지 확인합니다.
- 보고: 개발자, PM 및 규정 준수 이해관계자에게 적합한 구조화된 형식으로 결과를 보고합니다.
- 통합: 접근성 회귀를 방지하기 위해 CI/CD 파이프라인에 통합합니다.
주요 차별점:
- 프레임워크 인식 수정 패턴 (일반적인 HTML 조언이 아님)
- 접근 가능한 대안 제안이 포함된 색상 대비 분석
- 최신 성공 기준(포커스 외형, 드래그 동작, 대상 크기)을 포함한 WCAG 2.2 범위
- GitHub Actions, GitLab CI 및 Azure DevOps를 통한 CI/CD 파이프라인 통합
/a11y-audit슬래시 명령어 지원
기능
핵심 기능
| 기능 | 설명 |
|---|---|
| 전체 WCAG 2.2 스캔 | 코드베이스의 모든 레벨 A 및 AA 성공 기준을 확인합니다. |
| 프레임워크 감지 | React, Next.js, Vue, Angular, Svelte 또는 일반 HTML을 자동 감지합니다. |
| 심각도 분류 | 각 위반을 치명적, 주요 또는 경미로 분류합니다. |
| 수정 코드 생성 | 모든 문제에 대해 before/after 코드 차이(diff)를 생성합니다. |
| 색상 대비 검사기 | AA 및 AAA 비율에 대해 전경/배경 쌍을 검증합니다. |
| 접근 가능한 대안 | 대비 요구 사항을 충족하는 대체 색상을 제안합니다. |
| 규정 준수 보고 | 통과/실패 요약이 포함된 이해관계자 보고서를 생성합니다. |
| CI/CD 통합 | GitHub Actions, GitLab CI, Azure DevOps 파이프라인 구성을 제공합니다. |
| 키보드 탐색 감사 | 누락된 포커스 관리 및 탭 순서 문제를 감지합니다. |
| ARIA 검증 | 잘못되었거나, 중복되거나, 누락된 ARIA 속성을 확인합니다. |
| 라이브 리전 감지 | aria-live 알림이 없는 동적 콘텐츠를 식별합니다. |
| 양식 접근성 | 레이블 연결, 오류 메시지 및 입력 유형을 검증합니다. |
WCAG 2.2 적용 범위 매트릭스
| 원칙 | 레벨 A 기준 | 레벨 AA 기준 |
|---|---|---|
| 인지 가능 | 1.1.1 비텍스트 콘텐츠, 1.2.1-1.2.3 시간 기반 미디어, 1.3.1-1.3.3 적응 가능, 1.4.1-1.4.2 구별 가능 | 1.3.4-1.3.5 적응 가능, 1.4.3-1.4.5 대비 및 텍스트 이미지, 1.4.10-1.4.13 리플로우 및 콘텐츠 |
| 운용 가능 | 2.1.1-2.1.2 키보드, 2.2.1-2.2.2 시간 제한, 2.3.1 발작, 2.4.1-2.4.4 탐색 가능, 2.5.1-2.5.4 입력 | 2.4.5-2.4.7 탐색 가능, 2.4.11 포커스 외형 (신규 2.2), 2.5.7 드래그 (신규 2.2), 2.5.8 대상 크기 (신규 2.2) |
| 이해 가능 | 3.1.1 언어, 3.2.1-3.2.2 예측 가능, 3.3.1-3.3.2 입력 지원 | 3.1.2 부분 언어, 3.2.3-3.2.4 예측 가능, 3.3.3-3.3.4 오류 처리, 3.3.7 중복 입력 (신규 2.2), 3.3.8 접근 가능한 인증 (신규 2.2) |
| 견고함 | 4.1.2 이름/역할/값 | 4.1.3 상태 메시지 |
심각도 정의
| 심각도 | 정의 | 예 | SLA |
|---|---|---|---|
| 치명적(Critical) | 전체 사용자 그룹의 접근을 차단 | 정보 용도 이미지에 누락된 대체 텍스트, 기본 탐색에 키보드 접근 불가 | 릴리스 전 수정 |
| 주요(Major) | 경험을 저하시키는 심각한 장벽 | 본문 텍스트의 불충분한 색상 대비, 누락된 양식 레이블 | 현재 스프린트 내 수정 |
| 경미(Minor) | 마찰을 유발하는 사용성 문제 | 중복된 ARIA 역할, 차선의 제목 계층 구조 | 다음 2개 스프린트 내 수정 |
사용법
빠른 시작
스킬을 활성화하고 프로젝트에서 감사를 실행합니다:
# 전체 프로젝트 스캔
python scripts/a11y_scanner.py /path/to/project
# 도구 사용을 위해 JSON 출력으로 스캔
python scripts/a11y_scanner.py /path/to/project --json
# 특정 값의 색상 대비 확인
python scripts/contrast_checker.py --fg "#777777" --bg "#ffffff"
# CSS/Tailwind 파일 전체의 대비 확인
python scripts/contrast_checker.py --file /path/to/styles.css
슬래시 명령어
대화형 감사 세션을 위해 /a11y-audit 슬래시 명령어를 사용하세요:
/a11y-audit # 현재 프로젝트 감사
/a11y-audit --scope src/ # 특정 디렉토리 감사
/a11y-audit --fix # 감사 및 자동 수정 적용
/a11y-audit --report # 이해관계자 보고서 생성
/a11y-audit --ci # CI 호환 결과 출력
3단계 워크플로
1단계: 스캔
스캐너는 소스 트리를 탐색하여 사용 중인 프레임워크를 감지하고 적절한 규칙 세트를 적용합니다.
python scripts/a11y_scanner.py /path/to/project --format table
샘플 출력:
A11Y 감사 보고서 - /path/to/project
감지된 프레임워크: React (Next.js)
스캔된 파일: 127
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
치명적 (3개 문제)
[1.1.1] src/components/Hero.tsx:14
<img> 요소에 대체 텍스트 누락
[2.1.1] src/components/Modal.tsx:8
모달 대화상자 내부에 포커스 고정되지 않음
[1.4.3] src/styles/globals.css:42
.subtitle의 대비율 2.8:1 (4.5:1 필요)
주요 (7개 문제)
[2.4.11] src/components/Button.tsx:22
포커스 표시기 보이지 않음 (2px 아웃라인 필요)
[1.3.1] src/components/Form.tsx:31
입력에 연결된 <label> 누락
...
경미 (4개 문제)
[1.3.1] src/components/Nav.tsx:5
<nav>에 중복된 role="navigation" 있음
...
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
요약: 14개 위반 (치명적 3개, 주요 7개, 경미 4개)
WCAG 2.2 레벨 A: 8개 문제
WCAG 2.2 레벨 AA: 6개 문제
2단계: 수정
각 위반에 대해 프레임워크별 수정을 적용합니다. 스킬은 모든 문제 유형에 대해 정확한 before/after 코드를 제공합니다.
전체 수정 카탈로그는 아래의 프레임워크별 수정 패턴 섹션을 참조하세요.
3단계: 검증
스캐너를 다시 실행하여 모든 수정 사항이 적용되었고 회귀가 발생하지 않았는지 확인합니다:
# 수정 후 재스캔
python scripts/a11y_scanner.py /path/to/project --format table
# 기준선과 비교
python scripts/a11y_scanner.py /path/to/project --baseline audit-baseline.json
검증 출력:
검증 결과
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
이전 스캔: 14개 위반 (치명적 3개, 주요 7개, 경미 4개)
현재 스캔: 2개 위반 (치명적 0개, 주요 1개, 경미 1개)
해결됨: 12개 위반
신규 문제: 0개 회귀
상태: 개선됨 (85.7% 감소)
예제
예제 1: React 컴포넌트 감사
여러 접근성 문제가 있는 React 컴포넌트가 있다고 가정합니다:
// 수정 전: src/components/ProductCard.tsx
function ProductCard({ product }) {
return (
<div onClick={() => navigate(`/product/${product.id}`)}>
<img src={product.image} />
<div style={{ color: '#aaa', fontSize: '12px' }}>
{product.name}
</div>
<span style={{ color: '#999' }}>${product.price}</span>
</div>
);
}
감지된 위반 사항:
| # | WCAG | 심각도 | 문제 |
|---|---|---|---|
| 1 | 1.1.1 | 치명적 | <img>에 alt 속성 누락 |
| 2 | 2.1.1 | 치명적 | <div onClick> 키보드 접근 불가 |
| 3 | 1.4.3 | 주요 | 흰색 배경의 #aaa 색상 대비 실패 (2.32:1, 4.5:1 필요) |
| 4 | 1.4.3 | 주요 | 흰색 배경의 #999 색상 대비 실패 (2.85:1, 4.5:1 필요) |
| 5 | 4.1.2 | 주요 | 대화형 요소에 역할 및 접근 가능한 이름 누락 |
// 수정 후: src/components/ProductCard.tsx
function ProductCard({ product }) {
return (
<a
href={`/product/${product.id}`}
className="product-card"
aria-label={`${product.name} - ${product.price}달러 보기`}
>
<img src={product.image} alt={product.imageAlt || product.name} />
<div style={{ color: '#595959', fontSize: '12px' }}>
{product.name}
</div>
<span style={{ color: '#767676' }}>${product.price}</span>
</a>
);
}
변경된 사항:
<div onClick>을<a href>로 대체하여 기본 키보드 및 스크린 리더 지원 제공<img>에alt속성 추가 및 의미 있는 대체 텍스트 제공aria-label이 보조 기술에 전체 컨텍스트 제공#aaa색상을#595959로 대체 (7.01:1 대비율 -- AA 및 AAA 통과)#999색상을#767676로 대체 (4.54:1 대비율 -- AA 통과)
예제 2: Vue SFC 양식 감사
<!-- 수정 전: src/components/LoginForm.vue -->
<template>
<form @submit="handleLogin">
<input type="text" placeholder="이메일" v-model="email" />
<input type="password" placeholder="비밀번호" v-model="password" />
<div v-if="error" style="color: red">{{ error }}</div>
<div @click="handleLogin">로그인</div>
</form>
</template>
감지된 위반 사항:
| # | WCAG | 심각도 | 문제 |
|---|---|---|---|
| 1 | 1.3.1 | 치명적 | 입력에 연결된 <label> 요소 누락 |
| 2 | 3.3.2 | 주요 | 플레이스홀더 텍스트만 레이블로 사용 (입력 시 사라짐) |
| 3 | 2.1.1 | 치명적 | <div @click> 키보드 접근 불가 |
| 4 | 4.1.3 | 주요 | 오류 메시지가 스크린 리더에 전달되지 않음 |
| 5 | 3.3.1 | 주요 | 오류가 입력에 프로그래밍 방식으로 연결되지 않음 |
<!-- 수정 후: src/components/LoginForm.vue -->
<template>
<form @submit.prevent="handleLogin" aria-label="계정에 로그인">
<div class="field">
<label for="login-email">이메일</label>
<input
id="login-email"
type="email"
v-model="email"
autocomplete="email"
required
:aria-describedby="emailError ? 'email-error' : undefined"
:aria-invalid="!!emailError"
/>
<span v-if="emailError" id="email-error" role="alert">
{{ emailError }}
</span>
</div>
<div class="field">
<label for="login-password">비밀번호</label>
<input
id="login-password"
type="password"
v-model="password"
autocomplete="current-password"
required
:aria-describedby="passwordError ? 'password-error' : undefined"
:aria-invalid="!!passwordError"
/>
<span v-if="passwordError" id="password-error" role="alert">
{{ passwordError }}
</span>
</div>
<div v-if="error" role="alert" aria-live="assertive" class="form-error">
{{ error }}
</div>
<button type="submit">로그인</button>
</form>
</template>
예제 3: Angular 템플릿 감사
<!-- 수정 전: src/app/dashboard/dashboard.component.html -->
<div class="tabs">
<div *ngFor="let tab of tabs"
(click)="selectTab(tab)"
[class.active]="tab.active">
{{ tab.label }}
</div>
</div>
<div class="tab-content">
<div *ngIf="selectedTab">{{ selectedTab.content }}</div>
</div>
감지된 위반 사항:
| # | WCAG | 심각도 | 문제 |
|---|---|---|---|
| 1 | 4.1.2 | 치명적 | 탭 위젯에 ARIA 역할 (tablist, tab, tabpanel) 누락 |
| 2 | 2.1.1 | 치명적 | 탭 키보드 탐색 불가 (화살표 키, Home, End) |
| 3 | 2.4.11 | 주요 | 활성 탭에 보이는 포커스 표시기 없음 |
<!-- 수정 후: src/app/dashboard/dashboard.component.html -->
<div class="tabs" role="tablist" aria-label="대시보드 섹션">
<button
*ngFor="let tab of tabs; let i = index"
role="tab"
[id]="'tab-' + tab.id"
[attr.aria-selected]="tab.active"
[attr.aria-controls]="'panel-' + tab.id"
[attr.tabindex]="tab.active ? 0 : -1"
(click)="selectTab(tab)"
(keydown)="handleTabKeydown($event, i)"
class="tab-button"
[class.active]="tab.active">
{{ tab.label }}
</button>
</div>
<div
*ngIf="selectedTab"
role="tabpanel"
[id]="'panel-' + selectedTab.id"
[attr.aria-labelledby]="'tab-' + selectedTab.id"
tabindex="0"
class="tab-content">
{{ selectedTab.content }}
</div>
키보드 탐색을 위한 지원 TypeScript:
// dashboard.component.ts
handleTabKeydown(event: KeyboardEvent, index: number): void {
const tabCount = this.tabs.length;
let newIndex = index;
switch (event.key) {
case 'ArrowRight':
newIndex = (index + 1) % tabCount;
break;
case 'ArrowLeft':
newIndex = (index - 1 + tabCount) % tabCount;
break;
case 'Home':
newIndex = 0;
break;
case 'End':
newIndex = tabCount - 1;
break;
default:
return;
}
event.preventDefault();
this.selectTab(this.tabs[newIndex]);
// 새 탭 버튼으로 포커스 이동
const tabElement = document.getElementById(`tab-${this.tabs[newIndex].id}`);
tabElement?.focus();
}
예제 4: Next.js 페이지 레벨 감사
// 수정 전: src/app/page.tsx
export default function Home() {
return (
<main>
<div className="text-4xl font-bold">Acme에 오신 것을 환영합니다</div>
<div className="mt-4">
저희 플랫폼으로 더 나은 제품을 만드세요.
</div>
<div className="mt-8 bg-blue-600 text-white px-6 py-3 rounded cursor-pointer"
onClick={() => router.push('/signup')}>
시작하기
</div>
</main>
);
}
감지된 위반 사항:
| # | WCAG | 심각도 | 문제 |
|---|---|---|---|
| 1 | 1.1.1 | 치명적 | 페이지에 주요 이미지/아이콘에 대한 대체 텍스트 누락 |
| 2 | 2.1.1 | 치명적 | "시작하기" <div>가 <a> 또는 <button>이 아니므로 키보드 접근 불가 |
| 3 | 1.4.1 | 주요 | 색상만으로 링크를 구분 (색맹 사용자에게 문제) |
| 4 | 2.4.1 | 경미 | 페이지 제목이 없거나 설명적이지 않음 |
// 수정 후: src/app/page.tsx
export default function Home() {
return (
<main>
<h1 className="text-4xl font-bold">Acme에 오신 것을 환영합니다</h1>
<p className="mt-4">
저희 플랫폼으로 더 나은 제품을 만드세요.
</p>
<a
href="/signup"
className="inline-block mt-8 bg-blue-600 text-white px-6 py-3 rounded focus:outline-2 focus:outline-offset-2 focus:outline-blue-600"
>
시작하기
</a>
</main>
);
}
구성
CI/CD 파이프라인 통합
GitHub Actions
.github/workflows/a11y-audit.yml:
name: 접근성 감사
on:
pull_request:
branches: [main, develop]
push:
branches: [main]
jobs:
a11y-audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Python 설정
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: 접근성 감사 실행
run: |
python scripts/a11y_scanner.py . --json --output a11y-report.json
- name: 보고서 업로드
uses: actions/upload-artifact@v4
with:
name: a11y-report
path: a11y-report.json
- name: 위반 사항 확인
run: |
python -c "
import json
with open('a11y-report.json') as f:
report = json.load(f)
if report['summary']['critical'] > 0:
print('치명적 위반 사항이 발견되었습니다. 검토하세요.')
exit(1)
"
GitLab CI
.gitlab-ci.yml:
a11y-audit:
stage: quality
image: python:3.11-slim
script:
- python scripts/a11y_scanner.py . --json --output a11y-report.json
- |
python -c "
import json
with open('a11y-report.json') as f:
report = json.load(f)
if report['summary']['critical'] > 0:
print('치명적 위반 사항이 발견되었습니다. 검토하세요.')
exit(1)
"
artifacts:
paths:
- a11y-report.json
when: always
only:
- main
- merge_requests
Azure DevOps
azure-pipelines.yml:
trigger:
- main
pool:
vmImage: ubuntu-latest
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.11'
- script: |
python scripts/a11y_scanner.py . --json --output $(Build.ArtifactStagingDirectory)/a11y-report.json
displayName: '접근성 감사 실행'
- task: PublishBuildArtifacts@1
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)/a11y-report.json'
ArtifactName: 'a11y-report'
프레임워크별 수정 패턴
React / Next.js 패턴
| 위반 | 수정 전 | 수정 후 |
|---|---|---|
| 이미지에 누락된 alt | <img src={src} /> | <img src={src} alt={altText} /> |
| 비키보드 클릭 | <div onClick={fn}> | <button onClick={fn}> 또는 <a href={url}> |
| 낮은 색상 대비 | color: '#aaa' | color: '#595959' (7.01:1) |
| 누락된 양식 레이블 | <input placeholder="..." /> | <label htmlFor="id">...</label><input id="id" /> |
| 잘못된 ARIA | <div role="button"> | <button> 또는 <div role="button" tabIndex={0} onKeyDown={...}> |
Vue 패턴
| 위반 | 수정 전 | 수정 후 |
|---|---|---|
| 누락된 양식 레이블 | <input placeholder="..."> | <label for="id">...</label><input id="id"> |
| 비키보드 클릭 | <div @click="fn"> | <button @click="fn"> 또는 <router-link> |
| 오류 없음 | <div v-if="error">{{ error }}</div> | <div v-if="error" role="alert">{{ error }}</div> |
| 라이브 리전 누락 | 동적 콘텐츠 업데이트 | aria-live="polite"로 래핑 |
| 잘못된 헤딩 순서 | 레벨 건너뛰기 <h2> → <h4> | <h2> → <h3>으로 수정 |
Angular 패턴
| 위반 | 수정 전 | 수정 후 |
|---|---|---|
| ARIA 탭 누락 | <div *ngFor="let tab of tabs" (click)="selectTab(tab)">{{tab.label}}</div> | <button *ngFor="..." role="tab" [attr.aria-selected]="tab.active" (click)="selectTab(tab)" (keydown)="handleKeydown($event, i)">{{tab.label}}</button> |
| 이미지 alt 누락 | <img [src]="src"> | <img [src]="src" [alt]="altText"> |
| 양식 레이블 누락 | <input [(ngModel)]="val"> | <label for="id">...</label><input id="id" [(ngModel)]="val"> |
| 포커스 표시기 없음 | <button class="tab"> | <button class="tab" cdkFocusInitial> 또는 사용자 정의 포커스 스타일 |
Svelte 패턴
| 위반 | 수정 전 | 수정 후 |
|---|---|---|
| 이미지 alt 누락 | <img src={src}> | <img src={src} alt={altText}> |
| 비키보드 엘리먼트 | <div on:click={fn}> | <button on:click={fn}> 또는 <a href={url}> |
| 라이브 리전 누락 | {:then value} | <div aria-live="polite">{value}</div> |
| 포커스 관리 | 모달/드롭다운 누락 | use:focusTrap 액션 사용 |
문제 해결
일반적인 문제
| 문제 | 해결 방법 |
|---|---|
Python 3.8+가 필요합니다 | Python --version으로 버전 확인. Python.org에서 업데이트 |
프레임워크를 감지할 수 없음 | 프로젝트 루트에 package.json, angular.json, svelte.config.js, 또는 vue.config.js가 있는지 확인 |
스캔 시간이 너무 김 | --scope 또는 --exclude로 범위를 좁힘 |
CI에서 가져오기 오류 | CI 작업 환경에 스크립트가 설치되었는지 확인 |
거짓 양성 위반 | --baseline 파일로 예외를 관리 |
스캔 제외
# 테스트 파일 제외
python scripts/a11y_scanner.py . --exclude "**/test/**"
# 타사 라이브러리 제외
python scripts/a11y_scanner.py . --exclude "node_modules/**,dist/**"
# 여러 디렉토리 제외
python scripts/a11y_scanner.py . --exclude "**/*.test.*,**/*.spec.*"
추가 리소스
라이선스
MIT