11. μ›Ή μ ‘κ·Όμ„± (Web Accessibility - A11y)

11. μ›Ή μ ‘κ·Όμ„± (Web Accessibility - A11y)

ν•™μŠ΅ λͺ©ν‘œ

  • μ›Ή μ ‘κ·Όμ„±μ˜ μ€‘μš”μ„±κ³Ό 법적 μš”κ΅¬μ‚¬ν•­ 이해
  • WCAG κ°€μ΄λ“œλΌμΈκ³Ό μ€€μˆ˜ μˆ˜μ€€ ν•™μŠ΅
  • ARIA 속성을 ν™œμš©ν•œ μ ‘κ·Όμ„± ν–₯상
  • ν‚€λ³΄λ“œ λ„€λΉ„κ²Œμ΄μ…˜ κ΅¬ν˜„
  • 슀크린 리더 ν˜Έν™˜μ„± ν…ŒμŠ€νŠΈ

λͺ©μ°¨

  1. μ ‘κ·Όμ„± κ°œμš”
  2. WCAG κ°€μ΄λ“œλΌμΈ
  3. μ‹œλ§¨ν‹± HTML
  4. ARIA 속성
  5. ν‚€λ³΄λ“œ μ ‘κ·Όμ„±
  6. ν…ŒμŠ€νŠΈμ™€ 도ꡬ
  7. μ—°μŠ΅ 문제

1. μ ‘κ·Όμ„± κ°œμš”

1.1 μ›Ή μ ‘κ·Όμ„±μ΄λž€?

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    μ›Ή μ ‘κ·Όμ„± μ •μ˜                                β”‚
β”‚                                                                 β”‚
β”‚   "μž₯μ•  여뢀와 관계없이 λͺ¨λ“  μ‚¬λžŒμ΄ μ›Ή μ½˜ν…μΈ μ™€ κΈ°λŠ₯을           β”‚
β”‚    μΈμ‹ν•˜κ³ , μ΄ν•΄ν•˜κ³ , νƒμƒ‰ν•˜κ³ , μƒν˜Έμž‘μš©ν•  수 μžˆλ„λ‘ ν•˜λŠ” 것"   β”‚
β”‚                                                                 β”‚
β”‚   λŒ€μƒ:                                                         β”‚
β”‚   - μ‹œκ° μž₯μ•  (μ „λ§Ή, μ €μ‹œλ ₯, 색맹)                               β”‚
β”‚   - 청각 μž₯μ•  (농아, λ‚œμ²­)                                       β”‚
β”‚   - μš΄λ™ μž₯μ•  (마우슀 μ‚¬μš© λΆˆκ°€)                                 β”‚
β”‚   - 인지 μž₯μ•  (ν•™μŠ΅ μž₯μ• , 집쀑λ ₯ μž₯μ• )                           β”‚
β”‚   - μΌμ‹œμ  μž₯μ•  (뢀상, 밝은 ν™˜κ²½)                                β”‚
β”‚   - 상황적 μ œμ•½ (μž‘μ€ ν™”λ©΄, 느린 μ—°κ²°)                           β”‚
β”‚                                                                 β”‚
β”‚   "a11y" = accessibility (a + 11κΈ€μž + y)                       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

1.2 μ ‘κ·Όμ„±μ˜ μ€‘μš”μ„±

법적 μš”κ΅¬μ‚¬ν•­:
- ν•œκ΅­: μž₯μ• μΈμ°¨λ³„κΈˆμ§€λ²•, μ›Ή μ ‘κ·Όμ„± μΈμ¦μ œλ„ (KWCAG)
- λ―Έκ΅­: ADA (Americans with Disabilities Act), Section 508
- 유럽: EN 301 549, European Accessibility Act

λΉ„μ¦ˆλ‹ˆμŠ€ κ°€μΉ˜:
- 더 넓은 μ‚¬μš©μž 기반 (μ „ 세계 인ꡬ의 15%κ°€ μž₯μ• λ₯Ό 가짐)
- SEO ν–₯상 (검색 엔진도 ν…μŠ€νŠΈ 기반)
- 법적 리슀크 κ°μ†Œ
- λΈŒλžœλ“œ 이미지 κ°œμ„ 
- λͺ¨λ“  μ‚¬μš©μžμ˜ UX ν–₯상

