11. μΉ μ κ·Όμ± (Web Accessibility - A11y)
11. μΉ μ κ·Όμ± (Web Accessibility - A11y)¶
νμ΅ λͺ©ν¶
- μΉ μ κ·Όμ±μ μ€μμ±κ³Ό λ²μ μꡬμ¬ν μ΄ν΄
- WCAG κ°μ΄λλΌμΈκ³Ό μ€μ μμ€ νμ΅
- ARIA μμ±μ νμ©ν μ κ·Όμ± ν₯μ
- ν€λ³΄λ λ€λΉκ²μ΄μ ꡬν
- μ€ν¬λ¦° 리λ νΈνμ± ν μ€νΈ
λͺ©μ°¨¶
- μ κ·Όμ± κ°μ
- WCAG κ°μ΄λλΌμΈ
- μλ§¨ν± HTML
- ARIA μμ±
- ν€λ³΄λ μ κ·Όμ±
- ν μ€νΈμ λꡬ
- μ°μ΅ λ¬Έμ
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;
}
});