웹 접근성(A11y), 모든 사용자를 위한 웹사이트 만들기
안녕하세요! 포테코입니다.
웹 접근성(Accessibility, 줄여서 A11y)은 모든 사용자가 웹사이트를 사용할 수 있도록 보장하는 것이에요. 시각 장애, 청각 장애, 운동 장애 등 다양한 능력을 가진 사용자들이 동등하게 웹 콘텐츠에 접근할 수 있어야 해요.
이번 글에서는 WCAG 가이드라인과 실무에서 바로 적용할 수 있는 웹 접근성 개선 방법을 체크리스트 형태로 정리했어요. Next.js와 React 프로젝트에서 바로 활용할 수 있는 예시와 함께 설명할게요.
웹 접근성은 법적 의무이기도 하지만, 더 중요한 것은 모든 사용자가 웹사이트를 사용할 수 있도록 하는 것이에요. 접근성을 개선하면 SEO와 사용자 경험도 함께 개선돼요.
웹 접근성이 왜 중요한가?
웹 접근성은 단순히 "좋은 것"이 아니에요. 법적 요구사항이기도 하지만, 더 중요한 것은 모든 사용자가 웹사이트를 사용할 수 있도록 하는 것이에요.
웹 접근성이 중요한 이유는 다음과 같아요:
- 법적 요구사항: 많은 국가에서 웹 접근성을 법적으로 요구해요
- 더 넓은 사용자층: 접근성을 개선하면 더 많은 사용자가 웹사이트를 사용할 수 있어요
- SEO 개선: 검색 엔진은 접근성이 좋은 사이트를 선호해요
- 사용자 경험 향상: 모든 사용자에게 더 나은 경험을 제공해요
WCAG 가이드라인 이해하기
WCAG(Web Content Accessibility Guidelines)는 웹 접근성의 국제 표준이에요. 4가지 원칙으로 구성되어 있어요:
| 원칙 | 설명 | 핵심 개념 |
|---|---|---|
| 인지 가능성 (Perceivable) | 정보와 UI 요소를 사용자가 인지할 수 있어야 함 | 대체 텍스트, 색상 대비, 텍스트 크기 |
| 조작 가능성 (Operable) | UI 요소와 네비게이션이 조작 가능해야 함 | 키보드 접근, 충분한 시간 제공, 발작 유발 방지 |
| 이해 가능성 (Understandable) | 정보와 UI 조작이 이해 가능해야 함 | 읽기 쉬운 텍스트, 예측 가능한 동작, 오류 식별 |
| 견고성 (Robust) | 다양한 기술로 해석 가능해야 함 | 유효한 HTML, 보조 기술 호환 |
WCAG는 A, AA, AAA 세 가지 수준으로 나뉘어요. 대부분의 웹사이트는 AA 수준을 목표로 하는 것이 좋아요. AAA는 매우 엄격한 기준이라 모든 콘텐츠에 적용하기 어려울 수 있어요.
실전 체크리스트
1. 시맨틱 HTML 사용
체크리스트:
- 적절한 HTML5 시맨틱 태그 사용 (
<header>,<nav>,<main>,<article>,<section>,<footer>) - 제목 계층 구조 올바르게 사용 (h1 → h2 → h3 순서)
- 목록은
<ul>,<ol>,<li>사용 - 폼 요소에 적절한
<label>연결
실전 예시:
// ✅ 좋은 예
<header>
<nav aria-label="메인 네비게이션">
<ul>
<li><a href="/">홈</a></li>
<li><a href="/about">소개</a></li>
</ul>
</nav>
</header>
<main>
<article>
<h1>제목</h1>
<p>본문 내용</p>
</article>
</main>
// ❌ 나쁜 예
<div>
<div>
<div>홈</div>
<div>소개</div>
</div>
</div>2. 이미지에 대체 텍스트 제공
체크리스트:
- 모든 이미지에
alt속성 추가 - 장식용 이미지는
alt=""사용 - 정보를 전달하는 이미지는 명확한 설명 제공
- 복잡한 이미지(차트, 그래프)는 추가 설명 제공
실전 예시:
// ✅ 좋은 예
<Image
src="/hero.jpg"
alt="Poteco Studio의 팀이 함께 작업하는 모습"
width={1200}
height={630}
/>
// 장식용 이미지
<Image
src="/decoration.jpg"
alt=""
width={100}
height={100}
/>
// ❌ 나쁜 예
<img src="/hero.jpg" />3. 색상 대비 확보
체크리스트:
- 텍스트와 배경의 대비 비율 4.5:1 이상 (일반 텍스트)
- 큰 텍스트(18pt 이상)는 3:1 이상
- 색상만으로 정보를 전달하지 않기
실전 예시:
/* ✅ 좋은 예 - 충분한 대비 */
.text-primary {
color: #1a1a1a; /* 배경이 #ffffff일 때 대비 16.5:1 */
background-color: #ffffff;
}
/* ❌ 나쁜 예 - 대비 부족 */
.text-light {
color: #cccccc; /* 배경이 #ffffff일 때 대비 1.6:1 */
background-color: #ffffff;
}색상만으로 정보를 전달하지 마세요. 예를 들어, "빨간색 텍스트는 필수 항목입니다"라고 하지 말고, 아이콘이나 텍스트로도 표시해야 합니다.
4. 키보드 네비게이션 지원
체크리스트:
- 모든 인터랙티브 요소에 키보드로 접근 가능
- 포커스 순서가 논리적
- 포커스 표시가 명확함
- 키보드 트랩 방지
실전 예시:
// ✅ 좋은 예 - 키보드 접근 가능
<button
onClick={handleClick}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
handleClick();
}
}}
>
클릭하세요
</button>
// 모달에서 키보드 트랩 방지
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
onClose();
}
};
document.addEventListener('keydown', handleEscape);
return () => document.removeEventListener('keydown', handleEscape);
}, [onClose]);5. ARIA 속성 활용
체크리스트:
- 필요한 경우에만 ARIA 사용 (시맨틱 HTML이 우선)
aria-label로 명확한 레이블 제공aria-describedby로 추가 설명 연결aria-hidden으로 불필요한 요소 숨기기
실전 예시:
// ✅ 좋은 예
<button
aria-label="메뉴 닫기"
onClick={closeMenu}
>
<CloseIcon aria-hidden="true" />
</button>
// 폼 오류 메시지
<label htmlFor="email">이메일</label>
<input
id="email"
type="email"
aria-invalid={hasError}
aria-describedby={hasError ? "email-error" : undefined}
/>
{hasError && (
<span id="email-error" role="alert">
올바른 이메일 형식을 입력해주세요
</span>
)}6. 폼 접근성
체크리스트:
- 모든 입력 필드에
<label>연결 - 필수 항목은
required속성과 시각적 표시 - 오류 메시지를 명확하게 표시
- 오류 필드로 자동 포커스 이동
실전 예시:
// ✅ 좋은 예
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="name">
이름 <span aria-label="필수 항목">*</span>
</label>
<input
id="name"
type="text"
required
aria-required="true"
aria-invalid={errors.name ? 'true' : 'false'}
aria-describedby={errors.name ? 'name-error' : undefined}
/>
{errors.name && (
<span id="name-error" role="alert">
{errors.name}
</span>
)}
</div>
</form>7. 스크린 리더 대응
체크리스트:
- 페이지 구조를 명확히 표시 (
<main>,<nav>등) - 스킵 링크 제공
- 랜드마크 역할 명시
- 동적 콘텐츠 변경 시 알림
실전 예시:
// ✅ 스킵 링크
<a href="#main-content" className="sr-only focus:not-sr-only">
메인 콘텐츠로 건너뛰기
</a>
<main id="main-content">
{/* 메인 콘텐츠 */}
</main>
// 동적 콘텐츠 변경 알림
<div role="status" aria-live="polite" aria-atomic="true">
{notification && <p>{notification}</p>}
</div>8. 애니메이션 및 모션
체크리스트:
- 사용자 선호도에 따라 애니메이션 비활성화 (
prefers-reduced-motion) - 발작을 유발할 수 있는 깜빡임 방지
- 중요한 정보는 애니메이션 없이도 접근 가능
실전 예시:
/* ✅ 좋은 예 */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* 깜빡임 방지 */
.flash {
animation: none; /* 3초에 3회 이상 깜빡이지 않도록 */
}Next.js/React 프로젝트에서의 접근성
1. 접근성 테스트 도구
체크리스트:
- 개발 중 ESLint 플러그인 사용 (
eslint-plugin-jsx-a11y) - Lighthouse 접근성 감사 실행
- 키보드만으로 전체 사이트 탐색 테스트
- 스크린 리더로 테스트 (NVDA, JAWS, VoiceOver)
설치 및 설정:
pnpm add -D eslint-plugin-jsx-a11y// .eslintrc.js
module.exports = {
plugins: ['jsx-a11y'],
rules: {
'jsx-a11y/alt-text': 'error',
'jsx-a11y/anchor-is-valid': 'error',
'jsx-a11y/aria-props': 'error',
// ... 기타 규칙
},
};2. 접근성 컴포넌트 라이브러리
추천 라이브러리:
- Radix UI: 접근성을 고려한 헤드리스 컴포넌트
- React Aria: Adobe의 접근성 컴포넌트 프리미티브
- Reach UI: 접근성 우선 컴포넌트
실전 예시:
// Radix UI 사용 예시
import * as Dialog from '@radix-ui/react-dialog';
<Dialog.Root>
<Dialog.Trigger>열기</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.Content>
<Dialog.Title>제목</Dialog.Title>
<Dialog.Description>설명</Dialog.Description>
<Dialog.Close>닫기</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>종합 체크리스트
HTML 구조
- 시맨틱 HTML 태그 사용
- 제목 계층 구조 올바르게 사용
- 랜드마크 역할 명시
이미지 및 미디어
- 모든 이미지에 적절한
alt텍스트 - 비디오에 자막 제공
- 오디오에 전사본 제공
색상 및 대비
- 텍스트 대비 비율 4.5:1 이상
- 색상만으로 정보 전달하지 않기
- 다크 모드 지원 고려
키보드 접근성
- 모든 기능이 키보드로 접근 가능
- 포커스 순서가 논리적
- 포커스 표시가 명확함
- 키보드 트랩 방지
폼 접근성
- 모든 입력 필드에 레이블 연결
- 필수 항목 명시
- 오류 메시지 명확하게 표시
- 오류 필드로 자동 포커스
ARIA 활용
- 필요한 경우에만 ARIA 사용
aria-label로 명확한 레이블aria-describedby로 추가 설명- 동적 콘텐츠 변경 시 알림
테스트
- ESLint 접근성 규칙 적용
- Lighthouse 접근성 감사
- 키보드만으로 탐색 테스트
- 스크린 리더 테스트
실제 프로젝트 적용 예시
예시 1: 접근 가능한 버튼 컴포넌트
interface ButtonProps {
children: React.ReactNode;
onClick: () => void;
ariaLabel?: string;
disabled?: boolean;
}
export function AccessibleButton({
children,
onClick,
ariaLabel,
disabled = false,
}: ButtonProps) {
return (
<button
onClick={onClick}
disabled={disabled}
aria-label={ariaLabel}
aria-disabled={disabled}
onKeyDown={(e) => {
if ((e.key === 'Enter' || e.key === ' ') && !disabled) {
e.preventDefault();
onClick();
}
}}
className={clsx(
'px-4 py-2 rounded',
'focus:outline-none focus:ring-2 focus:ring-primary',
disabled && 'opacity-50 cursor-not-allowed'
)}
>
{children}
</button>
);
}예시 2: 접근 가능한 모달
export function AccessibleModal({
isOpen,
onClose,
title,
children,
}: ModalProps) {
useEffect(() => {
if (isOpen) {
// 모달이 열릴 때 포커스를 모달로 이동
const modal = document.getElementById('modal');
modal?.focus();
// 배경 스크롤 방지
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
}
}, [isOpen]);
if (!isOpen) return null;
return (
<div
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
id="modal"
tabIndex={-1}
className="fixed inset-0 z-50"
>
<div
className="fixed inset-0 bg-black/50"
onClick={onClose}
aria-hidden="true"
/>
<div className="fixed inset-0 flex items-center justify-center p-4">
<div className="bg-white rounded-lg p-6 max-w-md w-full">
<h2 id="modal-title" className="text-xl font-bold mb-4">
{title}
</h2>
{children}
<button
onClick={onClose}
aria-label="모달 닫기"
className="mt-4"
>
닫기
</button>
</div>
</div>
</div>
);
}접근성 테스트 도구
1. 자동화 테스트
- axe DevTools: 브라우저 확장 프로그램
- WAVE: 웹 접근성 평가 도구
- Lighthouse: Chrome DevTools 내장
2. 수동 테스트
- 키보드만으로 탐색: Tab, Shift+Tab, Enter, Space, 화살표 키
- 스크린 리더 테스트: NVDA(Windows), JAWS(Windows), VoiceOver(Mac)
- 색상 대비 확인: WebAIM Contrast Checker
마무리
웹 접근성은 한 번에 완성되는 것이 아니에요. 지속적으로 개선해나가는 과정이에요.
접근성을 개선하면 모든 사용자에게 더 나은 경험을 제공할 수 있어요. 작은 것부터 시작해서 점진적으로 개선해나가는 것을 추천해요.
개발 초기부터 접근성을 고려하면 나중에 수정하는 것보다 훨씬 쉽고 효율적이에요.
다음 단계
- 현재 상태 파악: Lighthouse나 axe DevTools로 접근성 감사 실행
- 우선순위 정하기: 가장 큰 영향을 주는 항목부터 개선
- 점진적 개선: 한 번에 하나씩 접근성 개선
- 정기적 테스트: 새로운 기능 추가 시 접근성 테스트
추천 도구
- axe DevTools: 브라우저 확장 프로그램으로 쉽게 테스트할 수 있어요
- Lighthouse: Chrome DevTools에 내장된 접근성 감사
- WAVE: 웹 접근성 평가 도구
- NVDA: 무료 스크린 리더 (Windows)
웹 접근성은 모든 사용자를 위한 웹사이트를 만드는 핵심이에요. 앞으로도 웹 접근성 개선을 위한 실전 팁과 WCAG 가이드라인 적용 사례를 계속 공유할 예정이니 기대해 주세요!
