웹 접근성(a11y) 실전 가이드 — ARIA, 키보드, 스크린 리더

웹 접근성이 중요한 이유

웹 접근성(a11y, accessibility의 a와 y 사이 11글자)은 장애가 있는 사용자도 웹을 동등하게 이용할 수 있도록 만드는 것입니다. 건물에 계단만 있으면 휠체어 사용자는 들어갈 수 없습니다. 웹에서 경사로 역할을 하는 것이 접근성입니다.

전 세계 인구의 약 15%(10억 명)가 어떤 형태의 장애를 가지고 있습니다. 접근성은 도덕적 의무이자, 법적 요구사항이며, 비즈니스 기회이기도 합니다.

WCAG 4대 원칙 (POUR)

원칙의미예시
Perceivable (인식 가능)모든 콘텐츠를 인식할 수 있어야 함이미지에 대체 텍스트 제공
Operable (조작 가능)키보드만으로 모든 기능 사용 가능탭으로 네비게이션, Enter로 활성화
Understandable (이해 가능)콘텐츠와 UI를 이해할 수 있어야 함명확한 에러 메시지, 일관된 UI
Robust (견고함)다양한 기술로 접근 가능스크린 리더, 음성 인식 등

시맨틱 HTML: 접근성의 기초

시맨틱 HTML은 접근성의 80%를 차지합니다. ARIA를 사용하기 전에 올바른 HTML 요소를 먼저 사용하세요.

<!-- 잘못됨: div로 모든 것을 만듦 (스크린 리더가 구조 파악 불가) -->
<div class="header">
  <div class="nav">
    <div class="link" onclick="navigate('/')">홈</div>
    <div class="link" onclick="navigate('/about')">소개</div>
  </div>
</div>
<div class="main">
  <div class="article">
    <div class="title">웹 접근성 가이드</div>
    <div class="content">...</div>
  </div>
</div>

<!-- 올바름: 시맨틱 요소 사용 (자동으로 역할과 구조 전달) -->
<header>
  <nav aria-label="메인 네비게이션">
    <a href="/">홈</a>
    <a href="/about">소개</a>
  </nav>
</header>
<main>
  <article>
    <h1>웹 접근성 가이드</h1>
    <p>...</p>
  </article>
</main>
<footer>
  <p>Copyright 2026</p>
</footer>

시맨틱 요소 → ARIA 역할 매핑

HTML 요소암묵적 ARIA 역할스크린 리더 읽음
navnavigation”네비게이션”
mainmain”메인 콘텐츠”
headerbanner”배너”
footercontentinfo”콘텐츠 정보”
buttonbutton”버튼”
a[href]link”링크”
h1~h6heading”제목 레벨 N”
ul/ollist”목록, N개 항목”

ARIA 속성 실전

ARIA(Accessible Rich Internet Applications)는 시맨틱 HTML로 표현할 수 없는 복잡한 UI에 접근성 정보를 추가합니다.

핵심 규칙: ARIA보다 시맨틱 HTML 우선

<!-- 잘못됨: div에 ARIA 역할 부여 -->
<div role="button" tabindex="0" onclick="submit()">제출</div>

<!-- 올바름: 네이티브 button 사용 -->
<button onclick="submit()">제출</button>
<!-- button은 자동으로 role="button", 키보드 지원, 포커스 관리 제공 -->

자주 쓰는 ARIA 패턴

<!-- 1. 아코디언 -->
<button
  aria-expanded="false"
  aria-controls="panel-1"
  id="accordion-1">
  자주 묻는 질문
</button>
<div
  id="panel-1"
  role="region"
  aria-labelledby="accordion-1"
  hidden>
  <p>접근성은 모든 사용자를 위한 것입니다.</p>
</div>

<!-- 2. 탭 패널 -->
<div role="tablist" aria-label="콘텐츠 탭">
  <button role="tab" aria-selected="true"
          aria-controls="tab-panel-1" id="tab-1">
    개요
  </button>
  <button role="tab" aria-selected="false"
          aria-controls="tab-panel-2" id="tab-2"
          tabindex="-1">
    상세
  </button>
</div>
<div role="tabpanel" id="tab-panel-1"
     aria-labelledby="tab-1">
  <p>개요 내용입니다.</p>
</div>
<div role="tabpanel" id="tab-panel-2"
     aria-labelledby="tab-2" hidden>
  <p>상세 내용입니다.</p>
</div>

<!-- 3. 실시간 알림 (스크린 리더가 즉시 읽음) -->
<div aria-live="polite" aria-atomic="true" id="status">
  <!-- JavaScript로 동적 메시지 삽입 -->
</div>

<!-- 4. 모달 다이얼로그 -->
<dialog aria-labelledby="modal-title" aria-modal="true">
  <h2 id="modal-title">삭제 확인</h2>
  <p>정말 삭제하시겠습니까?</p>
  <button>확인</button>
  <button>취소</button>
</dialog>

키보드 네비게이션

모든 인터랙티브 요소는 키보드로 접근 가능해야 합니다.

// 탭 패널 키보드 네비게이션 구현
document.querySelector("[role='tablist']").addEventListener("keydown", (e) => {
  const tabs = [...e.currentTarget.querySelectorAll("[role='tab']")];
  const currentIndex = tabs.indexOf(e.target);

  let newIndex;

  switch (e.key) {
    case "ArrowRight": // 오른쪽 화살표 → 다음 탭
      newIndex = (currentIndex + 1) % tabs.length;
      break;
    case "ArrowLeft":  // 왼쪽 화살표 → 이전 탭
      newIndex = (currentIndex - 1 + tabs.length) % tabs.length;
      break;
    case "Home":       // Home → 첫 번째 탭
      newIndex = 0;
      break;
    case "End":        // End → 마지막 탭
      newIndex = tabs.length - 1;
      break;
    default:
      return; // 다른 키는 무시
  }

  // 포커스 이동 및 탭 활성화
  tabs[newIndex].focus();
  tabs[newIndex].click();
  e.preventDefault();
});
/* 포커스 스타일 — 키보드 사용자를 위한 시각적 표시 */

