Skip to content
Published on

웹 접근성(a11y) 완전 가이드 2025: WCAG 2.2, ARIA, 키보드 내비게이션 — 모두를 위한 웹

Authors

1. 왜 접근성인가

10억 명의 사용자

전 세계 인구의 약 15%, 약 10억 명이 어떤 형태로든 장애를 가지고 있습니다. 웹 접근성은 단순히 "좋은 일"이 아니라 비즈니스 필수 사항입니다.

장애 유형전 세계 인구웹에서의 영향
시각 장애2.2억 명스크린 리더, 확대기 사용
청각 장애4.66억 명자막, 수화 필요
운동 장애수억 명키보드, 음성 입력
인지 장애다양단순한 UI, 명확한 언어

법적 요구사항

  • 미국 ADA: 웹사이트도 "공공 편의시설"로 간주. 소송 급증
  • EU EAA (European Accessibility Act): 2025년 6월 시행. 디지털 서비스 필수
  • 한국 장애인차별금지법: 웹 접근성 의무. 공공기관 + 민간 확대
  • WCAG 2.2: 국제 표준. 대부분의 법률이 AA 레벨을 요구

비즈니스 가치

접근성은 비용이 아니라 투자입니다.

  • SEO 개선: 시맨틱 HTML과 alt 텍스트는 검색 엔진이 좋아합니다
  • 사용자 확대: 전체 인구의 15%에게 도달
  • 법적 리스크 감소: 소송 비용 대비 사전 투자가 훨씬 저렴
  • 일반 사용자 경험 향상: 키보드 단축키, 명확한 레이블은 모든 사용자에게 유익

2. WCAG 2.2: 4가지 원칙

POUR 원칙

WCAG는 4가지 핵심 원칙 POUR(Perceivable, Operable, Understandable, Robust)을 기반으로 합니다.

원칙의미예시
Perceivable (인지 가능)정보를 인지할 수 있어야 함alt 텍스트, 자막, 색상 대비
Operable (운용 가능)UI를 조작할 수 있어야 함키보드, 충분한 시간, 발작 방지
Understandable (이해 가능)콘텐츠와 UI를 이해할 수 있어야 함명확한 언어, 일관된 내비게이션
Robust (견고)다양한 기술에서 작동해야 함유효한 HTML, ARIA 호환

적합성 레벨

  • Level A: 최소 요구사항 (필수)
  • Level AA: 대부분의 법률이 요구하는 수준 (권장 목표)
  • Level AAA: 최고 수준 (전체 사이트에 적용하기 어려움)

WCAG 2.2의 새로운 성공 기준

WCAG 2.2는 2023년 10월에 발표되었으며, 다음과 같은 새로운 기준이 추가되었습니다.

성공 기준레벨설명
2.4.11 Focus Not Obscured (Minimum)AA포커스된 요소가 다른 콘텐츠에 완전히 가려지면 안 됨
2.4.12 Focus Not Obscured (Enhanced)AAA포커스된 요소가 부분적으로도 가려지면 안 됨
2.4.13 Focus AppearanceAAA포커스 인디케이터 크기와 대비 요구사항
2.5.7 Dragging MovementsAA드래그가 필요한 기능에 대안 제공
2.5.8 Target Size (Minimum)AA터치 대상 최소 24x24 CSS 픽셀
3.2.6 Consistent HelpA도움말 메커니즘이 일관된 위치에
3.3.7 Redundant EntryA이전에 입력한 정보를 다시 요구하지 않음
3.3.8 Accessible AuthenticationAA인지 기능 테스트 없는 인증
3.3.9 Accessible Authentication (Enhanced)AAA더 엄격한 인증 접근성

3. 시맨틱 HTML: 접근성의 기초

올바른 요소 사용

시맨틱 HTML은 접근성의 80%를 해결합니다. 스크린 리더는 HTML 요소의 의미를 이해합니다.