2. WCAG κ°€μ΄λ“œλΌμΈ

2.1 WCAG 원칙 (POUR)

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    WCAG 4λŒ€ 원칙                                 β”‚
β”‚                                                                 β”‚
β”‚   P - Perceivable (μΈμ‹μ˜ μš©μ΄μ„±)                               β”‚
β”‚       μ½˜ν…μΈ λ₯Ό μ‚¬μš©μžκ°€ 인식할 수 μžˆμ–΄μ•Ό 함                      β”‚
β”‚       - λŒ€μ²΄ ν…μŠ€νŠΈ                                             β”‚
β”‚       - μžλ§‰, μ˜€λ””μ˜€ μ„€λͺ…                                       β”‚
β”‚       - 색상 λŒ€λΉ„                                               β”‚
β”‚                                                                 β”‚
β”‚   O - Operable (운용의 μš©μ΄μ„±)                                  β”‚
β”‚       UI μ»΄ν¬λ„ŒνŠΈλ₯Ό μ‘°μž‘ν•  수 μžˆμ–΄μ•Ό 함                         β”‚
β”‚       - ν‚€λ³΄λ“œ μ ‘κ·Όμ„±                                           β”‚
β”‚       - μΆ©λΆ„ν•œ μ‹œκ°„                                             β”‚
β”‚       - λ°œμž‘ 예방                                               β”‚
β”‚                                                                 β”‚
β”‚   U - Understandable (μ΄ν•΄μ˜ μš©μ΄μ„±)                            β”‚
β”‚       μ½˜ν…μΈ κ°€ 이해 κ°€λŠ₯ν•΄μ•Ό 함                                  β”‚
β”‚       - 읽기 κ°€λŠ₯                                               β”‚
β”‚       - 예츑 κ°€λŠ₯                                               β”‚
β”‚       - μž…λ ₯ 지원                                               β”‚
β”‚                                                                 β”‚
β”‚   R - Robust (견고성)                                           β”‚
β”‚       λ‹€μ–‘ν•œ κΈ°μˆ μ—μ„œ μ ‘κ·Ό κ°€λŠ₯ν•΄μ•Ό 함                           β”‚
β”‚       - ν˜Έν™˜μ„±                                                  β”‚
β”‚       - 보쑰 기술 지원                                          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

2.2 μ€€μˆ˜ μˆ˜μ€€

레벨 A (ν•„μˆ˜):
- 이미지에 λŒ€μ²΄ ν…μŠ€νŠΈ
- ν‚€λ³΄λ“œλ‘œ λͺ¨λ“  κΈ°λŠ₯ μ ‘κ·Ό κ°€λŠ₯
- κΉœλΉ‘μ΄λŠ” μ½˜ν…μΈ  μ œν•œ

레벨 AA (ꢌμž₯ - λŒ€λΆ€λΆ„μ˜ 법적 μš”κ΅¬μ‚¬ν•­):
- 색상 λŒ€λΉ„ 4.5:1 이상
- ν…μŠ€νŠΈ 크기 쑰절 κ°€λŠ₯
- μΌκ΄€λœ λ„€λΉ„κ²Œμ΄μ…˜
- 였λ₯˜ 식별 및 μ„€λͺ…

레벨 AAA (μ΅œμƒμœ„):
- 색상 λŒ€λΉ„ 7:1 이상
- μˆ˜μ–΄ 톡역
- λͺ¨λ“  μ•½μ–΄ μ„€λͺ…

3. μ‹œλ§¨ν‹± HTML

3.1 μ‹œλ§¨ν‹± μš”μ†Œ μ‚¬μš©

<!-- μ’‹μ§€ μ•Šμ€ 예 -->
<div class="header">
  <div class="nav">
    <div class="nav-item">ν™ˆ</div>
    <div class="nav-item">μ†Œκ°œ</div>
  </div>
</div>
<div class="main">
  <div class="article">
    <div class="title">제λͺ©</div>
    <div class="content">λ‚΄μš©</div>
  </div>
</div>
<div class="footer">ν‘Έν„°</div>

<!-- 쒋은 예 - μ‹œλ§¨ν‹± HTML -->
<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>
<footer>ν‘Έν„°</footer>

3.2 제λͺ© ꡬ쑰 (Heading Hierarchy)

