본문으로 건너뛰기

Cursor에서 웹 접근성 (a11y): WCAG, ARIA 및 axe-core

Cursor에서 웹 접근성

웹 접근성(a11y)은 장애가 있는 사람을 포함한 모든 사람이 애플리케이션을 인식, 이해 및 상호 작용할 수 있도록 보장합니다. AI 기반 IDE 에디터인 Cursor는 ARIA 준수 마크업을 생성하고, 자동화된 감사를 실행하며, 코드베이스 전체에 WCAG 수정을 적용하여 접근 가능한 개발을 가속화합니다. 이 가이드는 ARIA 레이블, WCAG 준수, axe-core 테스트, AI 지원 a11y 수정 및 스크린 리더 테스트를 다룹니다.

사전 요구 사항​

시작하기 전에 다음이 있는지 확인하세요:

  • Cursor(최신 버전 권장)를 IDE 에디터로 사용
  • 웹 프로젝트(React, Vue 또는 일반 HTML 및 JavaScript)
  • 테스트 도구용 Node.js 18 이상
  • HTML 시맨틱에 대한 기본 지식
팁

접근성은 나중에 개조하는 것보다 초기에 구축하는 것이 훨씬 저렴합니다. 처음부터 Cursor에서 프로젝트를 열고 작성하면서 AI가 컴포넌트를 감사하도록 하세요. 개발 중 누락된 레이블을 수정하는 데는 몇 초가 걸리지만, 프로덕션 앱에서 동일한 문제를 수정하는 데는 몇 주가 걸립니다.

WCAG 준수 이해​

Web Content Accessibility Guidelines(WCAG)는 웹 콘텐츠를 접근 가능하게 만드는 방법을 정의합니다. 네 가지 원칙을 중심으로 구성되어 있으며, 종종 POUR라고 합니다:

  • 인식 가능(Perceivable): 정보와 사용자 인터페이스 컴포넌트는 사용자가 인식할 수 있는 방식으로 제공되어야 합니다.
  • 운용 가능(Operable): 사용자 인터페이스 컴포넌트와 탐색은 운용 가능해야 합니다.
  • 이해 가능(Understandable): 콘텐츠와 사용자 인터페이스의 운용은 이해 가능해야 합니다.
  • 견고함(Robust): 콘텐츠는 보조 기술이 해석할 수 있을 만큼 견고해야 합니다.

WCAG는 세 가지 적합 수준을 정의합니다: A(최소), AA(대부분의 조직에 대한 법적 목표), AAA(강화). Cursor에게 특정 기능에 대한 AA 수준 요구 사항을 요약해 달라고 요청하세요:

날짜 선택기와 신용카드 입력이 있는 다단계 체크아웃 양식에
적용되는 WCAG 2.2 레벨 AA 성공 기준을 나열해주세요.

Cursor는 관련 기준을 반환하고 각각을 구체적인 코드 제안으로 충족하는 방법을 설명합니다.

시맨틱 HTML 기초​

시맨틱 HTML은 접근성의 기초입니다. 올바른 요소는 종종 ARIA의 필요성을 완전히 제거합니다. Cursor는 올바른 마크업을 선택하고 생성하는 데 도움을 줍니다.

시맨틱 컴포넌트 생성​

Cursor에게 접근성이 내장된 탐색 모음을 만들어 달라고 요청하세요:

다음과 같은 반응형 사이트 탐색을 React로 만들어주세요:
- 접근 가능한 이름이 있는 <nav> 랜드마크 사용
- 페이지 상단에 스킵 링크
- aria-expanded 및 aria-controls로 모바일 메뉴 토글
- Escape로 닫고 토글 버튼으로 포커스 반환
- 전체 키보드 탐색 지원

Cursor가 컴포넌트를 생성합니다:

import { useState, useRef, useEffect } from 'react';