<!-- 나쁨: div로 모든 것을 만들기 -->
<div class="button" onclick="submit()">제출</div>
<div class="header">사이트 제목</div>
<div class="nav">
  <div class="link" onclick="goto('/')"></div>
</div>

<!-- 좋음: 시맨틱 요소 사용 -->
<button type="submit">제출</button>
<header><h1>사이트 제목</h1></header>
<nav>
  <a href="/"></a>
</nav>

<button>은 자동으로 키보드 포커스를 받고, Enter/Space로 활성화되며, 스크린 리더가 "버튼"으로 인식합니다. <div>는 이 모든 것을 수동으로 구현해야 합니다.

랜드마크(Landmarks)

<body>
  <header>
    <nav aria-label="메인 내비게이션">...</nav>
  </header>

  <main>
    <article>
      <h1>기사 제목</h1>
      <section aria-labelledby="section-1">
        <h2 id="section-1">섹션 1</h2>
        ...
      </section>
    </article>

    <aside aria-label="관련 링크">...</aside>
  </main>

  <footer>...</footer>
</body>

스크린 리더 사용자는 랜드마크 간에 빠르게 이동할 수 있습니다. VoiceOver에서 Rotor를 사용하면 header, nav, main, footer로 즉시 점프합니다.

제목 계층 구조

<!-- 나쁨: 제목 레벨 건너뛰기 -->
<h1>페이지 제목</h1>
<h3>하위 섹션</h3>  <!-- h2를 건너뜀! -->
<h5>세부 항목</h5>   <!-- h4를 건너뜀! -->

<!-- 좋음: 순차적 제목 구조 -->
<h1>페이지 제목</h1>
  <h2>섹션 A</h2>
    <h3>하위 섹션 A-1</h3>
    <h3>하위 섹션 A-2</h3>
  <h2>섹션 B</h2>
    <h3>하위 섹션 B-1</h3>

스크린 리더 사용자의 67%가 제목으로 페이지를 탐색합니다. 제목 레벨을 건너뛰면 문서 구조를 파악하기 어렵습니다.


4. ARIA: 접근성 의미 추가

ARIA의 첫 번째 규칙

ARIA의 첫 번째 규칙: ARIA를 사용하지 마세요. 네이티브 HTML 요소로 충분하다면 ARIA가 필요 없습니다.

<!-- ARIA 불필요: 네이티브 요소로 충분 -->
<button>삭제</button>                    <!-- role="button" 불필요 -->
<input type="checkbox" />                <!-- role="checkbox" 불필요 -->
<nav>                                    <!-- role="navigation" 불필요 -->

<!-- ARIA 필요: 네이티브 요소가 없는 경우 -->
<div role="tablist">
  <button role="tab" aria-selected="true">탭 1</button>
  <button role="tab" aria-selected="false">탭 2</button>
</div>
<div role="tabpanel">탭 1 내용</div>

핵심 ARIA 속성

<!-- aria-label: 시각적 텍스트가 없는 요소에 레이블 제공 -->
<button aria-label="메뉴 닫기">
  <svg><!-- X 아이콘 --></svg>
</button>

<!-- aria-labelledby: 다른 요소의 텍스트를 레이블로 참조 -->
<h2 id="cart-heading">장바구니</h2>
<ul aria-labelledby="cart-heading">
  <li>상품 1</li>
  <li>상품 2</li>
</ul>

<!-- aria-describedby: 추가 설명 연결 -->
<input
  type="password"
  aria-describedby="pw-hint"
/>
<p id="pw-hint">8자 이상, 특수문자 포함</p>

<!-- aria-live: 동적 콘텐츠 변경 알림 -->
<div aria-live="polite">
  장바구니에 3개 상품이 있습니다.
</div>

<!-- aria-expanded: 확장/축소 상태 -->
<button aria-expanded="false" aria-controls="menu">
  메뉴
</button>
<ul id="menu" hidden>...</ul>

