Claude Code로 웹 접근성 최적화하기
Claude Code를 사용하여 웹 접근성을 최적화하는 방법을 알아봅니다. 실용적인 코드 예시와 단계별 가이드를 포함합니다.
Claude Code를 접근성 대응에 활용하는 장점
웹 접근성 대응은 중요하지만, WCAG 기준의 이해와 ARIA 속성의 올바른 사용법은 복잡합니다. Claude Code는 접근성 모범 사례를 반영한 코드 생성과 기존 코드 감사를 효율적으로 수행할 수 있습니다.
기존 코드의 접근성 감사
> src/components/ 하위의 모든 컴포넌트를 접근성 관점에서 리뷰해줘.
> WCAG 2.1 AA 기준으로 체크하고, 문제점과 수정안을 목록으로 정리해줘.
Claude Code는 다음과 같은 문제를 감지합니다:
- 이미지에 alt 속성이 누락되어 있음
- 폼 요소에 label 요소가 연결되어 있지 않음
- 색상 대비 비율이 부족함
- 키보드로 조작할 수 없는 인터랙티브 요소가 있음
- ARIA 속성이 부적절하게 사용되고 있음
접근성을 갖춘 컴포넌트 생성
모달 다이얼로그
> 접근성을 갖춘 모달 다이얼로그를 만들어줘.
> 포커스 트랩, ESC 키로 닫기, 배경 스크롤 잠금을 구현해줘.
import { useEffect, useRef, useCallback } from 'react';
interface ModalProps {
isOpen: boolean;
onClose: () => void;
title: string;
children: React.ReactNode;
}
function Modal({ isOpen, onClose, title, children }: ModalProps) {
const modalRef = useRef<HTMLDivElement>(null);
const previousFocus = useRef<HTMLElement | null>(null);
// 포커스 트랩
const trapFocus = useCallback((e: KeyboardEvent) => {
if (!modalRef.current) return;
const focusable = modalRef.current.querySelectorAll<HTMLElement>(
'a[href], button:not([disabled]), input:not([disabled]), select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.key === 'Tab') {
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
if (e.key === 'Escape') onClose();
}, [onClose]);
useEffect(() => {
if (isOpen) {
previousFocus.current = document.activeElement as HTMLElement;
document.body.style.overflow = 'hidden';
document.addEventListener('keydown', trapFocus);
// 모달 내 첫 번째 포커스 가능 요소에 포커스
setTimeout(() => {
modalRef.current?.querySelector<HTMLElement>('[autofocus], button')?.focus();
}, 0);
}
return () => {
document.body.style.overflow = '';
document.removeEventListener('keydown', trapFocus);
previousFocus.current?.focus();
};
}, [isOpen, trapFocus]);
if (!isOpen) return null;
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center"
role="presentation"
>
<div
className="fixed inset-0 bg-black/50"
aria-hidden="true"
onClick={onClose}
/>
<div
ref={modalRef}
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
className="relative z-10 mx-4 w-full max-w-lg rounded-lg bg-white p-6 shadow-xl dark:bg-gray-800"
>
<h2 id="modal-title" className="text-xl font-bold mb-4">
{title}
</h2>
{children}
<button
onClick={onClose}
aria-label="Close"
className="absolute right-4 top-4 rounded-full p-1 hover:bg-gray-100 dark:hover:bg-gray-700"
>
<svg aria-hidden="true" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" />
</svg>
</button>
</div>
</div>
);
}
접근성을 갖춘 드롭다운 메뉴
function DropdownMenu({ label, items }: { label: string; items: MenuItem[] }) {
const [isOpen, setIsOpen] = useState(false);
const [activeIndex, setActiveIndex] = useState(-1);
const menuRef = useRef<HTMLUListElement>(null);
const handleKeyDown = (e: React.KeyboardEvent) => {
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
setActiveIndex(prev => Math.min(prev + 1, items.length - 1));
break;
case 'ArrowUp':
e.preventDefault();
setActiveIndex(prev => Math.max(prev - 1, 0));
break;
case 'Enter':
case ' ':
e.preventDefault();
if (activeIndex >= 0) items[activeIndex].onClick();
setIsOpen(false);
break;
case 'Escape':
setIsOpen(false);
break;
}
};
return (
<div className="relative" onKeyDown={handleKeyDown}>
<button
aria-haspopup="true"
aria-expanded={isOpen}
onClick={() => setIsOpen(!isOpen)}
>
{label}
</button>
{isOpen && (
<ul ref={menuRef} role="menu" className="absolute mt-1 rounded-md border bg-white shadow-lg dark:bg-gray-800">
{items.map((item, i) => (
<li
key={i}
role="menuitem"
tabIndex={-1}
className={`cursor-pointer px-4 py-2 ${i === activeIndex ? 'bg-blue-100 dark:bg-blue-900' : ''}`}
onClick={item.onClick}
>
{item.label}
</li>
))}
</ul>
)}
</div>
);
}
폼 접근성
> 문의 양식을 접근성을 갖춘 버전으로 다시 만들어줘.
> 유효성 검사 오류도 스크린 리더에 전달되도록 해줘.
function ContactForm() {
const [errors, setErrors] = useState<Record<string, string>>({});
return (
<form aria-label="문의 양식" noValidate>
<div className="mb-4">
<label htmlFor="name" className="block font-medium mb-1">
이름 <span aria-label="required">*</span>
</label>
<input
id="name"
type="text"
required
aria-required="true"
aria-invalid={!!errors.name}
aria-describedby={errors.name ? 'name-error' : undefined}
className="w-full rounded border p-2"
/>
{errors.name && (
<p id="name-error" role="alert" className="mt-1 text-sm text-red-600">
{errors.name}
</p>
)}
</div>
<div className="mb-4">
<label htmlFor="email" className="block font-medium mb-1">
이메일 주소 <span aria-label="required">*</span>
</label>
<input
id="email"
type="email"
required
aria-required="true"
aria-invalid={!!errors.email}
aria-describedby="email-hint email-error"
className="w-full rounded border p-2"
/>
<p id="email-hint" className="mt-1 text-xs text-gray-500">
e.g., user@example.com
</p>
{errors.email && (
<p id="email-error" role="alert" className="mt-1 text-sm text-red-600">
{errors.email}
</p>
)}
</div>
<button type="submit" className="rounded bg-blue-600 px-4 py-2 text-white">
전송
</button>
</form>
);
}
자동화 테스트 도입
> jest-axe를 사용한 접근성 테스트를 추가해줘.
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
describe('Modal accessibility', () => {
it('should have no accessibility violations', async () => {
const { container } = render(
<Modal isOpen={true} onClose={() => {}} title="Test Modal">
<p>Content</p>
</Modal>
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});
정리
Claude Code를 활용하면 WCAG 기준에 맞는 접근성 컴포넌트 생성부터 기존 코드 감사까지 효율적으로 수행할 수 있습니다. 훅 기능으로 접근성 테스트를 자동 실행하는 설정도 효과적입니다. 자세한 내용은 훅 기능 가이드를 참고하세요. 일상 개발 플로우에 적용하는 팁은 생산성을 3배 높이는 Tips에서 소개합니다.
Claude Code의 상세 정보는 Anthropic 공식 문서를 확인하세요. WCAG 가이드라인의 상세 내용은 W3C WCAG 2.1을 참고하세요.
Claude Code 워크플로우를 한 단계 업그레이드하세요
지금 바로 Claude Code에 복사해 쓸 수 있는 검증된 프롬프트 템플릿 50선.
이 글을 작성한 사람
Masa
Claude Code를 적극 활용하는 엔지니어. 10개 언어, 2,000페이지 이상의 테크 미디어 claudecode-lab.com을 운영 중.
관련 글
Claude Code 커스텀 슬래시 커맨드 만들기 — 나만의 개발 워크플로우
Claude Code에서 커스텀 슬래시 커맨드를 만드는 방법을 설명합니다. 파일 배치, 인수 전달, 반복 작업 자동화까지 실용적인 코드 예제와 함께 소개합니다.
Claude Code 생산성을 3배로 높이는 10가지 팁
Claude Code를 더 효과적으로 활용하는 10가지 실전 팁을 공개합니다. 프롬프트 전략부터 워크플로 단축키까지, 오늘부터 바로 적용해 보세요.
Canvas/WebGL Optimization: Claude Code 활용 가이드
canvas/webgl optimization: Claude Code 활용. 실용적인 팁과 코드 예시를 포함합니다.