<!-- μ˜¬λ°”λ₯Έ 제λͺ© 계측 -->
<h1>μ›Ήμ‚¬μ΄νŠΈ 제λͺ©</h1>
  <h2>μ„Ήμ…˜ 1</h2>
    <h3>ν•˜μœ„ μ„Ήμ…˜ 1.1</h3>
    <h3>ν•˜μœ„ μ„Ήμ…˜ 1.2</h3>
  <h2>μ„Ήμ…˜ 2</h2>
    <h3>ν•˜μœ„ μ„Ήμ…˜ 2.1</h3>
      <h4>μ„ΈλΆ€ ν•­λͺ© 2.1.1</h4>

<!-- 잘λͺ»λœ 예 - 레벨 κ±΄λ„ˆλ›°κΈ° -->
<h1>제λͺ©</h1>
<h3>λ°”λ‘œ h3둜 κ±΄λ„ˆλ›°λ©΄ μ•ˆ 됨</h3>

<!-- νŽ˜μ΄μ§€λ‹Ή h1은 ν•˜λ‚˜λ§Œ -->

3.3 이미지 μ ‘κ·Όμ„±

<!-- 정보λ₯Ό μ „λ‹¬ν•˜λŠ” 이미지 -->
<img src="chart.png" alt="2024λ…„ 맀좜 κ·Έλž˜ν”„: 1λΆ„κΈ° 100λ§Œμ›, 2λΆ„κΈ° 150λ§Œμ›, 3λΆ„κΈ° 200λ§Œμ›">

<!-- μž₯μ‹μš© 이미지 (λŒ€μ²΄ ν…μŠ€νŠΈ 비움) -->
<img src="decoration.png" alt="" role="presentation">

<!-- λ³΅μž‘ν•œ 이미지 (κΈ΄ μ„€λͺ… 제곡) -->
<figure>
  <img src="complex-diagram.png" alt="μ‹œμŠ€ν…œ μ•„ν‚€ν…μ²˜ λ‹€μ΄μ–΄κ·Έλž¨" aria-describedby="diagram-desc">
  <figcaption id="diagram-desc">
    이 λ‹€μ΄μ–΄κ·Έλž¨μ€ ν΄λΌμ΄μ–ΈνŠΈ, μ›Ή μ„œλ²„, λ°μ΄ν„°λ² μ΄μŠ€ κ°„μ˜
    데이터 흐름을 λ³΄μ—¬μ€λ‹ˆλ‹€...
  </figcaption>
</figure>

<!-- 링크 λ‚΄ 이미지 -->
<a href="/products">
  <img src="product.jpg" alt="μ‹ μ œν’ˆ 보기">
</a>

3.4 폼 μ ‘κ·Όμ„±

<!-- λͺ…μ‹œμ  λ ˆμ΄λΈ” μ—°κ²° -->
<label for="email">이메일:</label>
<input type="email" id="email" name="email" required>

<!-- κ·Έλ£Ήν™”λœ 폼 μš”μ†Œ -->
<fieldset>
  <legend>배솑 μ£Όμ†Œ</legend>

  <label for="street">λ„λ‘œλͺ… μ£Όμ†Œ:</label>
  <input type="text" id="street" name="street">

  <label for="city">μ‹œ/κ΅°/ꡬ:</label>
  <input type="text" id="city" name="city">
</fieldset>

<!-- 였λ₯˜ λ©”μ‹œμ§€ μ—°κ²° -->
<label for="password">λΉ„λ°€λ²ˆν˜Έ:</label>
<input
  type="password"
  id="password"
  aria-describedby="password-error password-hint"
  aria-invalid="true"
>
<span id="password-hint">8자 이상 μž…λ ₯ν•˜μ„Έμš”</span>
<span id="password-error" role="alert">λΉ„λ°€λ²ˆν˜Έκ°€ λ„ˆλ¬΄ μ§§μŠ΅λ‹ˆλ‹€</span>

4. ARIA 속성