<!-- aria-hidden: 스크린 리더에서 숨기기 -->
<span aria-hidden="true">🔥</span>
<span class="sr-only">인기</span>

Live Regions

<!-- aria-live="polite": 현재 읽기를 마친 후 알림 -->
<div aria-live="polite" aria-atomic="true">
  검색 결과: 42건
</div>

<!-- aria-live="assertive": 즉시 알림 (에러 등) -->
<div role="alert" aria-live="assertive">
  세션이 만료되었습니다. 다시 로그인해 주세요.
</div>

<!-- role="status": 상태 메시지 (polite와 유사) -->
<div role="status">
  파일 업로드 완료
</div>

<!-- role="log": 채팅 메시지 등 -->
<div role="log" aria-live="polite">
  <!-- 새 메시지가 추가됨 -->
</div>

5. 키보드 접근성

포커스 관리의 기본

모든 대화형 요소는 키보드로 접근 가능해야 합니다.

동작
Tab다음 포커스 가능한 요소로 이동
Shift + Tab이전 포커스 가능한 요소로 이동
Enter링크 활성화, 버튼 클릭
Space버튼 클릭, 체크박스 토글
Escape모달/팝업 닫기
화살표 키메뉴, 탭, 라디오 그룹 내 이동
<!-- 페이지 최상단에 배치 -->
<a href="#main-content" class="skip-link">
  본문으로 건너뛰기
</a>

<nav>
  <!-- 긴 내비게이션 메뉴 -->
</nav>

<main id="main-content" tabindex="-1">
  <!-- 메인 콘텐츠 -->
