SOFTMOA TECHNOLOGY

웹 접근성(A11y) 완벽 가이드 2025

소프트모아가 정리한 기술 기록입니다.

Contents
작성일 2026. 02. 11.

개요

웹 접근성(Web Accessibility, A11y)은 장애가 있는 사용자를 포함한 모든 사람이 웹을 이용할 수 있도록 보장하는 것입니다. 2025년 현재, 접근성은 법적 의무사항일 뿐 아니라 SEO, 사용성, 그리고 사회적 책임의 핵심입니다. WCAG 2.2 기준과 최신 HTML/ARIA 패턴을 중심으로 실전 가이드를 정리합니다.

핵심 개념

WCAG(Web Content Accessibility Guidelines)는 웹 접근성의 국제 표준입니다. 2.2 버전은 인지, 조작, 이해, 견고성의 4대 원칙을 기반으로 하며, A, AA, AAA 세 가지 준수 레벨이 있습니다. 대부분의 국가에서 AA 레벨을 법적 기준으로 삼고 있습니다.

ARIA(Accessible Rich Internet Applications)는 HTML만으로 표현할 수 없는 복잡한 인터랙션을 스크린 리더에 전달하는 명세입니다. role, aria-label, aria-describedby 등의 속성을 사용합니다.

키보드 접근성은 마우스 없이 Tab, Enter, Space, 화살표 키만으로 모든 기능을 사용할 수 있어야 함을 의미합니다.

스크린 리더는 시각 장애인이 웹을 이용하는 주요 수단으로, NVDA, JAWS, VoiceOver 등이 있습니다.

실전 예제

시맨틱 HTML로 접근성을 향상시킵니다.

<!-- 나쁜 예 -->
<div class="header">
  <div class="nav">
    <span onclick="navigate('/home')">홈</span>
    <span onclick="navigate('/about')">소개</span>
  </div>
</div>

<!-- 좋은 예 -->
<header>
  <nav aria-label="주요 메뉴">
    <ul>
      <li><a href="/home">홈</a></li>
      <li><a href="/about">소개</a></li>
    </ul>
  </nav>
</header>

버튼과 폼 요소의 올바른 접근성 구현입니다.

<!-- 나쁜 예 -->
<div class="button" onclick="submit()">전송</div>

<!-- 좋은 예 -->
<button type="submit">전송</button>

<!-- 폼 레이블 연결 -->
<label for="email">이메일 주소</label>
<input
  type="email"
  id="email"
  name="email"
  aria-describedby="email-help"
  aria-required="true"
  aria-invalid="false"
>
<small id="email-help">[email protected] 형식으로 입력하세요</small>

<!-- 에러 상태 -->
<input
  type="email"
  id="email"
  name="email"
  aria-describedby="email-error"
  aria-required="true"
  aria-invalid="true"
>
<span id="email-error" role="alert">유효한 이메일을 입력하세요</span>

모달(대화상자) 접근성 구현입니다.

<button id="open-modal">설정 열기</button>

<div
  role="dialog"
  aria-labelledby="modal-title"
  aria-describedby="modal-description"
  aria-modal="true"
  class="modal"
  hidden
>
  <div class="modal-content">
    <h2 id="modal-title">설정</h2>
    <p id="modal-description">애플리케이션 설정을 변경합니다.</p>

    <label for="theme">테마 선택</label>
    <select id="theme">
      <option value="light">밝게</option>
      <option value="dark">어둡게</option>
      <option value="auto">자동</option>
    </select>

    <div class="modal-actions">
      <button type="button" id="close-modal">닫기</button>
      <button type="submit">저장</button>
    </div>
  </div>
</div>
class AccessibleModal {
  constructor(modalEl, openBtn, closeBtn) {
    this.modal = modalEl;
    this.openBtn = openBtn;
    this.closeBtn = closeBtn;
    this.previousFocus = null;

    this.init();
  }

  init() {
    this.openBtn.addEventListener('click', () => this.open());
    this.closeBtn.addEventListener('click', () => this.close());

    // ESC 키로 닫기
    this.modal.addEventListener('keydown', (e) => {
      if (e.key === 'Escape') this.close();
    });

    // 포커스 트랩
    this.modal.addEventListener('keydown', (e) => {
      if (e.key === 'Tab') {
        this.trapFocus(e);
      }
    });
  }