4.1 ARIA κΈ°λ³Έ κ°œλ…

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    ARIA 속성 λΆ„λ₯˜                                β”‚
β”‚                                                                 β”‚
β”‚   μ—­ν•  (Roles):                                                 β”‚
β”‚   - μš”μ†Œμ˜ μœ ν˜•/λͺ©μ  μ •μ˜                                        β”‚
β”‚   - role="button", role="navigation", role="alert"             β”‚
β”‚                                                                 β”‚
β”‚   μƒνƒœ (States):                                                β”‚
β”‚   - μš”μ†Œμ˜ ν˜„μž¬ μƒνƒœ (λ³€κ²½ κ°€λŠ₯)                                 β”‚
β”‚   - aria-expanded, aria-checked, aria-selected                 β”‚
β”‚                                                                 β”‚
β”‚   속성 (Properties):                                            β”‚
β”‚   - μš”μ†Œμ˜ νŠΉμ„± (보톡 κ³ μ •)                                      β”‚
β”‚   - aria-label, aria-labelledby, aria-describedby              β”‚
β”‚                                                                 β”‚
β”‚   첫 번째 κ·œμΉ™: λ„€μ΄ν‹°λΈŒ HTML이 κ°€λŠ₯ν•˜λ©΄ ARIA μ‚¬μš©ν•˜μ§€ 말 것     β”‚
β”‚   <button> λŒ€μ‹  <div role="button">을 μ“°μ§€ 말 것                β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

4.2 μ£Όμš” ARIA 속성

<!-- aria-label: μ ‘κ·Ό κ°€λŠ₯ν•œ 이름 제곡 -->
<button aria-label="메뉴 λ‹«κΈ°">
  <svg><!-- X μ•„μ΄μ½˜ --></svg>
</button>

<!-- aria-labelledby: λ‹€λ₯Έ μš”μ†Œλ‘œ λ ˆμ΄λΈ” μ§€μ • -->
<h2 id="section-title">μ œν’ˆ λͺ©λ‘</h2>
<ul aria-labelledby="section-title">
  <li>μ œν’ˆ 1</li>
  <li>μ œν’ˆ 2</li>
</ul>

<!-- aria-describedby: μΆ”κ°€ μ„€λͺ… μ—°κ²° -->
<input type="text" aria-describedby="name-help">
<p id="name-help">이름을 ν•œκΈ€λ‘œ μž…λ ₯ν•˜μ„Έμš”</p>

<!-- aria-hidden: 보쑰 κΈ°μˆ μ—μ„œ μˆ¨κΉ€ -->
<span aria-hidden="true">β˜…</span> <!-- μž₯μ‹μš© μ•„μ΄μ½˜ -->
<span class="sr-only">별점 5점</span> <!-- 슀크린 λ¦¬λ”μš© ν…μŠ€νŠΈ -->

<!-- aria-live: 동적 μ½˜ν…μΈ  μ•Œλ¦Ό -->
<div aria-live="polite">μƒˆ λ©”μ‹œμ§€κ°€ λ„μ°©ν–ˆμŠ΅λ‹ˆλ‹€</div>
<div aria-live="assertive" role="alert">였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€!</div>

4.3 μƒνƒœ 관리

<!-- ν™•μž₯/μΆ•μ†Œ μƒνƒœ -->
<button
  aria-expanded="false"
  aria-controls="menu-content"
  id="menu-button"
>
  메뉴
</button>
<div id="menu-content" hidden>
  <!-- 메뉴 λ‚΄μš© -->
</div>

<script>
const button = document.getElementById('menu-button');
const content = document.getElementById('menu-content');

button.addEventListener('click', () => {
  const expanded = button.getAttribute('aria-expanded') === 'true';
  button.setAttribute('aria-expanded', !expanded);
  content.hidden = expanded;
});
</script>

<!-- 선택 μƒνƒœ -->
<ul role="listbox" aria-label="색상 선택">
  <li role="option" aria-selected="true">λΉ¨κ°•</li>
  <li role="option" aria-selected="false">νŒŒλž‘</li>
  <li role="option" aria-selected="false">초둝</li>
</ul>

<!-- λΉ„ν™œμ„±ν™” μƒνƒœ -->
<button aria-disabled="true">제좜 λΆˆκ°€</button>

4.4 라이브 리전 (Live Regions)

<!-- μƒνƒœ λ©”μ‹œμ§€ -->
<div role="status" aria-live="polite">
  3개 ν•­λͺ©μ΄ μž₯λ°”κ΅¬λ‹ˆμ— μΆ”κ°€λ˜μ—ˆμŠ΅λ‹ˆλ‹€.
</div>