</main>
.skip-link {
  position: absolute;
  left: -9999px;
  top: auto;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.skip-link:focus {
  position: fixed;
  top: 10px;
  left: 10px;
  width: auto;
  height: auto;
  padding: 12px 24px;
  background: #000;
  color: #fff;
  z-index: 9999;
  font-size: 1rem;
}

Focus Trap (포커스 트랩)

모달이 열렸을 때 포커스가 모달 안에서만 순환해야 합니다.

function useFocusTrap(containerRef: React.RefObject<HTMLElement>) {
  useEffect(() => {
    const container = containerRef.current
    if (!container) return

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

    const focusableElements = container.querySelectorAll(focusableSelector)
    const firstElement = focusableElements[0] as HTMLElement
    const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement

    function handleKeyDown(e: KeyboardEvent) {
      if (e.key !== 'Tab') return

      if (e.shiftKey) {
        if (document.activeElement === firstElement) {
          e.preventDefault()
          lastElement.focus()
        }
      } else {
        if (document.activeElement === lastElement) {
          e.preventDefault()
          firstElement.focus()
        }
      }
    }

    container.addEventListener('keydown', handleKeyDown)
    firstElement?.focus()

    return () => container.removeEventListener('keydown', handleKeyDown)
  }, [containerRef])
}

Tab 순서 관리

<!-- tabindex 값 -->
<!-- tabindex="0": 자연스러운 순서에 포함 -->
<div role="button" tabindex="0">커스텀 버튼</div>

<!-- tabindex="-1": 프로그래밍 방식으로만 포커스 가능 -->
<div id="error-message" tabindex="-1">에러 발생!</div>

<!-- tabindex 양수 사용 금지! 순서가 꼬입니다 -->
<!-- 나쁨: tabindex="1", tabindex="2", tabindex="3" -->

6. 색상과 대비

대비율 요구사항

텍스트 종류AA 레벨AAA 레벨
일반 텍스트 (14px 미만)4.5:17:1
큰 텍스트 (18px 이상 또는 14px 볼드)3:14.5:1
UI 컴포넌트, 그래픽3:1-

CSS로 대비 확보

/* 좋은 대비: #333 on #fff = 12.63:1 */
body {
  color: #333333;
  background-color: #ffffff;
}

/* 링크: 주변 텍스트와 3:1 대비 + 밑줄 또는 다른 시각적 단서 */
a {
  color: #0066cc;          /* #333과 3:1 이상 대비 */
  text-decoration: underline;
}

/* 포커스 인디케이터: 3:1 대비 필수 */
:focus-visible {
  outline: 3px solid #1a73e8;
  outline-offset: 2px;
}

/* 다크 모드에서도 대비 유지 */
@media (prefers-color-scheme: dark) {
  body {
    color: #e0e0e0;        /* #121212와 13.28:1 */
    background-color: #121212;
  }

  a {
    color: #8ab4f8;        /* #e0e0e0와 3:1 이상 */
  }
}

색맹 대응

/* 색상만으로 정보를 전달하지 않기 */

/* 나쁨: 색상만으로 에러 표시 */
.error-field {
  border-color: red;
}

/* 좋음: 색상 + 아이콘 + 텍스트 */
.error-field {
  border-color: #d32f2f;
  border-width: 2px;
}

.error-field::before {
  content: "⚠ ";
}

.error-message {
  color: #d32f2f;
  font-weight: bold;
}

대비 확인 도구

  • Chrome DevTools: 요소 검사 시 대비 비율 표시
  • axe DevTools: 전체 페이지 대비 검사
  • Colour Contrast Analyser (CCA): 독립 실행형 도구
  • Stark: Figma/Sketch 플러그인

7. 이미지와 미디어

alt 텍스트 가이드

<!-- 정보성 이미지: 내용을 설명 -->
<img src="chart.png" alt="2025년 매출 추이: 1분기 100만, 2분기 150만, 3분기 200만" />

<!-- 장식성 이미지: 빈 alt -->
<img src="decorative-line.png" alt="" />

<!-- 기능성 이미지 (링크/버튼): 동작을 설명 -->
<a href="/home">
  <img src="logo.png" alt="홈으로 이동" />
</a>

<!-- 복잡한 이미지: 긴 설명 제공 -->
<figure>
  <img src="infographic.png" alt="접근성 통계 인포그래픽" aria-describedby="info-desc" />
  <figcaption id="info-desc">
    전 세계 10억 명이 장애를 가지고 있으며,
    웹사이트의 97%가 접근성 오류를 포함합니다.
    가장 흔한 오류는 낮은 색상 대비(83%)입니다.
  </figcaption>
</figure>

<!-- SVG 접근성 -->
<svg role="img" aria-labelledby="svg-title">
  <title id="svg-title">다운로드 아이콘</title>
  <path d="..." />
</svg>

비디오 접근성

<video controls>
  <source src="tutorial.mp4" type="video/mp4" />
  <!-- 자막 (캡션) -->
  <track kind="captions" src="captions-ko.vtt" srclang="ko" label="한국어" default />
  <track kind="captions" src="captions-en.vtt" srclang="en" label="English" />
  <!-- 음성 해설 -->
  <track kind="descriptions" src="descriptions-ko.vtt" srclang="ko" label="음성 해설" />
</video>

오디오 콘텐츠

모든 오디오 콘텐츠에는 텍스트 대안(트랜스크립트)이 필요합니다.


8. 폼 접근성

레이블과 입력 연결

<!-- 방법 1: for/id 연결 (권장) -->
<label for="email">이메일 주소</label>
<input type="email" id="email" name="email" autocomplete="email" />

<!-- 방법 2: label로 감싸기 -->
<label>
  이메일 주소
  <input type="email" name="email" autocomplete="email" />
</label>

<!-- 방법 3: aria-labelledby -->
<span id="email-label">이메일 주소</span>
<input type="email" aria-labelledby="email-label" autocomplete="email" />

에러 메시지와 유효성 검사

<div class="form-group">
  <label for="password">비밀번호</label>
  <input
    type="password"
    id="password"
    aria-describedby="pw-requirements pw-error"
    aria-invalid="true"
    autocomplete="new-password"
  />
  <p id="pw-requirements" class="hint">
    8자 이상, 대소문자, 숫자, 특수문자 포함
  </p>
  <p id="pw-error" class="error" role="alert">
    비밀번호가 요구사항을 충족하지 않습니다.
  </p>
</div>

필수 필드

<!-- aria-required + 시각적 표시 -->
<label for="name">
  이름 <span aria-hidden="true" class="required">*</span>
</label>
<input
  type="text"
  id="name"
  required
  aria-required="true"
  autocomplete="name"
/>
<p class="form-note">* 표시는 필수 항목입니다</p>

자동 완성 (autocomplete)

<!-- WCAG 1.3.5: autocomplete 속성 사용 -->
<input type="text" autocomplete="given-name" />  <!-- 이름 -->
<input type="text" autocomplete="family-name" />  <!-- 성 -->
<input type="email" autocomplete="email" />        <!-- 이메일 -->
<input type="tel" autocomplete="tel" />            <!-- 전화번호 -->
<input type="text" autocomplete="street-address" /> <!-- 주소 -->

9. React/Next.js 접근성 패턴

SPA에서의 포커스 관리

SPA(Single Page Application)에서는 페이지 전환 시 포커스가 자동으로 이동하지 않습니다.

// 라우트 변경 시 포커스 이동
function useRouteAnnounce() {
  const pathname = usePathname()
  const announceRef = useRef<HTMLDivElement>(null)

  useEffect(() => {
    // 메인 콘텐츠로 포커스 이동
    const main = document.querySelector('main')
    if (main) {
      main.setAttribute('tabindex', '-1')
      main.focus()
    }
  }, [pathname])

  return (
    <div
      ref={announceRef}
      role="status"
      aria-live="polite"
      className="sr-only"
    >
      페이지가 로드되었습니다
    </div>
  )
}

라우트 변경 알림

// Next.js App Router: 라우트 변경 알림
'use client'
import { usePathname } from 'next/navigation'
import { useEffect, useState } from 'react'

function RouteAnnouncer() {
  const pathname = usePathname()
  const [announcement, setAnnouncement] = useState('')

  useEffect(() => {
    const pageTitle = document.title
    setAnnouncement(`${pageTitle} 페이지로 이동했습니다`)
  }, [pathname])

  return (
    <div
      role="status"
      aria-live="assertive"
      aria-atomic="true"
      className="sr-only"
    >
      {announcement}
    </div>
  )
}

접근성 있는 모달 (Dialog)

'use client'
import { useEffect, useRef } from 'react'

interface DialogProps {
  isOpen: boolean
  onClose: () => void
  title: string
  children: React.ReactNode
}

function AccessibleDialog({ isOpen, onClose, title, children }: DialogProps) {
  const dialogRef = useRef<HTMLDialogElement>(null)
  const previousFocusRef = useRef<HTMLElement | null>(null)

  useEffect(() => {
    const dialog = dialogRef.current
    if (!dialog) return

    if (isOpen) {
      previousFocusRef.current = document.activeElement as HTMLElement
      dialog.showModal()
    } else {
      dialog.close()
      previousFocusRef.current?.focus()
    }
  }, [isOpen])

  return (
    <dialog
      ref={dialogRef}
      aria-labelledby="dialog-title"
      onClose={onClose}
    >
      <h2 id="dialog-title">{title}</h2>
      {children}
      <button onClick={onClose}>닫기</button>
    </dialog>
  )
}

Radix UI / Headless UI 활용

import * as Dialog from '@radix-ui/react-dialog'

// Radix UI는 접근성을 자동으로 처리합니다
function MyDialog() {
  return (
    <Dialog.Root>
      <Dialog.Trigger asChild>
        <button>프로필 편집</button>
      </Dialog.Trigger>
      <Dialog.Portal>
        <Dialog.Overlay className="overlay" />
        <Dialog.Content className="content">
          <Dialog.Title>프로필 편집</Dialog.Title>
          <Dialog.Description>
            프로필 정보를 변경하세요.
          </Dialog.Description>
          {/* 폼 필드 */}
          <Dialog.Close asChild>
            <button aria-label="닫기">X</button>
          </Dialog.Close>
        </Dialog.Content>
      </Dialog.Portal>
    </Dialog.Root>
  )
}

스크린 리더 전용 텍스트

/* sr-only 유틸리티 클래스 */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border-width: 0;
}
// 사용 예시
<button>
  <TrashIcon />
  <span className="sr-only">항목 삭제</span>