export function SiteNav({ links }: { links: { href: string; label: string }[] }) {
const [open, setOpen] = useState(false);
const toggleRef = useRef<HTMLButtonElement>(null);

useEffect(() => {
if (!open) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
setOpen(false);
toggleRef.current?.focus();
}
};
document.addEventListener('keydown', onKey);
return () => document.removeEventListener('keydown', onKey);
}, [open]);

return (
<>
<a href="#main" className="skip-link">메인 콘텐츠로 건너뛰기</a>
<nav aria-label="주요 탐색">
<button
ref={toggleRef}
aria-expanded={open}
aria-controls="mobile-menu"
onClick={() => setOpen((v) => !v)}
>
메뉴
</button>
<ul id="mobile-menu" hidden={!open}>
{links.map((link) => (
<li key={link.href}>
<a href={link.href}>{link.label}</a>
</li>
))}
</ul>
</nav>
</>
);
}
팁

ARIA의 첫 번째 규칙은: 네이티브 HTML 요소가 존재하면 ARIA를 사용하지 마세요. 진정한 <button>은 키보드 포커스, Enter 및 Space 활성화, 스크린 리더 시맨틱을 무료로 제공합니다. 비대화형 요소에 onClick이 보일 때마다 Cursor에게 "이 div 기반 버튼을 시맨틱 버튼으로 교체해 달라"고 요청하세요.

ARIA 레이블 및 역할​

네이티브 HTML이 필요한 시맨틱을 표현할 수 없을 때, ARIA가 그 간극을 채웁니다. Cursor는 올바른 ARIA 속성을 생성하고 일반적인 실수에 대해 경고합니다.

접근 가능한 양식​

양식 입력에는 레이블이 필요합니다. Cursor에게 접근 가능한 양식 필드를 생성해 달라고 요청하세요:

다음과 같은 재사용 가능한 FormField 컴포넌트를 만들어주세요:
- htmlFor를 사용하는 연결된 <label>
- 필수일 때 aria-required 속성
- 도움말 및 오류 텍스트에 대한 aria-describedby 링크
- role="alert"가 있는 오류 메시지
import { useId } from 'react';

interface FormFieldProps {
label: string;
name: string;
type?: string;
required?: boolean;
error?: string;
helper?: string;
}

export function FormField({ label, name, type = 'text', required, error, helper }: FormFieldProps) {
const fieldId = useId();
const helperId = `${fieldId}-helper`;
const errorId = `${fieldId}-error`;
const describedBy = [helper && helperId, error && errorId].filter(Boolean).join(' ') || undefined;

return (
<div>
<label htmlFor={fieldId}>
{label}
{required && <span aria-hidden="true">*</span>}
</label>
<input
id={fieldId}
name={name}
type={type}
required={required}
aria-required={required}
aria-describedby={describedBy}
aria-invalid={!!error}
/>
{helper && !error && (
<p id={helperId} className="helper-text">{helper}</p>
)}
{error && (
<p id={errorId} role="alert" className="error-text">{error}</p>
)}
</div>
);
}

동적 업데이트를 위한 라이브 영역​

콘텐츠가 동적으로 업데이트될 때(알림, 검색 결과), 스크린 리더는 변경 사항을 알리기 위해 라이브 영역이 필요합니다. Cursor가 패턴을 생성합니다:

export function SearchResults({ query, results }: { query: string; results: string[] }) {
return (
<div
aria-live="polite"
aria-atomic="true"
role="status"
>
{results.length === 0 ? (
<p>"{query}"에 대한 결과가 없습니다.</p>
) : (
<p>"{query}"에 대한 결과 {results.length}개를 찾았습니다.</p>
)}
<ul>
{results.map((r) => <li key={r}>{r}</li>)}
</ul>
</div>
);
}
경고

긴급하지 않은 업데이트에는 aria-live="polite"를 사용하고, 중요한 경고에만 aria-live="assertive"를 사용하세요. assertive 라이브 영역을 남용하면 스크린 리더 사용자가 끊임없이 방해받아 앱을 사용할 수 없게 됩니다. 성공 메시지에 절대 role="alert"를 사용하지 마세요. 즉각적인 주의가 필요한 실제 오류에만 사용하세요.