/* 잘못됨: 포커스 스타일 제거 */
*:focus { outline: none; } /* 절대 금지! */

/* 올바름: 명확한 포커스 표시 */
:focus-visible {
  outline: 3px solid #4f46e5;     /* 인디고 색상 아웃라인 */
  outline-offset: 2px;            /* 요소로부터 2px 떨어져서 */
  border-radius: 4px;
}

/* 마우스 클릭 시에는 아웃라인 숨김, 키보드 탐색 시에만 표시 */
:focus:not(:focus-visible) {
  outline: none;
}

/* 건너뛰기 링크 — 키보드 사용자가 반복 네비게이션 건너뛰기 */
.skip-link {
  position: absolute;
  top: -100%;
  left: 0;
  z-index: 9999;
  padding: 1rem;
  background: #1e1b4b;
  color: white;
}

.skip-link:focus {
  top: 0;                         /* 포커스 시 화면에 표시 */
}
<!-- 건너뛰기 링크 사용 -->
<body>
  <a href="#main-content" class="skip-link">본문으로 건너뛰기</a>
  <header>...</header>
  <nav>...</nav>
  <main id="main-content">
    <!-- 메인 콘텐츠 -->
  </main>
</body>

이미지와 대체 텍스트

<!-- 정보를 전달하는 이미지 — 구체적 설명 -->
<img src="chart.png" alt="2025년 분기별 매출 — 1분기 100억, 2분기 150억, 3분기 200억" />

<!-- 장식용 이미지 — 빈 alt (스크린 리더가 무시) -->
<img src="decoration.svg" alt="" />

<!-- 링크 안의 이미지 — 링크 목적 설명 -->
<a href="/profile">
  <img src="avatar.jpg" alt="내 프로필로 이동" />
</a>

<!-- 복잡한 이미지 — 상세 설명 연결 -->
<figure>
  <img src="architecture.png"
       alt="마이크로서비스 아키텍처 다이어그램"
       aria-describedby="arch-desc" />
  <figcaption id="arch-desc">
    API Gateway가 3개의 서비스(인증, 상품, 주문)로 요청을 라우팅하며,
    각 서비스는 독립된 데이터베이스를 사용합니다.
  </figcaption>
</figure>

폼 접근성

<!-- 올바른 폼 구조 -->
<form aria-labelledby="form-title">
  <h2 id="form-title">회원가입</h2>

  <!-- label과 input 연결 필수 -->
  <div>
    <label for="user-email">이메일 (필수)</label>
    <input
      type="email"
      id="user-email"
      name="email"
      required
      aria-required="true"
      aria-describedby="email-hint email-error"
      autocomplete="email" />
    <p id="email-hint">예: user@example.com</p>
    <p id="email-error" role="alert" hidden>
      유효한 이메일을 입력해주세요.
    </p>
  </div>

  <!-- 비밀번호 — 요구사항 안내 -->
  <div>
    <label for="user-pw">비밀번호 (필수)</label>
    <input
      type="password"
      id="user-pw"
      name="password"
      required
      aria-required="true"
      aria-describedby="pw-requirements"
      minlength="8"
      autocomplete="new-password" />
    <ul id="pw-requirements">
      <li>8자 이상</li>
      <li>영문, 숫자, 특수문자 포함</li>
    </ul>
  </div>

  <button type="submit">가입하기</button>
</form>

접근성 테스트 도구

도구유형용도
axe DevTools브라우저 확장자동화된 접근성 검사
LighthouseChrome 내장접근성 점수 측정
NVDA스크린 리더 (Windows)실제 스크린 리더 테스트
VoiceOver스크린 리더 (macOS)실제 스크린 리더 테스트
Colour Contrast Analyser데스크톱 앱색상 대비 확인
# axe-core CLI로 자동 테스트
npx @axe-core/cli https://example.com

# jest-axe로 컴포넌트 테스트에 접근성 검사 통합
npm install --save-dev jest-axe

실전 팁

  • 시맨틱 HTML을 먼저 사용하세요: 대부분의 접근성 문제는 올바른 HTML 요소를 사용하는 것만으로 해결됩니다. divspan 남용을 피하세요.
  • 키보드만으로 사이트를 사용해보세요: Tab, Shift+Tab, Enter, Space, Escape, 화살표 키로 모든 기능을 사용할 수 있는지 직접 확인하세요.
  • :focus-visible 스타일을 제거하지 마세요: 포커스 표시를 없애면 키보드 사용자는 현재 위치를 알 수 없습니다.
  • 색상에만 의존하지 마세요: 에러를 빨간색으로만 표시하면 색각 이상 사용자는 인지할 수 없습니다. 아이콘이나 텍스트를 함께 제공하세요.
  • ARIA는 최후의 수단입니다: ARIA를 잘못 사용하면 접근성이 오히려 나빠집니다. “No ARIA is better than bad ARIA”라는 규칙을 기억하세요.
  • 실제 스크린 리더로 테스트하세요: Windows에서는 NVDA(무료), macOS에서는 VoiceOver(내장)로 직접 듣고 확인하는 것이 가장 확실합니다.

이 글이 도움이 되었나요?