</button>

<a href="/cart">
  <CartIcon />
  <span className="sr-only">장바구니 (3개 상품)</span>
</a>

10. 테스팅과 자동화

axe-core로 자동 테스트

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

expect.extend(toHaveNoViolations)

describe('Button', () => {
  it('접근성 위반 없음', async () => {
    const { container } = render(<Button>클릭</Button>)
    const results = await axe(container)
    expect(results).toHaveNoViolations()
  })
})

Playwright + axe 통합 테스트

import { test, expect } from '@playwright/test'
import AxeBuilder from '@axe-core/playwright'

test('홈페이지 접근성', async ({ page }) => {
  await page.goto('/')

  const accessibilityScanResults = await new AxeBuilder({ page })
    .withTags(['wcag2a', 'wcag2aa', 'wcag22aa'])
    .analyze()

  expect(accessibilityScanResults.violations).toEqual([])
})

// 키보드 내비게이션 테스트
test('키보드로 메뉴 탐색', async ({ page }) => {
  await page.goto('/')

  // Tab으로 Skip Link로 이동
  await page.keyboard.press('Tab')
  const skipLink = page.getByText('본문으로 건너뛰기')
  await expect(skipLink).toBeFocused()

  // Enter로 Skip Link 활성화
  await page.keyboard.press('Enter')
  const main = page.locator('main')
  await expect(main).toBeFocused()
})