axe-core 자동화 테스트​

자동화 도구는 접근성 문제의 약 30~40퍼센트를 감지합니다. axe-core는 업계 표준입니다. Cursor는 이를 테스트 스위트와 CI 파이프라인에 통합할 수 있습니다.

Jest로 axe-core 설정​

Cursor에게 자동화된 접근성 테스트를 추가해 달라고 요청하세요:

내 React 프로젝트에 axe-core를 추가해주세요. Testing Library로
SiteNav 컴포넌트를 렌더링하고 axe 규칙을 실행하는 Jest 테스트를
만들어주세요.

Cursor가 종속성을 설치하고 테스트를 작성합니다:

npm install -D @axe-core/react axe-core jest-axe @testing-library/react @testing-library/jest-dom
// src/components/SiteNav.test.tsx
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
import { SiteNav } from './SiteNav';

expect.extend(toHaveNoViolations);

const links = [
{ href: '/', label: '홈' },
{ href: '/about', label: '소개' },
];

describe('SiteNav', () => {
it('닫혀있을 때 접근성 위반 없음', async () => {
const { container } = render(<SiteNav links={links} />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});

it('열려있을 때 접근성 위반 없음', async () => {
const { container, getByText } = render(<SiteNav links={links} />);
getByText('메뉴').click();
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});

재사용 가능한 axe 테스트 헬퍼​

Cursor는 모든 컴포넌트 테스트가 일관되게 유지되도록 재사용 가능한 헬퍼를 생성할 수 있습니다:

import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';

expect.extend(toHaveNoViolations);

export async function expectAccessible(component: React.ReactElement) {
const { container } = render(component);
const results = await axe(container, {
rules: {
// 의도적으로 다른 곳에서 처리하는 규칙 비활성화
region: { enabled: false },
},
});
expect(results).toHaveNoViolations();
return results;
}

모든 컴포넌트 테스트에서 사용하세요:

import { expectAccessible } from '../test/expectAccessible';
import { FormField } from './FormField';

it('접근 가능함', async () => {
await expectAccessible(<FormField label="이메일" name="email" required />);
});
팁

패키지 스크립트로 각 커밋 전에 전체 axe 스위트를 실행하세요. Cursor에게 "모든 axe 테스트를 실행하고 위반 시 실패하는 a11y 스크립트를 package.json에 추가해 달라"고 요청하세요. 이는 회귀가 메인 브랜치에 도달하는 것을 방지합니다.

AI 지원 접근성 수정​

Cursor의 AI는 전체 컴포넌트 파일을 스캔하고, 접근성 문제를 찾고, 한 번에 수정을 적용할 수 있습니다. 이는 가장 가치 있는 a11y 워크플로 중 하나입니다.

컴포넌트 감사​

Cursor에서 컴포넌트를 열고 AI 채팅에서 이 프롬프트를 실행하세요:

이 컴포넌트를 WCAG 2.2 AA 준수에 대해 감사해주세요. 줄 번호와
함께 모든 문제를 나열하고, 실패 이유를 설명하고, 수정 사항과
함께 코드를 다시 작성해주세요. 각 변경 사항을 설명하는 주석을
추가해주세요.

Cursor는 다음을 보고할 수 있습니다:

  • 12행: <div onClick>은 키보드로 접근할 수 없습니다. <button>으로 교체하세요.
  • 20행: <img>에 alt 속성이 없습니다. 설명적인 alt 텍스트를 추가하세요.
  • 28행: 색상 대비가 3.1:1로, 일반 텍스트의 최소 4.5:1 미만입니다.
  • 35행: 모달 대화 상자에 포커스 트랩이 없습니다. 사용자가 대화 상자 뒤로 Tab할 수 있습니다.

Cursor가 수정을 적용합니다:

// 이전
<div className="card" onClick={openDetails}>
<img src={product.image} />
<h3 style={{ color: '#999' }}>{product.name}</h3>
</div>

// 이후
<button
className="card"
onClick={openDetails}
aria-label={`${product.name} 세부 정보 보기`}
>
<img src={product.image} alt={`${product.name} 제품 사진`} />
<h3 style={{ color: '#595959' }}>{product.name}</h3>
</button>

포커스 트랩 생성​

모달과 대화 상자는 키보드 사용자가 Tab으로 대화 상자를 벗어날 수 없도록 포커스 트랩이 필요합니다. Cursor에게 요청하세요:

이 모달 컴포넌트에 포커스 트랩을 추가해주세요. 열릴 때 첫 번째
포커스 가능한 요소로 포커스를 이동하고, 대화 상자 내에서 Tab과
Shift+Tab을 가두고, 닫힐 때 트리거 버튼으로 포커스를 반환해주세요.
import { useEffect, useRef } from 'react';

const FOCUSABLE = 'a[href], button:not([disabled]), input, textarea, select, [tabindex]:not([tabindex="-1"])';

export function useFocusTrap(active: boolean) {
const containerRef = useRef<HTMLDivElement>(null);

useEffect(() => {
if (!active || !containerRef.current) return;
const container = containerRef.current;
const elements = container.querySelectorAll<HTMLElement>(FOCUSABLE);
if (elements.length === 0) return;

const first = elements[0];
const last = elements[elements.length - 1];
first.focus();

const onKey = (e: KeyboardEvent) => {
if (e.key !== 'Tab') return;
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
};

container.addEventListener('keydown', onKey);
return () => container.removeEventListener('keydown', onKey);
}, [active]);

return containerRef;
}
경고

기본 HTML이 키보드로 접근 가능하지 않으면 포커스 트랩은 의미가 없습니다. 트랩은 실제로 포커스 가능한 요소만 관리합니다. 대화 상자에 onClick 핸들러가 있는 <div> 요소만 포함되어 있으면 포커스 트랩이 포커스할 대상이 없습니다. 트랩은 항상 시맨틱하고 포커스 가능한 요소와 함께 사용하세요.

스크린 리더 테스트​

자동화된 테스트는 구조적 문제를 감지하지만, 스크린 리더로 수동 테스트만이 실제 경험을 드러냅니다. Cursor는 테스트 스크립트와 체크리스트를 생성할 수 있습니다.

키보드 전용 테스트 체크리스트​

Cursor에게 키보드 테스트 체크리스트를 생성해 달라고 요청하세요:

탐색, 양식, 위젯 및 동적 콘텐츠를 다루는 웹 애플리케이션용
키보드 전용 테스트 체크리스트를 만들어주세요.

Cursor가 생성합니다:

  1. 트랩 없이 논리적 순서로 페이지를 Tab으로 탐색.
  2. 스킵 링크가 첫 번째 포커스 가능한 요소이며 메인 콘텐츠로 이동.
  3. 모든 대화형 요소가 Tab과 Shift+Tab으로 도달 가능.
  4. 사용자 정의 위젯(탭, 아코디언)이 화살표 키와 Home/End 지원.
  5. 모달이 포커스를 가두고 닫을 때 트리거로 반환.
  6. 모든 포커스 가능한 요소에 보이는 포커스 표시기가 있음.
  7. Enter와 Space가 버튼 활성화; Enter가 링크 활성화.
  8. 양식이 제출 버튼에서 Enter로 제출.

NVDA, JAWS 및 VoiceOver로 테스트​

Cursor는 각 스크린 리더에 대한 테스트 노트를 생성할 수 있습니다:

Windows에서 NVDA, macOS에서 VoiceOver, Android에서 TalkBack으로
내 앱을 테스트하기 위한 빠른 시작 가이드를 작성해주세요. 각
리더를 시작하는 단축키와 제목 및 랜드마크별로 탐색하는 방법을
포함해주세요.

Cursor는 비교 표와 탐색 치트 시트를 반환하여 프로젝트의 참조 파일로 저장할 수 있습니다.

색상 대비 및 시각적 디자인​

시각적 접근성은 WCAG 그림의 일부입니다. 낮은 대비는 가장 일반적인 실패 중 하나입니다.

AI로 대비 확인​

Cursor에게 디자인 토큰을 평가해 달라고 요청하세요:

tailwind.config.js에 내 색상 토큰이 있습니다. 모든 텍스트와 배경
쌍을 WCAG AA 대비 비율(일반 텍스트 4.5:1, 큰 텍스트 3:1)에 대해
확인해주세요. 실패를 표시하고 수정을 제안해주세요.

Cursor가 구성을 읽고 대비 비율을 계산하여 실패를 보고합니다:

// AA를 통과하지 못하는 tailwind.config.js 토큰
// text-gray-400 on bg-white: 2.85:1 — 실패
// 수정: text-gray-600 (#4B5563)을 사용하여 7.21:1 달성

Cursor가 구성을 업데이트합니다:

module.exports = {
theme: {
extend: {
colors: {
'text-secondary': '#4B5563', // was #9CA3AF, now passes AA
},
},
},
};
팁

의미를 전달하는 데 색상만 사용하지 마세요. 잘못된 필드 주변의 빨간색 테두리는 스크린 리더에도 의존하는 색맹 사용자에게 보이지 않습니다. 항상 색상과 텍스트 또는 아이콘을 함께 사용하세요. Cursor에게 "양식 전체에 색상과 함께 오류 상태를 전달하기 위해 텍스트와 아이콘을 추가해 달라"고 요청하여 수정하세요.

CI에서 접근성​

자동화된 a11y 검사는 CI 파이프라인에 속합니다. Cursor는 구성을 생성할 수 있습니다.

a11y 카테고리가 있는 Lighthouse CI​

Cursor에게 Lighthouse CI를 추가해 달라고 요청하세요:

접근성 카테고리가 활성화되고 최소 점수 0.9인 Lighthouse CI를
내 GitHub Actions 워크플로에 추가해주세요.
# .github/workflows/lighthouse.yml
name: Lighthouse
on: [pull_request]

jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci
- run: npm run build
- name: Run Lighthouse
uses: treosh/lighthouse-ci-action@v11
with:
urls: |
http://localhost:3000
uploadArtifacts: true
configPath: .lighthouserc.json
// .lighthouserc.json
{
"ci": {
"assert": {
"assertions": {
"categories:accessibility": ["error", { "minScore": 0.9 }]
}
},
"collect": {
"startServerCommand": "npm run start",
"startServerReadyPattern": "ready",
"numberOfRuns": 3
}
}
}

모범 사례 요약​

Cursor에서 접근 가능한 앱을 구축할 때 다음 원칙을 명심하세요:

  • 시맨틱 HTML 선호 - 네이티브 요소가 적합할 때 ARIA보다.
  • 모든 입력에 레이블 지정 - 진정한 <label> 또는 aria-label로.
  • 모든 것을 키보드로 운용 가능하게 - 마우스는 선택 사항.
  • 모든 컴포넌트 테스트에 axe-core로 자동화.
  • 수동 테스트 - 최소 하나의 스크린 리더로.
  • 일반 텍스트에 4.5:1 대비 유지 및 보이는 포커스 표시기.
  • CI에서 a11y 검사 실행 - 회귀가 배포되지 않도록.

WCAG 지식과 Cursor의 AI 기반 감사 및 코드 생성을 결합하면 모든 사람에게 작동하는 포용적인 인터페이스를 구축할 수 있습니다. Cursor가 접근 가능한 마크업과 테스트를 스캐폴드한 다음, 배포하기 전에 실제 스크린 리더로 결과를 검증하세요.