<!-- κ²½κ³  λ©”μ‹œμ§€ -->
<div role="alert" aria-live="assertive">
  μ„Έμ…˜μ΄ λ§Œλ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€. λ‹€μ‹œ λ‘œκ·ΈμΈν•˜μ„Έμš”.
</div>

<!-- λ‘œλ”© μƒνƒœ -->
<div aria-busy="true" aria-live="polite">
  데이터 λ‘œλ”© 쀑...
</div>

<!-- 폴리트 vs μ–΄μ„œν‹°λΈŒ -->
<!-- polite: ν˜„μž¬ μž‘μ—… μ™„λ£Œ ν›„ μ•Œλ¦Ό (ꢌμž₯) -->
<!-- assertive: μ¦‰μ‹œ μ•Œλ¦Ό (κΈ΄κΈ‰ν•œ 경우만) -->

5. ν‚€λ³΄λ“œ μ ‘κ·Όμ„±

5.1 포컀슀 관리

<!-- 포컀슀 κ°€λŠ₯ μš”μ†Œ -->
<!-- μžλ™: a[href], button, input, select, textarea -->

<!-- tabindex μ‚¬μš© -->
<div tabindex="0">포컀슀 κ°€λŠ₯ν•œ div</div>
<div tabindex="-1">ν”„λ‘œκ·Έλž˜λ°μœΌλ‘œλ§Œ 포컀슀 κ°€λŠ₯</div>
<!-- tabindex > 0은 μ‚¬μš© 자제 (νƒ­ μˆœμ„œ ν˜Όλž€) -->

<!-- 포컀슀 ν‘œμ‹œ μŠ€νƒ€μΌ -->
<style>
/* κΈ°λ³Έ 포컀슀 μŠ€νƒ€μΌ 제거 κΈˆμ§€ */
:focus {
  outline: 2px solid #4A90D9;
  outline-offset: 2px;
}

/* 마우슀 클릭 μ‹œ 포컀슀 링 숨기기 (선택적) */
:focus:not(:focus-visible) {
  outline: none;
}

/* ν‚€λ³΄λ“œ 포컀슀 μ‹œμ—λ§Œ ν‘œμ‹œ */
:focus-visible {
  outline: 3px solid #4A90D9;
  outline-offset: 2px;
}
</style>

5.2 ν‚€λ³΄λ“œ λ„€λΉ„κ²Œμ΄μ…˜ νŒ¨ν„΄

<!-- μŠ€ν‚΅ 링크 -->
<a href="#main-content" class="skip-link">
  본문으둜 κ±΄λ„ˆλ›°κΈ°
</a>

<style>
.skip-link {
  position: absolute;
  top: -40px;
  left: 0;
  padding: 8px;
  background: #000;
  color: #fff;
  z-index: 100;
}
.skip-link:focus {
  top: 0;
}
</style>

<!-- 메뉴 νƒ­ νŒ¨λ„ -->
<div role="tablist" aria-label="μ œν’ˆ 정보">
  <button role="tab" aria-selected="true" aria-controls="panel-1" id="tab-1">
    μ„€λͺ…
  </button>
  <button role="tab" aria-selected="false" aria-controls="panel-2" id="tab-2">
    리뷰
  </button>
</div>

<div role="tabpanel" id="panel-1" aria-labelledby="tab-1">
  μ œν’ˆ μ„€λͺ…...
</div>
<div role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>
  리뷰 λ‚΄μš©...
</div>

5.3 포컀슀 트랩 (λͺ¨λ‹¬)

// λͺ¨λ‹¬ 포컀슀 트랩
function trapFocus(element) {
  const focusableElements = element.querySelectorAll(
    'a[href], button, textarea, input, select, [tabindex]:not([tabindex="-1"])'
  );
  const firstElement = focusableElements[0];
  const lastElement = focusableElements[focusableElements.length - 1];

  element.addEventListener('keydown', (e) => {
    if (e.key !== 'Tab') return;

    if (e.shiftKey) {
      // Shift + Tab
      if (document.activeElement === firstElement) {
        lastElement.focus();
        e.preventDefault();
      }
    } else {
      // Tab
      if (document.activeElement === lastElement) {
        firstElement.focus();
        e.preventDefault();
      }
    }
  });

  // 첫 μš”μ†Œμ— 포컀슀
  firstElement.focus();
}