Lighthouse 접근성 점수

# CI에서 Lighthouse 실행
npx lighthouse http://localhost:3000 \
  --only-categories=accessibility \
  --output=json \
  --output-path=./lighthouse-report.json
// CI 파이프라인에서 접근성 점수 확인
const report = JSON.parse(fs.readFileSync('./lighthouse-report.json', 'utf-8'))
const accessibilityScore = report.categories.accessibility.score * 100

if (accessibilityScore < 90) {
  console.error(`접근성 점수 ${accessibilityScore}점 - 90점 이상 필요`)
  process.exit(1)
}

스크린 리더 수동 테스트 체크리스트

항목VoiceOver (Mac)NVDA (Windows)TalkBack (Android)
페이지 제목 읽기Cmd + F5Insert + T자동
랜드마크 탐색Rotor (VO + U)D/Shift+D스와이프
제목 탐색VO + Cmd + HH/Shift+H스와이프
폼 필드VO + TabTab터치 탐색
링크 목록RotorInsert + F7메뉴

11. 법적 요구사항과 규정

미국: ADA와 Section 508

  • ADA Title III: 웹사이트는 "공공 편의시설" 적용. WCAG 2.1 AA 요구
  • Section 508: 연방 정부 웹사이트 의무. WCAG 2.0 AA 기준
  • 소송 현황: 2023년 웹 접근성 소송 4,600건 이상

EU: European Accessibility Act (EAA)

  • 2025년 6월 28일 시행
  • 디지털 서비스 제공 기업 대상
  • WCAG 2.1 AA 이상 요구
  • 위반 시 벌금 부과

한국: 장애인차별금지법

  • 장애인차별금지 및 권리구제 등에 관한 법률 (2008년)
  • 웹 접근성 인증마크: 한국웹접근성인증평가원
  • 공공기관 의무, 민간으로 확대 적용
  • 한국형 웹 콘텐츠 접근성 지침 (KWCAG) 2.2: WCAG 2.2 기반

접근성 성명서(Accessibility Statement)

