์น ์ ๊ทผ์ฑ์ด๋? ๐ค1
์น ์ ๊ทผ์ฑ(Web Accessibility)์ ๋ชจ๋ ์ฌ์ฉ์, ํนํ ์ฅ์ ๋ฅผ ๊ฐ์ง ์ฌ๋๋ค์ด ์น ์ฝํ ์ธ ์ ๊ธฐ๋ฅ์ ์ ๊ทผํ๊ณ ์๋ก ์์ฉํ ์ ์๋๋ก ๋ณด์ฅํ๋ ๊ฒ์ ์๋ฏธํ๋ค.
์น ์ ๊ทผ์ฑ์ ์๊ฐ, ์ฒญ๊ฐ, ์ด๋ ์ฅ์ ๋ ๋ฌผ๋ก , ๋์ด๊ฐ ๋ง๊ฑฐ๋ ์ผ์์ ์ธ ์ฅ์ ๋ฅผ ๊ฐ์ง ์ฌ์ฉ์, ์ฌ์ง์ด๋ ์ ํ๋ ๋๋ฐ์ด์ค ํ๊ฒฝ์์๋ ์ค์ํ ์ญํ ์ ํ๋ค.
์๋ฅผ ๋ค์ด:
- ์๊ฐ ์ฅ์ ์ธ: ํ๋ฉด ์ฝ๊ธฐ ์ํํธ์จ์ด(Screen Reader)๋ฅผ ํตํด ์ฝํ ์ธ ์ ์ ๊ทผ.
- ์ด๋ ์ฅ์ ์ธ: ํค๋ณด๋๋ง์ผ๋ก ์น์ฌ์ดํธ ํ์.
- ์๊ฐ ์ด์์: ๋ช ํํ ๋๋น์ ๋น์ธ์ด์ ์ปค๋ฎค๋์ผ์ด์ ์ผ๋ก ์ ๋ณด ์ธ์ง.
์น ์ ๊ทผ์ฑ์ ์ค์์ฑ ๐
์น ์ ๊ทผ์ฑ์ ๋จ์ํ ๋ฒ์ ์๊ตฌ์ฌํญ์ ์ถฉ์กฑํ๋ ๊ฒ์ ๋์ด, ๋ชจ๋๋ฅผ ์ํ ํฌ์ฉ์ ์น์ ๋ง๋ ๋ค.
1. ๋ ๋ง์ ์ฌ์ฉ์์์ ์ฐ๊ฒฐ
์ ๊ทผ์ฑ์ด ์ข์ ์น์ฌ์ดํธ๋ ๋ ๋ง์ ์ฌ๋์๊ฒ ๋๋ฌํ ์ ์๋ค.
์ด๋ ์ฌ์ฉ์ ๊ธฐ๋ฐ์ ํ์ฅํ๊ณ ๋น์ฆ๋์ค ์ฑ์ฅ์๋ ๊ธฐ์ฌํ๋ค.
2. ๋ฒ์ ์๊ตฌ์ฌํญ ์ค์
๋ง์ ๊ตญ๊ฐ์์ **์น ์ ๊ทผ์ฑ ํ์ค(WCAG)**์ ๋ฒ์ ์ผ๋ก ์๊ตฌํ๊ณ ์์ผ๋ฉฐ, ์ด๋ฅผ ์ค์ํ์ง ์์ผ๋ฉด ๋ฒ๊ธ์ ๋ฌผ๊ฑฐ๋ ๋ฒ์ ๋ถ์์ ํ๋ง๋ฆด ์ ์๋ค.
3. ๋ ๋์ ์ฌ์ฉ์ ๊ฒฝํ
์ ๊ทผ์ฑ์ด ์ข์ ์น์ฌ์ดํธ๋ ๋ชจ๋ ์ฌ์ฉ์์๊ฒ ๋ ๋์ ์ฌ์ฉ์ ๊ฒฝํ์ ์ ๊ณตํ๋ค.
์ด๋ ์ฌ์ดํธ์ ์ ๋ขฐ๋๋ฅผ ๋์ด๊ณ , ์ดํ๋ฅ ์ ์ค์ด๋ ๋ฐ ๋์์ ์ค๋ค.
์น ์ ๊ทผ์ฑ์ 4๊ฐ์ง ๊ธฐ๋ณธ ์์น (POUR) ๐23
W3C์ ์น ์ ๊ทผ์ฑ ์ด๋์ ํฐ๋ธ(WAI)๋ ์น ์ ๊ทผ์ฑ์ **4๊ฐ์ง ๊ธฐ๋ณธ ์์น(POUR)**์ผ๋ก ์ ์ํ๋ค.
1. ์ธ์์ ์ฉ์ด์ฑ (Perceptibility)
์ฌ์ฉ์๋ ์ฝํ ์ธ ๋ฅผ ์ธ์งํ ์ ์์ด์ผ ํ๋ค.
- ์์ :
- ์ด๋ฏธ์ง์ ์ ์ ํ ๋์ฒด ํ ์คํธ ์ ๊ณต.
- ๋์์ ์ฝํ ์ธ ์ ์๋ง ์ถ๊ฐ.
<img src="example.jpg" alt="ํ๊ฒฝ ์ฌ์ง: ํด๋ณ์์ ์์์ด ์ง๊ณ ์๋ค." />
<video controls>
<track src="subtitles.vtt" kind="subtitles" srclang="ko" label="Korean" />
</video>2. ์ด์ฉ์ ์ฉ์ด์ฑ (Operability)
์ฌ์ฉ์๋ ์น์ฌ์ดํธ์ ๊ธฐ๋ฅ์ ์ฝ๊ฒ ์ฌ์ฉํ ์ ์์ด์ผ ํ๋ค.
- ์์ :
- ํค๋ณด๋๋ก ๋ชจ๋ ์์ ํ์ ๊ฐ๋ฅ.
- ๋ช ํํ ๋ฒํผ ๋ ์ด๋ธ ์ ๊ณต.
<button aria-label="์ํ ์ถ๊ฐ">Add to Cart</button>3. ์ดํด์ ์ฉ์ด์ฑ (Understandability)
์ฝํ ์ธ ๋ ์ดํด ๊ฐ๋ฅํด์ผ ํ๋ค.
- ์์ :
- ๋ช ํํ๊ณ ๊ฐ๊ฒฐํ ์ธ์ด ์ฌ์ฉ.
- ๋ณต์กํ ์ ๋ณด๋ฅผ ์ ๊ณตํ ๋๋ ์ถ๊ฐ ์ค๋ช ์ ๊ณต.
<p>
์ด ์น์ฌ์ดํธ๋ ์ฟ ํค๋ฅผ ์ฌ์ฉํฉ๋๋ค.
<a href="/cookie-policy">์์ธํ ์์๋ณด๊ธฐ</a>
</p>4. ๊ฒฌ๊ณ ์ฑ (Robustness)
์ฝํ ์ธ ๋ ๋ค์ํ ๊ธฐ์ ๊ณผ ํธํ ๊ฐ๋ฅํด์ผ ํ๋ค.
- ์์ :
- HTML ํ์ค ์ค์.
- ์คํฌ๋ฆฐ ๋ฆฌ๋์ ๊ฐ์ ๋ณด์กฐ ๊ธฐ์ ์ง์.
<!DOCTYPE html>
<html lang="ko">
<head>
<title>์น ์ ๊ทผ์ฑ ํ
์คํธ ํ์ด์ง</title>
</head>
<body>
<header>
<h1>์ ๊ทผ์ฑ ํ
์คํธ</h1>
</header>
</body>
</html>WCAG 2.1 ๋ ๋ฒจ๋ณ ๊ธฐ์ค (A / AA / AAA)
WCAG(Web Content Accessibility Guidelines)๋ W3C๊ฐ ์ ์ ํ ์น ์ฝํ ์ธ ์ ๊ทผ์ฑ ์ง์นจ์ผ๋ก, ํ์ฌ 2.1 ๋ฒ์ ์ด ๊ฐ์ฅ ๋๋ฆฌ ์ฌ์ฉ๋๋ค. ๊ฐ ์ฑ๊ณต ๊ธฐ์ค(Success Criterion)์ A, AA, AAA ์ธ ๊ฐ์ง ์ค์ ๋ ๋ฒจ๋ก ๋ถ๋ฅ๋๋ค.
- ๋ ๋ฒจ A: ๊ฐ์ฅ ๊ธฐ๋ณธ์ ์ธ ์๊ฑด. ์ด๋ฅผ ์ถฉ์กฑํ์ง ์์ผ๋ฉด ์ผ๋ถ ์ฌ์ฉ์๊ฐ ์ฝํ ์ธ ์ ์ ํ ์ ๊ทผํ ์ ์๋ค.
- ๋ ๋ฒจ AA: ๋๋ถ๋ถ์ ๋ฒ์ ์๊ตฌ์ฌํญ๊ณผ ์ ๋ถ ๊ธฐ๊ด ๊ธฐ์ค์ด ์๊ตฌํ๋ ์์ค. ๋๋ค์ ์ฅ์ ์ฌ์ฉ์๊ฐ ์ฝํ ์ธ ๋ฅผ ์ฌ์ฉํ ์ ์๋ค.
- ๋ ๋ฒจ AAA: ๊ฐ์ฅ ๋์ ์์ค. ๋ชจ๋ ๊ธฐ์ค์ ์ถฉ์กฑํ๋ ๊ฒ์ ํ์ค์ ์ผ๋ก ์ด๋ ค์ธ ์ ์์ผ๋ฏ๋ก ์ ์ฒด ์ฌ์ดํธ ๋ชฉํ๋ก ์ผ๊ธฐ๋ณด๋ค ํน์ ํ์ด์ง๋ ๊ธฐ๋ฅ์ ์ ์ฉํ๋ค.
์ฃผ์ ๊ธฐ์ค ์์ฝํ
| ์ฑ๊ณต ๊ธฐ์ค | ๋ ๋ฒจ | ์ค๋ช |
|---|---|---|
| 1.1.1 ๋นํ ์คํธ ์ฝํ ์ธ | A | ์ด๋ฏธ์ง ๋ฑ ๋นํ ์คํธ ์ฝํ ์ธ ์ ๋์ฒด ํ ์คํธ ์ ๊ณต |
| 1.2.1 ์ค๋์ค ์ ์ฉ / ๋น๋์ค ์ ์ฉ | A | ์ฌ์ ๋ นํ๋ ๋ฏธ๋์ด์ ๋๋ณธ ๋๋ ๋์ ์ ๊ณต |
| 1.2.2 ์๋ง (์ฌ์ ๋ นํ) | A | ๋์์์ ์๋ง ์ ๊ณต |
| 1.2.3 ์ค๋์ค ์ค๋ช ๋๋ ๋ฏธ๋์ด ๋์ | A | ๋์์์ ์ค๋์ค ์ค๋ช ๋๋ ์ ์ฒด ๋๋ณธ ์ ๊ณต |
| 1.2.4 ์๋ง (๋ผ์ด๋ธ) | AA | ์ค์๊ฐ ์คํธ๋ฆฌ๋ฐ์ ์๋ง ์ ๊ณต |
| 1.2.5 ์ค๋์ค ์ค๋ช (์ฌ์ ๋ นํ) | AA | ๋์์์ ์ค๋์ค ์ค๋ช ์ ๊ณต |
| 1.3.1 ์ ๋ณด์ ๊ด๊ณ | A | ๊ตฌ์กฐ์ ๊ด๊ณ๋ฅผ ํ๋ก๊ทธ๋๋ฐ ๋ฐฉ์์ผ๋ก ๊ฒฐ์ ๊ฐ๋ฅํ๋๋ก |
| 1.3.2 ์๋ฏธ ์๋ ์์ | A | ์ฝํ ์ธ ์์๊ฐ ์๋ฏธ ์๋ ๊ฒฝ์ฐ ์ฌ๋ฐ๋ฅธ ์์๋ก ์ ๊ณต |
| 1.3.3 ๊ฐ๊ฐ์ ํน์ฑ | A | ๋ชจ์, ์์, ์์น๋ง์ผ๋ก ์ฝํ ์ธ ์ดํด ๋ถ๊ฐํ๋๋ก ํ์ง ์์ |
| 1.3.4 ๋ฐฉํฅ (2.1 ์ถ๊ฐ) | AA | ์ฝํ ์ธ ๋ฅผ ํน์ ํ๋ฉด ๋ฐฉํฅ์ผ๋ก๋ง ์ ํํ์ง ์์ |
| 1.3.5 ์ ๋ ฅ ๋ชฉ์ ์๋ณ (2.1 ์ถ๊ฐ) | AA | ํผ ์ ๋ ฅ ํ๋์ ๋ชฉ์ ์ ํ๋ก๊ทธ๋๋ฐ ๋ฐฉ์์ผ๋ก ์๋ณ ๊ฐ๋ฅ |
| 1.4.1 ์์ ์ฌ์ฉ | A | ์์๋ง์ผ๋ก ์ ๋ณด ์ ๋ฌํ์ง ์์ |
| 1.4.2 ์ค๋์ค ์ ์ด | A | ์๋ ์ฌ์ ์ค๋์ค ์ค์ง/์์๊ฑฐ ์๋จ ์ ๊ณต |
| 1.4.3 ๋ช ์ ๋๋น (์ต์) | AA | ํ ์คํธ ๋ช ์ ๋๋น 4.5:1 ์ด์ |
| 1.4.4 ํ ์คํธ ํฌ๊ธฐ ์กฐ์ | AA | 200%๊น์ง ํ ์คํธ ํฌ๊ธฐ ํ๋ ์ ๊ธฐ๋ฅ ์์ค ์์ |
| 1.4.5 ํ ์คํธ ์ด๋ฏธ์ง | AA | ํ ์คํธ๋ ์ด๋ฏธ์ง ๋์ ์ค์ ํ ์คํธ๋ก ์ ๊ณต |
| 1.4.6 ๋ช ์ ๋๋น (๊ฐํ) | AAA | ํ ์คํธ ๋ช ์ ๋๋น 7:1 ์ด์ |
| 1.4.10 ์ฌ๋ฐฐ์น (2.1 ์ถ๊ฐ) | AA | 320px ๋๋น์์ ๊ฐ๋ก ์คํฌ๋กค ์์ด ์ฝํ ์ธ ํ์ |
| 1.4.11 ๋นํ ์คํธ ๋ช ์ ๋๋น (2.1 ์ถ๊ฐ) | AA | UI ์ปดํฌ๋ํธ ๋ฐ ๊ทธ๋ํฝ ๋ช ์ ๋๋น 3:1 ์ด์ |
| 1.4.12 ํ ์คํธ ๊ฐ๊ฒฉ (2.1 ์ถ๊ฐ) | AA | ํ ์คํธ ๊ฐ๊ฒฉ ๋ณ๊ฒฝ ์ ์ฝํ ์ธ /๊ธฐ๋ฅ ์์ค ์์ |
| 2.1.1 ํค๋ณด๋ | A | ๋ชจ๋ ๊ธฐ๋ฅ์ ํค๋ณด๋๋ก ์ฌ์ฉ ๊ฐ๋ฅ |
| 2.1.2 ํค๋ณด๋ ํธ๋ฉ ์์ | A | ํค๋ณด๋ ํฌ์ปค์ค๊ฐ ํน์ ์์์ ๊ฐํ์ง ์์ |
| 2.1.4 ๋ฌธ์ ๋จ์ถํค (2.1 ์ถ๊ฐ) | A | ๋จ์ผ ๋ฌธ์ ๋จ์ถํค๋ ๋๊ฑฐ๋ ์ฌ๋งคํ ๊ฐ๋ฅ |
| 2.4.1 ๋ธ๋ก ์ฐํ | A | ๋ฐ๋ณต ์ฝํ ์ธ ๊ฑด๋๋ฐ๊ธฐ ์๋จ ์ ๊ณต |
| 2.4.2 ํ์ด์ง ์ ๋ชฉ | A | ํ์ด์ง ๋ชฉ์ ์ ์ค๋ช ํ๋ ์ ๋ชฉ ์ ๊ณต |
| 2.4.3 ํฌ์ปค์ค ์์ | A | ๋ ผ๋ฆฌ์ ์ธ ํฌ์ปค์ค ์์ ์ ์ง |
| 2.4.4 ๋งํฌ ๋ชฉ์ (๋งฅ๋ฝ ๋ด) | A | ๋งํฌ ํ ์คํธ๋ก ๋งํฌ ๋ชฉ์ ์ ์ดํด ๊ฐ๋ฅ |
| 2.4.6 ์ ๋ชฉ๊ณผ ๋ ์ด๋ธ | AA | ์ ๋ชฉ๊ณผ ๋ ์ด๋ธ์ด ์ฃผ์ ๋๋ ๋ชฉ์ ์ ์ค๋ช |
| 2.4.7 ํฌ์ปค์ค ํ์ | AA | ํค๋ณด๋ ํฌ์ปค์ค ํ์๊ธฐ ์๊ฐ์ ์ผ๋ก ํ์ |
| 3.1.1 ํ์ด์ง ์ธ์ด | A | ํ์ด์ง์ ๊ธฐ๋ณธ ์ธ์ด๋ฅผ ํ๋ก๊ทธ๋๋ฐ ๋ฐฉ์์ผ๋ก ๊ฒฐ์ ๊ฐ๋ฅ |
| 3.2.1 ํฌ์ปค์ค ์ | A | ํฌ์ปค์ค ์ ์์์น ๋ชปํ ์ปจํ ์คํธ ๋ณ๊ฒฝ ์์ |
| 3.3.1 ์ค๋ฅ ์๋ณ | A | ์๋์ผ๋ก ๊ฐ์ง๋ ์ ๋ ฅ ์ค๋ฅ๋ฅผ ํ ์คํธ๋ก ์ค๋ช |
| 3.3.2 ๋ ์ด๋ธ ๋๋ ์ง์๋ฌธ | A | ์ฌ์ฉ์ ์ ๋ ฅ์ด ํ์ํ ๊ฒฝ์ฐ ๋ ์ด๋ธ ๋๋ ์ง์๋ฌธ ์ ๊ณต |
| 4.1.1 ํ์ฑ | A | ๋งํฌ์ ํ์ฑ ์ค๋ฅ ์ต์ํ |
| 4.1.2 ์ด๋ฆ, ์ญํ , ๊ฐ | A | UI ์ปดํฌ๋ํธ์ ์ด๋ฆ, ์ญํ , ๊ฐ์ ํ๋ก๊ทธ๋๋ฐ ๋ฐฉ์์ผ๋ก ์ค์ |
| 4.1.3 ์ํ ๋ฉ์์ง (2.1 ์ถ๊ฐ) | AA | ์ํ ๋ฉ์์ง๋ฅผ ํฌ์ปค์ค ์์ด๋ ๋ณด์กฐ ๊ธฐ์ ๋ก ์ ๋ฌ |
์ค๋ฌด์์๋ ๋ ๋ฒจ AA ์ถฉ์กฑ์ ๋ชฉํ๋ก ์ผ๋ ๊ฒ์ด ์ผ๋ฐ์ ์ด๋ฉฐ, ํ๊ตญ์ ๊ณต๊ณต ๊ธฐ๊ด ์ฌ์ดํธ๋ KWCAG(ํ๊ตญํ ์น ์ฝํ ์ธ ์ ๊ทผ์ฑ ์ง์นจ) ๊ธฐ์ค์ธ AA ๋ ๋ฒจ ์ค์๊ฐ ์๋ฌด๋ค.
ARIA (Accessible Rich Internet Applications) ์์ ๊ฐ์ด๋
ARIA๋ W3C๊ฐ ์ ์ํ ๊ธฐ์ ๋ช ์ธ๋ก, HTML๋ง์ผ๋ก๋ ํํํ๊ธฐ ์ด๋ ค์ด ๋์ UI ์ปดํฌ๋ํธ์ ์ ๊ทผ์ฑ ์ ๋ณด๋ฅผ ๋ณด์กฐ ๊ธฐ์ (์คํฌ๋ฆฐ ๋ฆฌ๋ ๋ฑ)์ ์ ๋ฌํ๋ ๋ฐ ์ฌ์ฉ๋๋ค.
ARIA์ First Rule: ์ฐ์ง ์์๋ ๋๋ค๋ฉด ์ฐ์ง ๋ง๋ผ
"If you can use a native HTML element or attribute with the semantics and behavior you require already built in, instead of re-purposing an element and adding an ARIA role, state or property to make it accessible, then do so."
์ฆ, <button>, <nav>, <main>, <header>, <form> ๊ฐ์ ์๋งจํฑ HTML ์์๊ฐ ์ด๋ฏธ ์ ๊ทผ์ฑ์ ๋ด์ฅํ๊ณ ์์ผ๋ฏ๋ก, <div role="button">์ฒ๋ผ ARIA๋ก ์ฌํํ๊ธฐ๋ณด๋ค ๋ค์ดํฐ๋ธ ์์๋ฅผ ์ฐ๋ ๊ฒ์ด ํจ์ฌ ๋ซ๋ค.
<!-- ๋์ ์: div์ role์ ๋ถ์ด๋ ๋ฐฉ์ -->
<div role="button" tabindex="0" onclick="submit()">์ ์ถ</div>
<!-- ์ข์ ์: ์๋งจํฑ ์์ ์ง์ ์ฌ์ฉ -->
<button type="submit">์ ์ถ</button>ARIA ์ญํ (Roles)
ARIA ์ญํ ์ ์์๊ฐ ๋ฌด์์ธ์ง๋ฅผ ์คํฌ๋ฆฐ ๋ฆฌ๋์ ์๋ ค์ค๋ค.
| ์ญํ (Role) | ์๋ฏธ | ์ฌ์ฉ ์ |
|---|---|---|
button |
ํด๋ฆญ ๊ฐ๋ฅํ ๋ฒํผ | <div role="button"> |
dialog |
๋ชจ๋ฌ ๋ค์ด์ผ๋ก๊ทธ | <div role="dialog"> |
alert |
์ฆ๊ฐ ์๋ฆผ ๋ฉ์์ง | <div role="alert"> |
alertdialog |
์ฌ์ฉ์ ์๋ต์ด ํ์ํ ์๋ฆผ | ํ์ธ/์ทจ์ ๋ชจ๋ฌ |
navigation |
๋ด๋น๊ฒ์ด์ ์์ญ | <div role="navigation"> |
main |
์ฃผ์ ์ฝํ ์ธ ์์ญ | <div role="main"> |
banner |
์ฌ์ดํธ ํค๋ | <div role="banner"> |
contentinfo |
์ฌ์ดํธ ํธํฐ | <div role="contentinfo"> |
search |
๊ฒ์ ์์ญ | <div role="search"> |
tablist |
ํญ ๋ชฉ๋ก ์ปจํ ์ด๋ | <ul role="tablist"> |
tab |
๊ฐ๋ณ ํญ | <li role="tab"> |
tabpanel |
ํญ ํจ๋ | <div role="tabpanel"> |
listbox |
์ ํ ๋ชฉ๋ก | ์ปค์คํ ๋๋กญ๋ค์ด |
option |
listbox ๋ด ํญ๋ชฉ | ๋๋กญ๋ค์ด ํญ๋ชฉ |
combobox |
์ ๋ ฅ+๋๋กญ๋ค์ด ์กฐํฉ | ์๋์์ฑ |
grid |
๋ฐ์ดํฐ ๊ทธ๋ฆฌ๋ | ์ปค์คํ ํ ์ด๋ธ |
progressbar |
์งํ ์ํ ํ์ | ์ ๋ก๋ ์งํ๋ฅ |
status |
๋น๊ธด๊ธ ์ํ ๋ฉ์์ง | ์ ์ฅ๋จ ์๋ฆผ |
tooltip |
ํดํ | ๋ง์ฐ์ค ์ค๋ฒ ์ค๋ช |
ARIA ์์ฑ (Properties)
์์ฑ์ ์์์ ๋ถ๊ฐ ์ ๋ณด๋ฅผ ์ ๊ณตํ๋ค. ๋น๊ต์ ์ ์ ์ด๋ค.
| ์์ฑ | ์ค๋ช | ์์ |
|---|---|---|
aria-label |
์์์ ์ด๋ฆ(๋ ์ด๋ธ)์ ์ง์ ์ง์ | aria-label="๋ซ๊ธฐ" |
aria-labelledby |
๋ค๋ฅธ ์์์ ID๋ก ๋ ์ด๋ธ ์ฐธ์กฐ | aria-labelledby="modal-title" |
aria-describedby |
์์๋ฅผ ์ค๋ช ํ๋ ๋ค๋ฅธ ์์์ ID ์ฐธ์กฐ | aria-describedby="error-msg" |
aria-required |
ํ์ ์ ๋ ฅ ์ฌ๋ถ | aria-required="true" |
aria-controls |
์ด ์์๊ฐ ์ ์ดํ๋ ์์์ ID | ํญ์ด ์ ์ดํ๋ ํจ๋ ID |
aria-haspopup |
ํ์ ๋ฉ๋ด/๋ฆฌ์คํธ๋ฐ์ค ์กด์ฌ ์ฌ๋ถ | aria-haspopup="listbox" |
aria-owns |
๋ ผ๋ฆฌ์ ์ผ๋ก ์์ ํ๋ ์์ ID | ๋ถ๋ชจ-์์ ๊ด๊ณ ๋ช ์ |
aria-live |
๋์ ์ฝํ ์ธ ์ ๋ฐ์ดํธ ์๋ฆผ ๋ฐฉ์ | aria-live="polite" |
aria-atomic |
live region ์ ์ฒด๋ฅผ ์ฝ์์ง ์ฌ๋ถ | aria-atomic="true" |
aria-relevant |
์ด๋ค ๋ณ๊ฒฝ์ ์๋ฆด์ง | additions text |
aria-roledescription |
์ญํ ์ ๋ํ ์ฌ๋์ด ์ฝ์ ์ ์๋ ์ค๋ช | aria-roledescription="์ฌ๋ผ์ด๋" |
ARIA ์ํ (States)
์ํ๋ ๋์ ์ผ๋ก ๋ณํ๋ ์ ๋ณด๋ฅผ ์ ๋ฌํ๋ค.
| ์ํ | ์ค๋ช | ์์ |
|---|---|---|
aria-expanded |
ํผ์นจ/์ ํ ์ํ | ๋๋กญ๋ค์ด, ์์ฝ๋์ธ |
aria-selected |
์ ํ ์ํ | ํญ, ๋ฆฌ์คํธ ํญ๋ชฉ |
aria-checked |
์ฒดํฌ ์ํ | ์ฒดํฌ๋ฐ์ค, ๋ผ๋์ค |
aria-disabled |
๋นํ์ฑ ์ํ | ๋นํ์ฑ ๋ฒํผ |
aria-hidden |
๋ณด์กฐ ๊ธฐ์ ์์ ์จ๊น | ์ฅ์์ฉ ์์ด์ฝ |
aria-invalid |
์ ํจํ์ง ์์ ์ ๋ ฅ | ํผ ๊ฒ์ฆ ์ค๋ฅ |
aria-pressed |
ํ ๊ธ ๋ฒํผ ๋๋ฆผ ์ํ | ์ข์์ ๋ฒํผ |
aria-busy |
๋ก๋ฉ ์ค ์ํ | ๋น๋๊ธฐ ๋ก๋ฉ |
aria-current |
ํ์ฌ ํญ๋ชฉ ํ์ | ํ์ฌ ํ์ด์ง ๋งํฌ |
์ค์ ์ฝ๋ ์์: ๋ชจ๋ฌ ๋ค์ด์ผ๋ก๊ทธ
<!-- ๋ชจ๋ฌ ํธ๋ฆฌ๊ฑฐ ๋ฒํผ -->
<button
type="button"
aria-haspopup="dialog"
onclick="openModal()"
>
๊ณ์ ์ญ์
</button>
<!-- ๋ชจ๋ฌ -->
<div
id="delete-modal"
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
aria-describedby="modal-desc"
hidden
>
<h2 id="modal-title">๊ณ์ ์ ์ญ์ ํ์๊ฒ ์ต๋๊น?</h2>
<p id="modal-desc">
์ด ์์
์ ๋๋๋ฆด ์ ์์ต๋๋ค. ๊ณ์ ๊ณผ ๊ด๋ จ๋ ๋ชจ๋ ๋ฐ์ดํฐ๊ฐ ์๊ตฌ์ ์ผ๋ก ์ญ์ ๋ฉ๋๋ค.
</p>
<div class="modal-actions">
<button type="button" onclick="confirmDelete()">์ญ์ ํ์ธ</button>
<button type="button" onclick="closeModal()">์ทจ์</button>
</div>
</div>
<script>
const modal = document.getElementById('delete-modal');
const triggerButton = document.querySelector('[aria-haspopup="dialog"]');
let focusableElements;
let firstFocusable;
let lastFocusable;
function openModal() {
modal.removeAttribute('hidden');
// ํฌ์ปค์ค ๊ฐ๋ฅํ ์์ ๋ชฉ๋ก ์ถ์ถ
focusableElements = modal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
firstFocusable = focusableElements[0];
lastFocusable = focusableElements[focusableElements.length - 1];
// ๋ชจ๋ฌ ๋ด ์ฒซ ์์๋ก ํฌ์ปค์ค ์ด๋
firstFocusable.focus();
// ํค๋ณด๋ ์ด๋ฒคํธ ๋ฑ๋ก
document.addEventListener('keydown', trapFocus);
}
function closeModal() {
modal.setAttribute('hidden', '');
document.removeEventListener('keydown', trapFocus);
// ํธ๋ฆฌ๊ฑฐ ๋ฒํผ์ผ๋ก ํฌ์ปค์ค ๋ณต๊ท
triggerButton.focus();
}
// Focus Trap: ๋ชจ๋ฌ ๋ด์์๋ง Tab ํค ์ํ
function trapFocus(e) {
if (e.key === 'Escape') {
closeModal();
return;
}
if (e.key !== 'Tab') return;
if (e.shiftKey) {
if (document.activeElement === firstFocusable) {
lastFocusable.focus();
e.preventDefault();
}
} else {
if (document.activeElement === lastFocusable) {
firstFocusable.focus();
e.preventDefault();
}
}
}
</script>์ค์ ์ฝ๋ ์์: ์ ๊ทผ ๊ฐ๋ฅํ ๋๋กญ๋ค์ด ๋ฉ๋ด
<div class="dropdown">
<button
type="button"
aria-haspopup="listbox"
aria-expanded="false"
aria-controls="lang-list"
id="lang-btn"
>
์ธ์ด ์ ํ
</button>
<ul
id="lang-list"
role="listbox"
aria-labelledby="lang-btn"
hidden
>
<li role="option" aria-selected="true" tabindex="0">ํ๊ตญ์ด</li>
<li role="option" aria-selected="false" tabindex="-1">English</li>
<li role="option" aria-selected="false" tabindex="-1">ๆฅๆฌ่ช</li>
</ul>
</div>
<script>
const btn = document.querySelector('[aria-haspopup="listbox"]');
const list = document.getElementById('lang-list');
btn.addEventListener('click', () => {
const isExpanded = btn.getAttribute('aria-expanded') === 'true';
btn.setAttribute('aria-expanded', String(!isExpanded));
list.hidden = isExpanded;
if (!isExpanded) {
list.querySelector('[aria-selected="true"]').focus();
}
});
list.addEventListener('keydown', (e) => {
const options = [...list.querySelectorAll('[role="option"]')];
const current = document.activeElement;
const idx = options.indexOf(current);
if (e.key === 'ArrowDown') {
options[(idx + 1) % options.length].focus();
e.preventDefault();
} else if (e.key === 'ArrowUp') {
options[(idx - 1 + options.length) % options.length].focus();
e.preventDefault();
} else if (e.key === 'Escape') {
btn.setAttribute('aria-expanded', 'false');
list.hidden = true;
btn.focus();
} else if (e.key === 'Enter' || e.key === ' ') {
options.forEach(opt => opt.setAttribute('aria-selected', 'false'));
current.setAttribute('aria-selected', 'true');
btn.textContent = current.textContent;
btn.setAttribute('aria-expanded', 'false');
list.hidden = true;
btn.focus();
}
});
</script>์ค์ ์ฝ๋ ์์: ํญ(Tab) ์ปดํฌ๋ํธ
<div class="tabs">
<ul role="tablist" aria-label="์ํ ์์ธ ์ ๋ณด">
<li role="presentation">
<button
role="tab"
id="tab-info"
aria-controls="panel-info"
aria-selected="true"
tabindex="0"
>
๊ธฐ๋ณธ ์ ๋ณด
</button>
</li>
<li role="presentation">
<button
role="tab"
id="tab-review"
aria-controls="panel-review"
aria-selected="false"
tabindex="-1"
>
๋ฆฌ๋ทฐ
</button>
</li>
<li role="presentation">
<button
role="tab"
id="tab-qna"
aria-controls="panel-qna"
aria-selected="false"
tabindex="-1"
>
Q&A
</button>
</li>
</ul>
<div role="tabpanel" id="panel-info" aria-labelledby="tab-info">
<p>์ํ์ ๊ธฐ๋ณธ ์ ๋ณด๊ฐ ์ฌ๊ธฐ ํ์๋ฉ๋๋ค.</p>
</div>
<div role="tabpanel" id="panel-review" aria-labelledby="tab-review" hidden>
<p>๋ฆฌ๋ทฐ ๋ด์ฉ์ด ์ฌ๊ธฐ ํ์๋ฉ๋๋ค.</p>
</div>
<div role="tabpanel" id="panel-qna" aria-labelledby="tab-qna" hidden>
<p>Q&A ๋ด์ฉ์ด ์ฌ๊ธฐ ํ์๋ฉ๋๋ค.</p>
</div>
</div>
<script>
const tabs = document.querySelectorAll('[role="tab"]');
const panels = document.querySelectorAll('[role="tabpanel"]');
tabs.forEach(tab => {
tab.addEventListener('click', activateTab);
tab.addEventListener('keydown', handleTabKeydown);
});
function activateTab(e) {
const targetTab = e.currentTarget;
tabs.forEach(t => {
t.setAttribute('aria-selected', 'false');
t.setAttribute('tabindex', '-1');
});
panels.forEach(p => p.setAttribute('hidden', ''));
targetTab.setAttribute('aria-selected', 'true');
targetTab.setAttribute('tabindex', '0');
document.getElementById(targetTab.getAttribute('aria-controls'))
.removeAttribute('hidden');
}
function handleTabKeydown(e) {
const tabList = [...tabs];
const idx = tabList.indexOf(e.currentTarget);
if (e.key === 'ArrowRight') {
tabList[(idx + 1) % tabList.length].focus();
} else if (e.key === 'ArrowLeft') {
tabList[(idx - 1 + tabList.length) % tabList.length].focus();
} else if (e.key === 'Home') {
tabList[0].focus();
} else if (e.key === 'End') {
tabList[tabList.length - 1].focus();
}
}
</script>ํค๋ณด๋ ๋ค๋น๊ฒ์ด์ ์ฌํ
Tab ์์ ๊ด๋ฆฌ
๋ธ๋ผ์ฐ์ ๋ ๊ธฐ๋ณธ์ ์ผ๋ก DOM ์์๋๋ก ํฌ์ปค์ค๋ฅผ ์ด๋์ํจ๋ค. tabindex ๊ฐ์ ๋ฐ๋ผ ๋์์ด ๋ฌ๋ผ์ง๋ค.
| tabindex ๊ฐ | ๋์ |
|---|---|
tabindex="0" |
์์ฐ์ค๋ฌ์ด DOM ์์์ ํฌํจ (๊ถ์ฅ) |
tabindex="-1" |
Tab ์์์์ ์ ์ธ, JavaScript๋ก๋ง ํฌ์ปค์ค ๊ฐ๋ฅ |
tabindex="1" ์ด์ |
ํน์ ์์ ๊ฐ์ ์ง์ (์ฌ์ฉ ๋น๊ถ์ฅ โ ์ ์ง๋ณด์ ์ด๋ ค์) |
<!-- Skip Navigation: ๋ฉ์ธ ์ฝํ
์ธ ๋ก ๋ฐ๋ก ์ด๋ํ๋ ๋งํฌ -->
<a href="#main-content" class="skip-link">๋ฉ์ธ ์ฝํ
์ธ ๋ก ๊ฑด๋๋ฐ๊ธฐ</a>
<nav><!-- ๋ฐ๋ณต๋๋ ๋ด๋น๊ฒ์ด์
๋ฉ๋ด --></nav>
<main id="main-content" tabindex="-1">
<h1>ํ์ด์ง ์ ๋ชฉ</h1>
<!-- ... -->
</main>/* Skip Link: ํ์์๋ ํ๋ฉด ๋ฐ์ ์จ๊ธฐ๊ณ , ํฌ์ปค์ค ์ ํ์ */
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: #000;
color: #fff;
padding: 8px;
z-index: 100;
transition: top 0.2s;
}
.skip-link:focus {
top: 0;
}Focus Trap ํจํด (๋ชจ๋ฌ)
๋ชจ๋ฌ์ด ์ด๋ ธ์ ๋ Tab ํค๊ฐ ๋ชจ๋ฌ ๋ฐ๊นฅ์ผ๋ก ๋๊ฐ๋ฉด ์ ๋๋ค. ์์ ๋ชจ๋ฌ ์์์์ ์ดํด๋ณธ trapFocus ํจ์๊ฐ ์ด ์ญํ ์ ํ๋ค. ์์ฝํ๋ฉด:
- ๋ชจ๋ฌ ๋ด ํฌ์ปค์ค ๊ฐ๋ฅํ ๋ชจ๋ ์์๋ฅผ ๋ชฉ๋กํํ๋ค.
- ๋ง์ง๋ง ์์์์ Tab์ ๋๋ฅด๋ฉด ์ฒซ ์์๋ก, ์ฒซ ์์์์ Shift+Tab์ ๋๋ฅด๋ฉด ๋ง์ง๋ง ์์๋ก ์ด๋์ํจ๋ค.
- Escape ํค๋ก ๋ชจ๋ฌ์ ๋ซ๊ณ ์๋ ํธ๋ฆฌ๊ฑฐ๋ก ํฌ์ปค์ค๋ฅผ ๋ฐํํ๋ค.
JavaScript๋ก Focus ๊ด๋ฆฌํ๋ ํจํด
// ํ์ด์ง ์ ํ ์ (SPA) ํฌ์ปค์ค ๊ด๋ฆฌ
function navigateTo(path) {
// ๋ผ์ฐํ
์ฒ๋ฆฌ
router.push(path);
// ์ ํ์ด์ง์ h1 ๋๋ main์ผ๋ก ํฌ์ปค์ค ์ด๋
requestAnimationFrame(() => {
const mainHeading = document.querySelector('main h1') ||
document.querySelector('[tabindex="-1"]#main-content');
if (mainHeading) {
mainHeading.setAttribute('tabindex', '-1');
mainHeading.focus({ preventScroll: false });
}
});
}
// ๋์ ์ผ๋ก ์ถ๊ฐ๋ ์ฝํ
์ธ ๋ก ํฌ์ปค์ค ์ด๋
function showNotification(message) {
const notification = document.createElement('div');
notification.setAttribute('role', 'alert');
notification.setAttribute('tabindex', '-1');
notification.textContent = message;
document.body.appendChild(notification);
notification.focus();
}์์ ๋๋น (Color Contrast) ์ฌํ
WCAG ๋ช ์ ๋๋น ๊ธฐ์ค
์์ ๋๋น๋ ์ ๊ฒฝ์(ํ ์คํธ ๋๋ UI ์์)๊ณผ ๋ฐฐ๊ฒฝ์ ๊ฐ์ ๋ฐ๊ธฐ ์ฐจ์ด๋ฅผ ๋น์จ๋ก ํํํ ๊ฒ์ด๋ค. ์ต์๊ฐ 1:1(์ฐจ์ด ์์)์์ ์ต๋๊ฐ 21:1(ํ๋ฐฑ)๊น์ง๋ค.
| ๊ธฐ์ค | ๋ ๋ฒจ | ์ ์ฉ ๋์ |
|---|---|---|
| 4.5:1 | AA | ์ผ๋ฐ ํ ์คํธ (18pt ๋ฏธ๋ง / ๊ตต์ง ์์ 14pt ๋ฏธ๋ง) |
| 3:1 | AA | ํฐ ํ ์คํธ (18pt ์ด์ ๋๋ ๊ตต์ 14pt ์ด์) |
| 3:1 | AA | ๋นํ ์คํธ UI ์์ ๋ฐ ๊ทธ๋ํฝ (WCAG 2.1, 1.4.11) |
| 7:1 | AAA | ์ผ๋ฐ ํ ์คํธ (๊ฐํ ๊ธฐ์ค) |
| 4.5:1 | AAA | ํฐ ํ ์คํธ (๊ฐํ ๊ธฐ์ค) |
๋ช ์ ๋๋น ๊ณ์ฐ ์๋ฆฌ
WCAG์์ ์ฌ์ฉํ๋ ์๋ ํ๋(Relative Luminance) ๊ณต์์ ๋ค์๊ณผ ๊ฐ๋ค.
- RGB ๊ฐ ์ฑ๋์ 0~1 ๋ฒ์๋ก ์ ๊ทํํ ํ ๊ฐ๋ง ๋ณด์ ์ ์ ์ฉํ๋ค.
- L = 0.2126 ร R + 0.7152 ร G + 0.0722 ร B
- ๋๋น์จ = (L1 + 0.05) / (L2 + 0.05) (L1 โฅ L2)
// ๋ช
์ ๋๋น ๊ณ์ฐ ํจ์
function getRelativeLuminance(hex) {
const rgb = parseInt(hex.slice(1), 16);
const r = ((rgb >> 16) & 0xff) / 255;
const g = ((rgb >> 8) & 0xff) / 255;
const b = ((rgb >> 0) & 0xff) / 255;
const toLinear = c =>
c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
}
function getContrastRatio(hex1, hex2) {
const l1 = getRelativeLuminance(hex1);
const l2 = getRelativeLuminance(hex2);
const lighter = Math.max(l1, l2);
const darker = Math.min(l1, l2);
return (lighter + 0.05) / (darker + 0.05);
}
// ์์
const ratio = getContrastRatio('#ffffff', '#767676');
console.log(ratio.toFixed(2)); // ์ฝ 4.54 โ AA ํต๊ณผ์์ ๋๋น ํ์ธ ๋๊ตฌ
| ๋๊ตฌ | ์ ํ | ํน์ง |
|---|---|---|
| WebAIM Contrast Checker | ์น | AA/AAA ์ฆ์ ํ์ธ, HEX/RGB ์ ๋ ฅ |
| Colour Contrast Analyser | ๋ฐ์คํฌํ ์ฑ | ํ๋ฉด์ ์์์ ์ง์ ์คํฌ์ดํธ๋ก ์ถ์ถ |
| Figma ํ๋ฌ๊ทธ์ธ (A11y Annotation Kit) | ๋์์ธ ํด | ๋์์ธ ๋จ๊ณ์์ ํ์ธ |
| Chrome DevTools | ๋ธ๋ผ์ฐ์ | ์์ ๊ฒ์ฌ > CSS ์์ ํด๋ฆญ ์ ๋๋น์จ ํ์ |
์์๋ง์ผ๋ก ์ ๋ณด ์ ๋ฌํ์ง ์๊ธฐ
<!-- ๋์ ์: ์์๋ง์ผ๋ก ์ค๋ฅ ํ์ -->
<input type="text" style="border-color: red;" />
<!-- ์ข์ ์: ์์ + ์์ด์ฝ + ํ
์คํธ ๋ฉ์์ง ๋ณํ -->
<div class="form-group">
<input
type="text"
aria-invalid="true"
aria-describedby="email-error"
style="border-color: #d73027;"
/>
<p id="email-error" role="alert" style="color: #d73027;">
โ ์ฌ๋ฐ๋ฅธ ์ด๋ฉ์ผ ์ฃผ์๋ฅผ ์
๋ ฅํด ์ฃผ์ธ์.
</p>
</div>ํผ(Form) ์ ๊ทผ์ฑ ์ต์ ํ
ํผ์ ์น์์ ์ ๊ทผ์ฑ ๋ฌธ์ ๊ฐ ๊ฐ์ฅ ๋ง์ด ๋ฐ์ํ๋ ์์ญ ์ค ํ๋๋ค.
label, fieldset, legend ์ฌ๋ฐ๋ฅด๊ฒ ์ฌ์ฉํ๊ธฐ
<!-- ๊ธฐ๋ณธ label ์ฐ๊ฒฐ -->
<div class="form-group">
<label for="username">์ฌ์ฉ์ ์ด๋ฆ <span aria-hidden="true">*</span></label>
<span class="sr-only">(ํ์)</span>
<input
type="text"
id="username"
name="username"
required
aria-required="true"
autocomplete="username"
/>
</div>
<!-- fieldset + legend: ๊ด๋ จ ์
๋ ฅ ํ๋ ๊ทธ๋ฃนํ -->
<fieldset>
<legend>์ฐ๋ฝ์ฒ ์ ๋ณด</legend>
<div class="form-group">
<label for="phone">์ ํ๋ฒํธ</label>
<input type="tel" id="phone" name="phone" autocomplete="tel" />
</div>
<div class="form-group">
<label for="email">์ด๋ฉ์ผ</label>
<input type="email" id="email" name="email" autocomplete="email" />
</div>
</fieldset>
<!-- ๋ผ๋์ค ๋ฒํผ ๊ทธ๋ฃน -->
<fieldset>
<legend>๊ฒฐ์ ๋ฐฉ๋ฒ์ ์ ํํด ์ฃผ์ธ์</legend>
<label>
<input type="radio" name="payment" value="card" /> ์ ์ฉ์นด๋
</label>
<label>
<input type="radio" name="payment" value="bank" /> ๊ณ์ข์ด์ฒด
</label>
<label>
<input type="radio" name="payment" value="phone" /> ํด๋ํฐ ๊ฒฐ์
</label>
</fieldset>/* ์คํฌ๋ฆฐ ๋ฆฌ๋ ์ ์ฉ ํ
์คํธ: ์๊ฐ์ ์ผ๋ก๋ ์จ๊ธฐ์ง๋ง ๋ณด์กฐ ๊ธฐ์ ์ ์ฝ์ */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}์ค๋ฅ ๋ฉ์์ง ์ ๊ทผ์ฑ
<form id="signup-form" novalidate>
<div class="form-group">
<label for="user-email">์ด๋ฉ์ผ ์ฃผ์</label>
<input
type="email"
id="user-email"
name="email"
aria-required="true"
aria-describedby="email-hint email-error"
/>
<p id="email-hint" class="field-hint">์: name@example.com</p>
<p id="email-error" class="field-error" role="alert" hidden>
์ด๋ฉ์ผ ์ฃผ์๋ฅผ ์ฌ๋ฐ๋ฅธ ํ์์ผ๋ก ์
๋ ฅํด ์ฃผ์ธ์.
</p>
</div>
<div class="form-group">
<label for="user-pw">๋น๋ฐ๋ฒํธ</label>
<input
type="password"
id="user-pw"
name="password"
aria-required="true"
aria-describedby="pw-requirements pw-error"
/>
<p id="pw-requirements" class="field-hint">
8์ ์ด์, ์๋ฌธยท์ซ์ยทํน์๋ฌธ์ ํฌํจ
</p>
<p id="pw-error" class="field-error" role="alert" hidden>
๋น๋ฐ๋ฒํธ๋ 8์ ์ด์์ด์ด์ผ ํฉ๋๋ค.
</p>
</div>
<button type="submit">๊ฐ์
ํ๊ธฐ</button>
</form>
<script>
const form = document.getElementById('signup-form');
form.addEventListener('submit', (e) => {
e.preventDefault();
let firstError = null;
const emailInput = document.getElementById('user-email');
const emailError = document.getElementById('email-error');
if (!emailInput.validity.valid) {
emailInput.setAttribute('aria-invalid', 'true');
emailError.removeAttribute('hidden');
firstError = firstError || emailInput;
} else {
emailInput.removeAttribute('aria-invalid');
emailError.setAttribute('hidden', '');
}
const pwInput = document.getElementById('user-pw');
const pwError = document.getElementById('pw-error');
if (pwInput.value.length < 8) {
pwInput.setAttribute('aria-invalid', 'true');
pwError.removeAttribute('hidden');
firstError = firstError || pwInput;
} else {
pwInput.removeAttribute('aria-invalid');
pwError.setAttribute('hidden', '');
}
// ์ฒซ ๋ฒ์งธ ์ค๋ฅ ํ๋๋ก ํฌ์ปค์ค ์ด๋
if (firstError) {
firstError.focus();
} else {
// ์ ์ถ ์ฒ๋ฆฌ
console.log('ํผ ์ ์ถ ์ฑ๊ณต');
}
});
</script>React์์์ ์ ๊ทผ์ฑ ๊ตฌํ
React๋ ๊ธฐ๋ณธ์ ์ผ๋ก JSX์์ ARIA ์์ฑ์ HTML๊ณผ ๋์ผํ๊ฒ ์ง์ํ๋ค. ๋ค๋ง class โ className, for โ htmlFor๋ก ๋ฐ๋๋ ์ ์ ์ฃผ์ํ๋ค.
๊ธฐ๋ณธ ARIA ์์ฑ ์ฌ์ฉ
// ์ ๊ทผ ๊ฐ๋ฅํ ์์ด์ฝ ๋ฒํผ
function IconButton({ onClick, label }) {
return (
<button type="button" onClick={onClick} aria-label={label}>
<svg aria-hidden="true" focusable="false">
{/* SVG ๋ด์ฉ */}
</svg>
</button>
);
}
// ์ ๊ทผ ๊ฐ๋ฅํ ํ ๊ธ ๋ฒํผ
function ToggleButton({ isPressed, onToggle, children }) {
return (
<button
type="button"
aria-pressed={isPressed}
onClick={onToggle}
className={isPressed ? 'btn-pressed' : 'btn'}
>
{children}
</button>
);
}useRef๋ก Focus ๊ด๋ฆฌ
import { useRef, useEffect } from 'react';
// ๋ชจ๋ฌ ์ปดํฌ๋ํธ
function Modal({ isOpen, onClose, title, children }) {
const modalRef = useRef(null);
const triggerRef = useRef(null);
useEffect(() => {
if (isOpen) {
// ๋ชจ๋ฌ์ด ์ด๋ฆฌ๋ฉด ์ฒซ ๋ฒ์งธ ํฌ์ปค์ค ๊ฐ๋ฅ ์์๋ก ์ด๋
const focusable = modalRef.current?.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (focusable?.length) focusable[0].focus();
} else {
// ๋ชจ๋ฌ์ด ๋ซํ๋ฉด ํธ๋ฆฌ๊ฑฐ๋ก ํฌ์ปค์ค ๋ณต๊ท
triggerRef.current?.focus();
}
}, [isOpen]);
if (!isOpen) return null;
return (
<div
ref={modalRef}
role="dialog"
aria-modal="true"
aria-labelledby="modal-heading"
>
<h2 id="modal-heading">{title}</h2>
{children}
<button type="button" onClick={onClose}>๋ซ๊ธฐ</button>
</div>
);
}
// ์ฌ์ฉ ์
function App() {
const [open, setOpen] = React.useState(false);
return (
<>
<button type="button" onClick={() => setOpen(true)}>
๋ชจ๋ฌ ์ด๊ธฐ
</button>
<Modal isOpen={open} onClose={() => setOpen(false)} title="๊ณต์ง์ฌํญ">
<p>์ฌ๊ธฐ์ ๋ชจ๋ฌ ๋ด์ฉ์ด ๋ค์ด๊ฐ๋๋ค.</p>
</Modal>
</>
);
}๋์ ์๋ฆผ (Live Region)
import { useState, useEffect } from 'react';
// ์คํฌ๋ฆฐ ๋ฆฌ๋๊ฐ ์๋์ผ๋ก ์ฝ์ด์ฃผ๋ ์๋ฆผ ์์ญ
function LiveAnnouncer() {
const [message, setMessage] = useState('');
// ์ ์ญ ์ปค์คํ
์ด๋ฒคํธ๋ก ๋ฉ์์ง ์์
useEffect(() => {
const handler = (e) => setMessage(e.detail);
window.addEventListener('announce', handler);
return () => window.removeEventListener('announce', handler);
}, []);
return (
<div
aria-live="polite"
aria-atomic="true"
className="sr-only"
>
{message}
</div>
);
}
// ์ด๋์๋ ์๋ฆผ ๋ฐ์ก
function announce(message) {
window.dispatchEvent(new CustomEvent('announce', { detail: message }));
}
// ์ฌ์ฉ ์: ์ฅ๋ฐ๊ตฌ๋ ์ถ๊ฐ ์
function AddToCartButton({ productName }) {
const handleClick = () => {
// ์ฅ๋ฐ๊ตฌ๋ ๋ก์ง ์ฒ๋ฆฌ
announce(`${productName}์ด(๊ฐ) ์ฅ๋ฐ๊ตฌ๋์ ์ถ๊ฐ๋์์ต๋๋ค.`);
};
return <button onClick={handleClick}>์ฅ๋ฐ๊ตฌ๋์ ์ถ๊ฐ</button>;
}์ค์ ์คํฌ๋ฆฐ ๋ฆฌ๋ ํ ์คํธ ์๋๋ฆฌ์ค
์คํฌ๋ฆฐ ๋ฆฌ๋ ํ ์คํธ๋ ์๋ํ ๋๊ตฌ๋ก ์ก์ ์ ์๋ ๋ฌธ์ ๋ฅผ ๋ฐ๊ฒฌํ๋ ๋ฐ ํ์์ ์ด๋ค.
์ฃผ์ ์คํฌ๋ฆฐ ๋ฆฌ๋
| ์คํฌ๋ฆฐ ๋ฆฌ๋ | ํ๋ซํผ | ์ฃผ๋ก ์ฐ๋ ๋ธ๋ผ์ฐ์ | ๋น๊ณ |
|---|---|---|---|
| NVDA | Windows | Chrome, Firefox | ๋ฌด๋ฃ, ์คํ์์ค |
| JAWS | Windows | Chrome, IE/Edge | ์์ฉ, ๊ธฐ์ ํ๊ฒฝ์์ ๋ง์ด ์ฌ์ฉ |
| VoiceOver | macOS / iOS | Safari | ์ด์์ฒด์ ๋ด์ฅ |
| TalkBack | Android | Chrome | ์ด์์ฒด์ ๋ด์ฅ |
| Narrator | Windows | Edge | ์ด์์ฒด์ ๋ด์ฅ, ๊ธฐ๋ฅ ์ ํ์ |
NVDA ๊ธฐ๋ณธ ๋จ์ถํค
| ๋จ์ถํค | ๋์ |
|---|---|
| Ins + F7 | ๋งํฌ/์ ๋ชฉ/๋๋๋งํฌ ๋ชฉ๋ก ์ด๊ธฐ |
| Ins + Space | ์ฐพ์๋ณด๊ธฐ ๋ชจ๋ โ ํผ ๋ชจ๋ ์ ํ |
| H | ๋ค์ ์ ๋ชฉ์ผ๋ก ์ด๋ |
| Shift + H | ์ด์ ์ ๋ชฉ์ผ๋ก ์ด๋ |
| 1~6 | ํด๋น ๋ ๋ฒจ ์ ๋ชฉ์ผ๋ก ์ด๋ |
| K | ๋ค์ ๋งํฌ๋ก ์ด๋ |
| B | ๋ค์ ๋ฒํผ์ผ๋ก ์ด๋ |
| F | ๋ค์ ํผ ํ๋๋ก ์ด๋ |
| Tab | ๋ค์ ํฌ์ปค์ค ๊ฐ๋ฅ ์์๋ก ์ด๋ |
| Ins + F5 | ํผ ํ๋ ๋ชฉ๋ก ์ด๊ธฐ |
VoiceOver ๊ธฐ๋ณธ ๋จ์ถํค (macOS)
| ๋จ์ถํค | ๋์ |
|---|---|
| Cmd + F5 | VoiceOver ์ผ๊ธฐ/๋๊ธฐ |
| Ctrl + Option + โ/โ | ๋ค์/์ด์ ์์ ์ฝ๊ธฐ |
| Ctrl + Option + Space | ํ์ฌ ์์ ํ์ฑํ |
| Ctrl + Option + U | ๋กํฐ ์ด๊ธฐ (๋งํฌ/์ ๋ชฉ/๋๋๋งํฌ ํ์) |
| Ctrl + Option + A | ํ์ฌ ์์น๋ถํฐ ์๋ ์ฝ๊ธฐ |
| Ctrl | ์ฝ๊ธฐ ์ค์ง |
ํ ์คํธ ์๋๋ฆฌ์ค ์ฒดํฌ๋ฆฌ์คํธ
โ
๊ธฐ๋ณธ ํ์
โก ํ์ด์ง ์ ๋ชฉ์ด ์ฌ๋ฐ๋ฅด๊ฒ ์ฝํ๋๊ฐ
โก ๋๋๋งํฌ(header, nav, main, footer)๊ฐ ์ธ์๋๋๊ฐ
โก ์ ๋ชฉ ๊ณ์ธต(h1โh2โh3)์ด ๋
ผ๋ฆฌ์ ์ธ๊ฐ
โก Skip Navigation ๋งํฌ๊ฐ ๋์ํ๋๊ฐ
โ
์ธํฐ๋์
โก ๋ฒํผ๊ณผ ๋งํฌ๊ฐ ์ญํ , ์ด๋ฆ, ์ํ์ ํจ๊ป ์ฝํ๋๊ฐ
โก ํผ ๋ ์ด๋ธ์ด ์
๋ ฅ ํ๋์ ์ฐ๊ฒฐ๋์ด ์๋๊ฐ
โก ์ค๋ฅ ๋ฉ์์ง๊ฐ ์๋์ผ๋ก ์ฝํ๋๊ฐ
โก ๋ชจ๋ฌ์ด ์ด๋ฆฌ๋ฉด ํฌ์ปค์ค๊ฐ ๋ชจ๋ฌ ๋ด๋ก ์ด๋ํ๋๊ฐ
โก ๋ชจ๋ฌ์ด ๋ซํ๋ฉด ํธ๋ฆฌ๊ฑฐ๋ก ํฌ์ปค์ค๊ฐ ๋์์ค๋๊ฐ
โก ๋๋กญ๋ค์ด / ํญ / ์์ฝ๋์ธ์ ์ํ(expanded/selected)๊ฐ ์ฝํ๋๊ฐ
โ
์ด๋ฏธ์ง & ๋ฏธ๋์ด
โก ์ ๋ณด๋ฅผ ๋ด์ ์ด๋ฏธ์ง์ ์๋ฏธ ์๋ alt ํ
์คํธ๊ฐ ์๋๊ฐ
โก ์ฅ์์ฉ ์ด๋ฏธ์ง์ ๋น alt=""๊ฐ ์๋๊ฐ
โก ๋์์์ ์๋ง์ด ์๋๊ฐ
โ
๋์ ์ฝํ
์ธ
โก ํ์ด์ง ์ ํ ํ ์คํฌ๋ฆฐ ๋ฆฌ๋๊ฐ ์ ํ์ด์ง๋ฅผ ์ธ์ํ๋๊ฐ (SPA)
โก ํ ์คํธ/์๋ฆผ ๋ฉ์์ง๊ฐ aria-live๋ก ์ฝํ๋๊ฐ
โก ๋น๋๊ธฐ ๋ก๋ฉ ์๋ฃ ํ ๊ฒฐ๊ณผ๊ฐ ์๋ ค์ง๋๊ฐ์ ๊ทผ์ฑ ์๋ํ ํ ์คํธ
์๋ํ ํ ์คํธ๋ ์ ๊ทผ์ฑ ๋ฌธ์ ์ ์ฝ 30~40%๋ฅผ ์ก์ ์ ์๋ค. ์คํฌ๋ฆฐ ๋ฆฌ๋ ํ ์คํธ์ ๋ณํํ๋ฉด ํจ๊ณผ์ ์ด๋ค.
jest-axe: ๋จ์ ํ ์คํธ์ ํตํฉ
npm install --save-dev jest-axe// Button.test.jsx
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
import Button from './Button';
expect.extend(toHaveNoViolations);
describe('Button ์ปดํฌ๋ํธ ์ ๊ทผ์ฑ', () => {
it('๊ธฐ๋ณธ ๋ฒํผ์ ์ ๊ทผ์ฑ ์๋ฐ์ด ์์ด์ผ ํ๋ค', async () => {
const { container } = render(<Button>์ ์ถ</Button>);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it('์์ด์ฝ ๋ฒํผ์ aria-label์ด ์์ด์ผ ํ๋ค', async () => {
const { container } = render(
<button aria-label="๊ฒ์">
<svg aria-hidden="true">...</svg>
</button>
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});Playwright + axe-core: E2E ํ ์คํธ์ ํตํฉ
npm install --save-dev @axe-core/playwright// accessibility.spec.js
const { test, expect } = require('@playwright/test');
const { checkA11y, injectAxe } = require('@axe-core/playwright');
test.describe('ํ์ด์ง ์ ๊ทผ์ฑ ๊ฒ์ฌ', () => {
test('ํํ์ด์ง ์ ๊ทผ์ฑ ๊ธฐ์ค ํต๊ณผ', async ({ page }) => {
await page.goto('/');
await injectAxe(page);
await checkA11y(page, null, {
axeOptions: {
runOnly: {
type: 'tag',
values: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'],
},
},
});
});
test('๋ก๊ทธ์ธ ํผ ์ ๊ทผ์ฑ ๊ธฐ์ค ํต๊ณผ', async ({ page }) => {
await page.goto('/login');
await injectAxe(page);
// ํน์ ์์ญ๋ง ๊ฒ์ฌ
await checkA11y(page, '#login-form');
});
test('๋ชจ๋ฌ ์ด๋ฆฐ ์ํ ์ ๊ทผ์ฑ ๊ฒ์ฌ', async ({ page }) => {
await page.goto('/');
await injectAxe(page);
// ๋ชจ๋ฌ ์ด๊ธฐ
await page.click('[data-testid="open-modal"]');
await page.waitForSelector('[role="dialog"]');
// ๋ชจ๋ฌ์ด ์ด๋ฆฐ ์ํ์์ ๊ฒ์ฌ
await checkA11y(page, '[role="dialog"]');
});
});CI/CD ํ์ดํ๋ผ์ธ์ ํตํฉ
# .github/workflows/accessibility.yml
name: Accessibility Tests
on:
pull_request:
branches: [main]
jobs:
a11y:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm run build
- run: npx playwright install --with-deps chromium
- run: npm run test:a11y
env:
BASE_URL: http://localhost:3000์น ์ ๊ทผ์ฑ์ ์ํ ์ค์ฒ ๋ฐฉ๋ฒ โ
1. ๋์์ธ ์ด๊ธฐ๋ถํฐ ์ ๊ทผ์ฑ ๊ณ ๋ ค
- ๋ฒํผ ํฌ๊ธฐ, ํ ์คํธ ๋๋น, ํค๋ณด๋ ๋ด๋น๊ฒ์ด์ ํฌํจ.
2. ์ ๊ธฐ์ ์ธ ์ ๊ทผ์ฑ ํ๊ฐ
WAVE,AXE,Lighthouse๊ฐ์ ๋๊ตฌ ํ์ฉ.
3. ์ฌ์ฉ์ ํผ๋๋ฐฑ ๋ฐ์
- ์ค์ ์ฌ์ฉ์ ํ ์คํธ๋ฅผ ํตํด ๊ฐ์ ์ ๋ฐ๊ฒฌ.
์์ฃผ ๋ฐ์ํ๋ ๋ฌธ์ ์ ํด๊ฒฐ ๋ฐฉ์ ๐ง
๋ฌธ์ 1: ๋์ฒด ํ ์คํธ ๋๋ฝ
- ํด๊ฒฐ ๋ฐฉ๋ฒ: ๋ชจ๋ ์ด๋ฏธ์ง์ ์ ์ ํ
altํ๊ทธ ์ถ๊ฐ.
<!-- ์ ๋ณด๋ฅผ ๋ด์ ์ด๋ฏธ์ง -->
<img src="chart.png" alt="2023๋
๋งค์ถ ๊ทธ๋ํ: 3๋ถ๊ธฐ ์ต๊ณ ์น ๊ธฐ๋ก" />
<!-- ์ฅ์์ฉ ์ด๋ฏธ์ง๋ ๋น alt -->
<img src="decoration.png" alt="" />
<!-- ๋ณต์กํ ์ด๋ฏธ์ง๋ ๊ธด ์ค๋ช
๋งํฌ ์ถ๊ฐ -->
<figure>
<img src="complex-chart.png" alt="์ฐ๊ฐ ๋งค์ถ ์ถ์ด ์ฐจํธ" aria-describedby="chart-desc" />
<figcaption id="chart-desc">
2020๋
50์ต, 2021๋
65์ต, 2022๋
80์ต, 2023๋
102์ต์ผ๋ก ๊พธ์คํ ์ฑ์ฅ.
3๋ถ๊ธฐ์ ์ ์ ํ ์ถ์๋ก ์ธํด ๊ฐ์ฅ ํฐ ํญ์ ์ฆ๊ฐ๋ฅผ ๋ณด์.
</figcaption>
</figure>๋ฌธ์ 2: ํค๋ณด๋ ์ ๊ทผ์ฑ ๋ถ์กฑ
- ํด๊ฒฐ ๋ฐฉ๋ฒ:
tabindex์์ฑ์ ์ฌ์ฉํด ํค๋ณด๋ ํ์ ๊ฒฝ๋ก ์ค์ .
<button tabindex="0">Submit</button>๋ฌธ์ 3: ๋๋น ๋ถ์กฑ
- ํด๊ฒฐ ๋ฐฉ๋ฒ: ํ ์คํธ์ ๋ฐฐ๊ฒฝ์ ๋ช ์ ๋๋น๋ฅผ 4.5:1 ์ด์์ผ๋ก ์ค์ .
๋ฌธ์ 4: ์ด์ ์ด๋ ๋ ผ๋ฆฌ์ ๊ตฌ์ฑ ๋ถ์กฑ
- ํด๊ฒฐ ๋ฐฉ๋ฒ:
aria-labelledby,aria-describedby๋ฑ์ ์ฌ์ฉํด ์ด์ ์ด๋์ ๋ช ํํ ์ค์ .
๋ฌธ์ 5: ๊น๋นก์ด๋ ์ฝํ ์ธ ์ ๊ณต
- ํด๊ฒฐ ๋ฐฉ๋ฒ: ์ด๋น 3~50ํ ๊น๋นก์ด๋ ์ฝํ ์ธ ๋ฅผ ์ฌ์ฉํ์ง ์์.
์น ์ ๊ทผ์ฑ ํ๊ฐ ๋๊ตฌ ๐
1. WAVE 4
- ํน์ง:
- ์น์ฌ์ดํธ์ ์ ๊ทผ์ฑ ๋ฌธ์ ๋ฅผ ์๊ฐ์ ์ผ๋ก ํ์ํ๋ค.
- ๋์ฒด ํ ์คํธ ๋๋ฝ, ์์ ๋๋น ๋ฑ ๋ค์ํ ์ด์๋ฅผ ๋ฐ๊ฒฌํ ์ ์๋ค.
- ์ฌ์ฉ ๋ฐฉ๋ฒ:
- WAVE ์น์ฌ์ดํธ์ ์ ์ํ์ฌ URL ์ ๋ ฅ.
- ๋ธ๋ผ์ฐ์ ํ์ฅ ํ๋ก๊ทธ๋จ ์ค์น๋ ๊ฐ๋ฅ.
- ์ฅ์ :
- ์ง๊ด์ ์ธ ์ธํฐํ์ด์ค๋ก ๋ฌธ์ ์ง์ ์ ์ฝ๊ฒ ํ์ .
2. AXE 5
- ํน์ง:
- ๋ธ๋ผ์ฐ์ ๊ฐ๋ฐ์ ๋๊ตฌ์ ํตํฉ๋์ด ์ค์๊ฐ ๊ฒ์ฌ๊ฐ ๊ฐ๋ฅํ๋ค.
- ์๋ํ๋ ํ ์คํธ๋ก ์ ์ํ ํผ๋๋ฐฑ ์ ๊ณต.
- ์ฌ์ฉ ๋ฐฉ๋ฒ:
- Chrome์ด๋ Firefox์์ ํ์ฅ ํ๋ก๊ทธ๋จ ์ค์น.
- ๊ฐ๋ฐ์ ๋๊ตฌ์์ AXE ํญ์ ํตํด ๊ฒ์ฌ ์คํ.
- ์ฅ์ :
- ์ง๊ด์ ์ธ ์ธํฐํ์ด์ค๋ก ๋ฌธ์ ์ง์ ์ ์ฝ๊ฒ ํ์ .
3. Lighthouse 6
- ํน์ง:
- Google์์ ์ ๊ณตํ๋ ์คํ์์ค ๋๊ตฌ๋ก, ์ฑ๋ฅ, SEO, ์ ๊ทผ์ฑ์ ํ ๋ฒ์ ํ๊ฐํ๋ค.
- PWA(Progressive Web App) ๊ฒ์ฌ๋ ์ง์.
- ์ฌ์ฉ ๋ฐฉ๋ฒ:
- Chrome ๋ธ๋ผ์ฐ์ ์์ ๊ฐ๋ฐ์ ๋๊ตฌ ์ด๊ธฐ.
- Lighthouse ํญ์์ ๊ฒ์ฌ ํญ๋ชฉ ์ ํ ํ "Generate Report" ํด๋ฆญ.
- ์ฅ์ :
- ์ข ํฉ์ ์ธ ์น์ฌ์ดํธ ํ์ง ํ๊ฐ ๊ฐ๋ฅ.
- ๊ฐ์ ์ฌํญ์ ๋ํ ๊ตฌ์ฒด์ ์ธ ์ ์ ์ ๊ณต.
ํ๊ตญ ์น ์ ๊ทผ์ฑ ์ธ์ฆ๋งํฌ (KWCAG)
KWCAG๋?
KWCAG(Korean Web Content Accessibility Guidelines)๋ ํ์ ์์ ๋ถ์์ ์ ์ ํ ํ๊ตญํ ์น ์ฝํ ์ธ ์ ๊ทผ์ฑ ์ง์นจ์ด๋ค. WCAG 2.1์ ๊ธฐ๋ฐ์ผ๋ก ํ๋ ํ๊ตญ์ ๋ฒ์ ยท๋ฌธํ์ ๋งฅ๋ฝ์ ๋ฐ์ํ๋ค.
ํ์ฌ ์ ์ฉ ์ค์ธ ๋ฒ์ ์ KWCAG 2.2 (2022๋ ๊ฐ์ )์ด๋ฉฐ, ์ด 4๊ฐ ์์น, 14๊ฐ ์ง์นจ, 33๊ฐ ๊ฒ์ฌ ํญ๋ชฉ์ผ๋ก ๊ตฌ์ฑ๋๋ค.
์ ์ฉ ๋์
| ๊ตฌ๋ถ | ๊ธฐ์ค |
|---|---|
| ๊ณต๊ณต๊ธฐ๊ด ์น์ฌ์ดํธ | ์๋ฌด ์ค์ (์ฅ์ ์ธ์ฐจ๋ณ๊ธ์ง๋ฒ ์ 21์กฐ) |
| ํ๊ตยท๊ต์ก๊ธฐ๊ด ์น์ฌ์ดํธ | ์๋ฌด ์ค์ |
| ๋ฏผ๊ฐ ์ฌ์ ์ | ๊ถ๊ณ ์์ค (์ผ๋ถ ์ ์ข ์๋ฌดํ ์ถ์ธ) |
์น ์ ๊ทผ์ฑ ์ธ์ฆ๋งํฌ ์ทจ๋ ์ ์ฐจ
KWCAG 2.2 ์ฃผ์ ๊ฒ์ฌ ํญ๋ชฉ (WCAG 2.1๊ณผ์ ์ฐจ์ด)
KWCAG๋ WCAG 2.1์ AA ์์ค์ ๊ธฐ๋ณธ์ผ๋ก ํ๋ฉด์ ํ๊ตญ ํ๊ฒฝ์ ๋ง๋ ํญ๋ชฉ์ ์ถ๊ฐํ๋ค. ํนํ ๋ชจ๋ฐ์ผ ์ ๊ทผ์ฑ ํญ๋ชฉ์ด ๋ณด๊ฐ๋์ด ์๋ค.
| KWCAG ํญ๋ชฉ | ์ค๋ช |
|---|---|
| ์ด์ ์ด๋ (2.1.1) | ๋ ผ๋ฆฌ์ ์ธ ์์๋ก ์ด์ ์ด๋, ์ด์ ์ ๊ฐ์ง ์์ ์๊ฐ ํ์ |
| ํ ์คํธ ์ฝํ ์ธ ์ ๋ช ๋ ๋๋น | 4.5:1 ์ด์ ๋ช ๋ ๋๋น (์ผ๋ฐ ํ ์คํธ) |
| ๋๋ฅด๊ธฐ ๋์ ์ง์ | ํฐ์น ์์ญ ์ต์ 44x44px (๋ชจ๋ฐ์ผ) |
| ์ธ์ฆ ์๋จ | ์ธ์ฆ ์๋จ ์ ๊ณต (์บก์ฐจ ๋ฑ์ ๋์ ์๋จ ๋ณํ) |
| ํ๋ฉด ํ๋/์ถ์ | ๋ชจ๋ฐ์ผ์์ 200% ํ๋ ์ ์ฝํ ์ธ ์์ค ์์ |
| ์ด์์ฒด์ ์ ๊ทผ์ฑ ๊ธฐ๋ฅ ์ง์ | ํ ์คํธ ํฌ๊ธฐ, ๊ณ ๋๋น ๋ชจ๋, ๋คํฌ ๋ชจ๋ ๋ฑ OS ์ค์ ๋ฐ์ |
์ธ์ฆ ์ฌ์ฌ ๊ธฐ๊ด
๊ตญ๋ด์์ ์น ์ ๊ทผ์ฑ ์ธ์ฆ๋งํฌ ์ฌ์ฌ๋ฅผ ์ํํ๋ ๊ณต์ธ ๊ธฐ๊ด์ ๋ค์๊ณผ ๊ฐ๋ค.
- ํ๊ตญ์น์ ๊ทผ์ฑํ๊ฐ์ผํฐ (KAWAAB)
- ์น์์น (WebWatch)
- ํ๊ตญ์ฅ์ ์ธ๋จ์ฒด์ด์ฐํฉํ ์ฐํ ํ๊ฐํ
- ์ ๋ณดํต์ ์ ๊ทผ์ฑํฅ์ํ์คํํฌ๋ผ ์ง์ ๊ธฐ๊ด
์ ๊ทผ์ฑ ํ ์คํธ ํ๋ก์ธ์ค ์ ์ฒด ํ๋ฆ
์น ์ ๊ทผ์ฑ์ ๋ชจ๋๋ฅผ ์ํ ์น ๐
์น ์ ๊ทผ์ฑ์ ๋จ์ํ ๊ท์ ๊ฐ ์๋, ๋ชจ๋ ์ฌ์ฉ์๊ฐ ๋๋ฑํ ๊ธฐํ๋ฅผ ๋๋ฆด ์ ์๋๋ก ๋ณด์ฅํ๋ ์ผ์ด๋ค.
์ ๊ทผ์ฑ์ด ์ข์ ์น์ฌ์ดํธ๋ ์ฅ์ ์ธ๋ง์ ์ํ ๊ฒ์ด ์๋๋ค. ๋ ธ์ธ ์ฌ์ฉ์, ์ผ์์ ๋ถ์์, ์ ์ฌ์ ๊ธฐ๊ธฐ ์ฌ์ฉ์, ๋๋ฆฐ ๋คํธ์ํฌ ํ๊ฒฝ์ ์ฌ์ฉ์ ๋ชจ๋์๊ฒ ์ด์ ์ ์ค๋ค. ๊ทธ๋ฆฌ๊ณ ๋ฌด์๋ณด๋ค, ์ข์ ์ ๊ทผ์ฑ์ ์ข์ UX์ ๊ฐ๋ค.
์ค์ฒ ๊ด์ ์์ ๊ถ์ฅํ๋ ์ฐ์ ์์๋ ๋ค์๊ณผ ๊ฐ๋ค.
- ์๋งจํฑ HTML๋ถํฐ ์์ํ๋ค.
<button>,<label>,<nav>๋ฑ ์ฌ๋ฐ๋ฅธ ์์๋ฅผ ์ฌ์ฉํ๋ฉด ๋๋ถ๋ถ์ ๊ธฐ๋ฐ์ด ํด๊ฒฐ๋๋ค. - ์์ ๋๋น์ ํฌ์ปค์ค ํ์๋ ๋์์ธ ๋จ๊ณ์์ ์ก๋๋ค.
- ํค๋ณด๋ ๋ค๋น๊ฒ์ด์ ์ ์๋์ผ๋ก ์ง์ ํ ์คํธํด ๋ณธ๋ค.
- ์คํฌ๋ฆฐ ๋ฆฌ๋๋ก ์ง์ ๋ด ์ฌ์ดํธ๋ฅผ ํ์ํด ๋ณธ๋ค. ์ด์ํจ์ด ๋๊ปด์ง๋ ์ง์ ์ด ๊ฐ์ ๋์์ด๋ค.
- ์๋ํ ํ ์คํธ๋ฅผ CI์ ํฌํจํด ํ๊ท๋ฅผ ๋ฐฉ์งํ๋ค.
์ด๋ฅผ ํตํด ๋ ๋ง์ ์ฌ์ฉ์์๊ฒ ๋ค๊ฐ๊ฐ๊ณ , ํฌ์ฉ์ ์ธ ๋์งํธ ํ๊ฒฝ์ ๋ง๋ค์ด๋ณด์!
๊ด๋ จ ๊ธ
- ๐จ CSS Flexbox & Grid ์์ ์ ๋ณต โ ์ ๊ทผ์ฑ ์๋ ๋ ์ด์์ ๊ตฌํ์ ๊ธฐ์ด
- ๐ React ์ ๋ฌธ: SPA, ์ปดํฌ๋ํธ, JSX ํต์ฌ ๊ฐ๋ โ React ์ปดํฌ๋ํธ์์ ์ ๊ทผ์ฑ ์ ์ฉํ๊ธฐ
- โก JavaScript ๋น๋๊ธฐ ์ฒ๋ฆฌ ์์ ๊ฐ์ด๋ โ ๋์ ์ฝํ ์ธ ์ ์ ๊ทผ์ฑ
- https://www.w3.org/WAI/fundamentals/accessibility-principles/ko)โฉ
- http://www.websoul.co.kr/accessibility/WA_guide.aspโฉ
- https://developer.mozilla.org/ko/docs/Web/Accessibility/Understanding_WCAGโฉ
- https://userway.org/?utm_source=google&utm_medium=cpc&utm_campaign=3rd%20tier%20|%20search%20|%20terrific%20|%20desktop%20|%20competitors%20july%2024%20|%20max%20conversion%20value%20target%20roas&utm_content=wave%20accessibility&utm_ad=706732394142&utm_term=wave%20accessibility&matchtype=p&device=c&GeoLoc=9208388&placement=&network=g&utm_id=21500700470&campaign_id=21500700470&adset_id=170875842851&ad_id=706732394142&cq_src=google_ads&cq_cmp=21500700470&cq_con=170875842851&cq_term=wave%20accessibility&cq_med=&cq_plac=&cq_net=g&cq_pos=&cq_plt=gp&keyword_id=kwd-324645068061โฉ
- https://www.deque.com/axe/โฉ
- https://github.com/GoogleChrome/lighthouseโฉ