5.4 ν‚€λ³΄λ“œ 단좕킀

<!-- accesskey (μ£Όμ˜ν•΄μ„œ μ‚¬μš©) -->
<button accesskey="s">μ €μž₯ (Alt+S)</button>

<!-- μ»€μŠ€ν…€ 단좕킀 κ΅¬ν˜„ -->
<script>
document.addEventListener('keydown', (e) => {
  // Ctrl/Cmd + K둜 검색
  if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
    e.preventDefault();
    document.getElementById('search').focus();
  }

  // Escape둜 λͺ¨λ‹¬ λ‹«κΈ°
  if (e.key === 'Escape') {
    closeModal();
  }
});
</script>

6. ν…ŒμŠ€νŠΈμ™€ 도ꡬ

6.1 μžλ™ν™” 도ꡬ

# Lighthouse (Chrome DevTools λ‚΄μž₯)
# Performance, Accessibility, SEO λ“± μΈ‘μ •

# axe DevTools (λΈŒλΌμš°μ € ν™•μž₯)
npm install @axe-core/react  # React ν”„λ‘œμ νŠΈμš©

# Pa11y (CLI 도ꡬ)
npm install -g pa11y
pa11y https://example.com

# eslint-plugin-jsx-a11y (React)
npm install eslint-plugin-jsx-a11y --save-dev

6.2 μˆ˜λ™ ν…ŒμŠ€νŠΈ 체크리슀트

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                 μˆ˜λ™ μ ‘κ·Όμ„± ν…ŒμŠ€νŠΈ 체크리슀트                     β”‚
β”‚                                                                 β”‚
β”‚ ν‚€λ³΄λ“œ ν…ŒμŠ€νŠΈ:                                                   β”‚
β”‚ β–‘ Tab ν‚€λ‘œ λͺ¨λ“  μƒν˜Έμž‘μš© μš”μ†Œμ— μ ‘κ·Ό κ°€λŠ₯                        β”‚
β”‚ β–‘ 포컀슀 ν‘œμ‹œκ°€ λͺ…ν™•ν•˜κ²Œ λ³΄μž„                                    β”‚
β”‚ β–‘ 논리적인 νƒ­ μˆœμ„œ                                               β”‚
β”‚ β–‘ ν‚€λ³΄λ“œ 트랩 μ—†μŒ (λͺ¨λ‹¬ μ œμ™Έ)                                   β”‚
β”‚ β–‘ Enter/Space둜 λ²„νŠΌ ν™œμ„±ν™”                                     β”‚
β”‚ β–‘ Escape둜 νŒμ—…/λͺ¨λ‹¬ λ‹«κΈ°                                       β”‚
β”‚                                                                 β”‚
β”‚ 슀크린 리더 ν…ŒμŠ€νŠΈ:                                              β”‚
β”‚ β–‘ 이미지 λŒ€μ²΄ ν…μŠ€νŠΈ 적절                                        β”‚
β”‚ β–‘ 제λͺ© ꡬ쑰 논리적                                               β”‚
β”‚ β–‘ 폼 λ ˆμ΄λΈ” μ—°κ²°                                                 β”‚
β”‚ β–‘ 였λ₯˜ λ©”μ‹œμ§€ 인식                                               β”‚
β”‚ β–‘ 동적 μ½˜ν…μΈ  μ•Œλ¦Ό                                               β”‚
β”‚                                                                 β”‚
β”‚ μ‹œκ° ν…ŒμŠ€νŠΈ:                                                     β”‚
β”‚ β–‘ 색상 λŒ€λΉ„ μΆ©λΆ„ (4.5:1 이상)                                   β”‚
β”‚ β–‘ μƒ‰μƒλ§ŒμœΌλ‘œ 정보 전달 μ•ˆ 함                                     β”‚
β”‚ β–‘ 200% ν™•λŒ€ μ‹œ 가독성                                           β”‚
β”‚ β–‘ μ• λ‹ˆλ©”μ΄μ…˜ μ œμ–΄ κ°€λŠ₯                                           β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

6.3 슀크린 리더 ν…ŒμŠ€νŠΈ

μ£Όμš” 슀크린 리더:
- NVDA (Windows, 무료)
- JAWS (Windows, 유료)
- VoiceOver (macOS/iOS, λ‚΄μž₯)
- TalkBack (Android, λ‚΄μž₯)