<!-- 웹사이트에 접근성 성명서 포함 권장 -->
<h1>접근성 성명서</h1>
<p>
  저희는 모든 사용자에게 접근 가능한 웹 경험을 제공하기 위해
  WCAG 2.2 AA 기준을 준수하고 있습니다.
</p>
<p>
  접근성 관련 문제를 발견하시면
  <a href="mailto:a11y@example.com">a11y@example.com</a>으로
  연락해 주세요.
</p>

12. 면접 퀴즈

Q1. WCAG의 4가지 원칙(POUR)을 설명하고, 각각의 예시를 들어주세요.

Perceivable(인지 가능): 모든 정보를 인지할 수 있어야 합니다. 이미지에 alt 텍스트 제공, 비디오에 자막 추가가 예시입니다. Operable(운용 가능): 모든 기능을 조작할 수 있어야 합니다. 키보드만으로 모든 기능 사용 가능, 충분한 시간 제공이 예시입니다. Understandable(이해 가능): 콘텐츠와 UI를 이해할 수 있어야 합니다. 명확한 에러 메시지, 일관된 내비게이션이 예시입니다. Robust(견고): 다양한 기술에서 작동해야 합니다. 유효한 HTML, 보조 기술 호환이 예시입니다.

Q2. ARIA의 첫 번째 규칙은 무엇이며, 왜 중요한가요?

ARIA의 첫 번째 규칙은 "네이티브 HTML 요소로 충분하다면 ARIA를 사용하지 마세요"입니다. 예를 들어 버튼에 role="button"을 추가하는 것은 불필요합니다. 네이티브 HTML 요소는 이미 접근성 의미, 키보드 동작, 포커스 관리가 내장되어 있기 때문입니다. ARIA를 잘못 사용하면 오히려 접근성을 해칠 수 있습니다.

Q3. 색상 대비 비율 4.5:1과 3:1은 각각 어떤 상황에 적용되나요?

4.5:1은 일반 크기 텍스트(18px 미만)에 대한 AA 레벨 요구사항입니다. 3:1은 큰 텍스트(18px 이상 또는 14px 볼드)와 UI 컴포넌트(버튼 테두리, 입력 필드 등)에 대한 AA 레벨 요구사항입니다. AAA 레벨은 일반 텍스트 7:1, 큰 텍스트 4.5:1을 요구합니다.

Q4. SPA에서 라우트 변경 시 접근성을 어떻게 보장하나요?

SPA에서 클라이언트 사이드 라우팅은 브라우저의 기본 페이지 로드와 달리 스크린 리더에 자동 알림을 제공하지 않습니다. 해결 방법으로는 라우트 변경 시 메인 콘텐츠로 포커스 이동, aria-live 리전으로 새 페이지 제목 알림, 문서 title 업데이트, Skip Link 제공 등이 있습니다. Next.js의 App Router는 내장 라우트 알림 기능을 제공합니다.

Q5. axe-core를 CI/CD 파이프라인에 통합하는 방법과 한계는?

axe-core는 jest-axe(유닛 테스트)나 @axe-core/playwright(E2E 테스트)로 CI에 통합할 수 있습니다. WCAG 태그로 필터링하여 특정 기준만 검사하고, 위반이 있으면 빌드를 실패시킵니다. 한계로는 자동 도구가 접근성 문제의 약 30-40%만 감지할 수 있다는 점입니다. 키보드 사용성, 스크린 리더 호환성, 인지적 접근성 등은 수동 테스트가 필수입니다.


References

  1. WCAG 2.2 - W3C Recommendation
  2. WAI-ARIA 1.2 Specification
  3. MDN Web Accessibility Guide
  4. A11y Project Checklist
  5. Deque axe-core
  6. WebAIM Million Report
  7. Radix UI Accessibility
  8. React Accessibility Docs
  9. Next.js Accessibility
  10. Inclusive Components by Heydon Pickering
  11. EU European Accessibility Act
  12. 한국웹접근성인증평가원
  13. Chrome DevTools Accessibility
  14. Stark Accessibility Tools