  open() {
    // 현재 포커스 저장
    this.previousFocus = document.activeElement;

    // 모달 표시
    this.modal.hidden = false;

    // 첫 번째 포커스 가능 요소로 이동
    const firstFocusable = this.modal.querySelector(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    );
    firstFocusable?.focus();

    // 배경 스크롤 방지
    document.body.style.overflow = 'hidden';
  }

  close() {
    this.modal.hidden = true;
    document.body.style.overflow = '';

    // 이전 포커스 복원
    this.previousFocus?.focus();
  }

  trapFocus(e) {
    const focusableElements = this.modal.querySelectorAll(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    );
    const firstFocusable = focusableElements[0];
    const lastFocusable = focusableElements[focusableElements.length - 1];

    if (e.shiftKey && document.activeElement === firstFocusable) {
      lastFocusable.focus();
      e.preventDefault();
    } else if (!e.shiftKey && document.activeElement === lastFocusable) {
      firstFocusable.focus();
      e.preventDefault();
    }
  }
}

const modal = new AccessibleModal(
  document.querySelector('.modal'),
  document.getElementById('open-modal'),
  document.getElementById('close-modal')
);

동적 콘텐츠 업데이트를 스크린 리더에 알립니다.

<!-- 실시간 알림 (즉시 전달) -->
<div role="alert" aria-live="assertive" aria-atomic="true">
  <!-- JavaScript로 여기에 메시지 삽입 -->
</div>

<!-- 폴라이트 알림 (현재 읽기가 끝난 후 전달) -->
<div role="status" aria-live="polite" aria-atomic="true">
  10개의 새 메시지가 있습니다.
</div>

<!-- 로딩 상태 -->
<button aria-busy="true" aria-live="polite">
  <span class="spinner" aria-hidden="true"></span>
  로딩 중...
</button>

이미지 접근성 모범 사례입니다.

<!-- 의미 있는 이미지 -->
<img
  src="chart.png"
  alt="2025년 1분기 매출 증가 추이: 1월 100만원, 2월 150만원, 3월 200만원"
>

<!-- 장식용 이미지 -->
<img src="decoration.png" alt="" role="presentation">

<!-- 복잡한 이미지 (긴 설명 필요) -->
<figure>
  <img
    src="complex-diagram.png"
    alt="시스템 아키텍처 다이어그램"
    aria-describedby="diagram-description"
  >
  <figcaption id="diagram-description">
    클라이언트는 API Gateway를 통해 백엔드 서비스에 접근합니다.
    백엔드는 마이크로서비스 아키텍처로 구성되어 있으며...
  </figcaption>
</figure>

활용 팁

  • 자동 테스트 도구: axe DevTools, WAVE, Lighthouse를 사용하여 자동 검사를 수행하세요. 하지만 자동 도구는 30-40%만 감지합니다.
  • 실제 스크린 리더 테스트: NVDA(Windows, 무료), VoiceOver(macOS/iOS, 내장)로 실제 사용자 경험을 확인하세요.
  • 키보드 내비게이션: 마우스를 치우고 Tab, Shift+Tab, Enter, Space만으로 모든 기능을 테스트하세요.
  • 색상 대비: WCAG AA는 4.5:1, AAA는 7:1 대비율을 요구합니다. WebAIM Contrast Checker로 확인하세요.
  • 포커스 표시: outline: none을 사용했다면 반드시 대체 포커스 스타일을 제공하세요.
  • 랜드마크 역할: header, nav, main, aside, footer 태그를 사용하여 페이지 구조를 명확히 하세요.

마무리

접근성은 특별한 사용자를 위한 추가 작업이 아니라 모든 사용자에게 더 나은 경험을 제공하는 기본 품질입니다. 시맨틱 HTML을 올바르게 사용하고, ARIA를 필요한 곳에만 적용하며, 키보드와 스크린 리더로 직접 테스트하는 것이 핵심입니다. 접근성을 갖춘 웹사이트는 SEO, 사용성, 법적 준수를 동시에 달성합니다.

Share this

Html and CSS

Have a questions?

견적 및 기술문의

mobile : 010-7931-4813

Contact Form