VoiceOver κΈ°λ³Έ λͺ…λ Ή (macOS):
- Cmd + F5: VoiceOver 켜기/끄기
- Ctrl + Option + λ°©ν–₯ν‚€: 탐색
- Ctrl + Option + Space: ν™œμ„±ν™”

NVDA κΈ°λ³Έ λͺ…λ Ή (Windows):
- Insert + Space: NVDA λͺ¨λ“œ μ „ν™˜
- Tab: λ‹€μŒ 포컀슀 κ°€λŠ₯ μš”μ†Œ
- H: λ‹€μŒ 제λͺ©
- B: λ‹€μŒ λ²„νŠΌ

7. μ—°μŠ΅ 문제

μ—°μŠ΅ 1: 이미지 μ ‘κ·Όμ„± κ°œμ„ 

λ‹€μŒ μ½”λ“œμ˜ 접근성을 κ°œμ„ ν•˜μ„Έμš”.

<!-- κ°œμ„  μ „ -->
<img src="sale-banner.jpg">
<img src="icon-cart.png" onclick="addToCart()">

<!-- κ°œμ„  ν›„ (μ˜ˆμ‹œ λ‹΅μ•ˆ) -->
<img src="sale-banner.jpg" alt="여름 세일 - μ „ ν’ˆλͺ© 30% 할인, 7μ›” 31μΌκΉŒμ§€">

<button type="button" onclick="addToCart()" aria-label="μž₯λ°”κ΅¬λ‹ˆμ— μΆ”κ°€">
  <img src="icon-cart.png" alt="">
</button>

μ—°μŠ΅ 2: 폼 μ ‘κ·Όμ„± κ°œμ„ 

λ‹€μŒ 폼의 접근성을 κ°œμ„ ν•˜μ„Έμš”.

<!-- κ°œμ„  μ „ -->
<form>
  <input type="text" placeholder="이름">
  <input type="email" placeholder="이메일">
  <div class="checkbox">
    <input type="checkbox"> μ•½κ΄€ λ™μ˜
  </div>
  <button>제좜</button>
</form>

<!-- κ°œμ„  ν›„ (μ˜ˆμ‹œ λ‹΅μ•ˆ) -->
<form>
  <div>
    <label for="name">이름 (ν•„μˆ˜)</label>
    <input type="text" id="name" name="name" required
           aria-describedby="name-help">
    <span id="name-help" class="help-text">μ‹€λͺ…을 μž…λ ₯ν•˜μ„Έμš”</span>
  </div>

  <div>
    <label for="email">이메일 (ν•„μˆ˜)</label>
    <input type="email" id="email" name="email" required>
  </div>

  <div>
    <input type="checkbox" id="terms" name="terms" required>
    <label for="terms">
      <a href="/terms">μ΄μš©μ•½κ΄€</a>에 λ™μ˜ν•©λ‹ˆλ‹€ (ν•„μˆ˜)
    </label>
  </div>

  <button type="submit">μ œμΆœν•˜κΈ°</button>
</form>

μ—°μŠ΅ 3: ν‚€λ³΄λ“œ μ ‘κ·Όμ„± κ΅¬ν˜„

λ“œλ‘­λ‹€μš΄ 메뉴에 ν‚€λ³΄λ“œ 접근성을 μΆ”κ°€ν•˜μ„Έμš”.

// μ˜ˆμ‹œ λ‹΅μ•ˆ
const dropdown = document.querySelector('.dropdown');
const button = dropdown.querySelector('button');
const menu = dropdown.querySelector('ul');
const items = menu.querySelectorAll('a');

button.addEventListener('keydown', (e) => {
  if (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown') {
    e.preventDefault();
    openMenu();
    items[0].focus();
  }
});

menu.addEventListener('keydown', (e) => {
  const currentIndex = Array.from(items).indexOf(document.activeElement);

  switch (e.key) {
    case 'ArrowDown':
      e.preventDefault();
      items[(currentIndex + 1) % items.length].focus();
      break;
    case 'ArrowUp':
      e.preventDefault();
      items[(currentIndex - 1 + items.length) % items.length].focus();
      break;
    case 'Escape':
      closeMenu();
      button.focus();
      break;
  }
});

λ‹€μŒ 단계

참고 자료

to navigate between lessons