๐ŸŒ ์›น ์ ‘๊ทผ์„ฑ์˜ ์ค‘์š”์„ฑ๊ณผ ๊ธฐ๋ณธ ์›์น™: ๋ชจ๋‘๋ฅผ ์œ„ํ•œ ์›น

@leekh8 ยท December 07, 2024 ยท 26 min read

์›น ์ ‘๊ทผ์„ฑ์ด๋ž€? ๐Ÿค”1

์›น ์ ‘๊ทผ์„ฑ(Web Accessibility)์€ ๋ชจ๋“  ์‚ฌ์šฉ์ž, ํŠนํžˆ ์žฅ์• ๋ฅผ ๊ฐ€์ง„ ์‚ฌ๋žŒ๋“ค์ด ์›น ์ฝ˜ํ…์ธ ์™€ ๊ธฐ๋Šฅ์— ์ ‘๊ทผํ•˜๊ณ  ์„œ๋กœ ์ž‘์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ๋ณด์žฅํ•˜๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•œ๋‹ค.

์›น ์ ‘๊ทผ์„ฑ์€ ์‹œ๊ฐ, ์ฒญ๊ฐ, ์šด๋™ ์žฅ์• ๋Š” ๋ฌผ๋ก , ๋‚˜์ด๊ฐ€ ๋งŽ๊ฑฐ๋‚˜ ์ผ์‹œ์ ์ธ ์žฅ์• ๋ฅผ ๊ฐ€์ง„ ์‚ฌ์šฉ์ž, ์‹ฌ์ง€์–ด๋Š” ์ œํ•œ๋œ ๋””๋ฐ”์ด์Šค ํ™˜๊ฒฝ์—์„œ๋„ ์ค‘์š”ํ•œ ์—ญํ• ์„ ํ•œ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด:

  • ์‹œ๊ฐ ์žฅ์• ์ธ: ํ™”๋ฉด ์ฝ๊ธฐ ์†Œํ”„ํŠธ์›จ์–ด(Screen Reader)๋ฅผ ํ†ตํ•ด ์ฝ˜ํ…์ธ ์— ์ ‘๊ทผ.
  • ์šด๋™ ์žฅ์• ์ธ: ํ‚ค๋ณด๋“œ๋งŒ์œผ๋กœ ์›น์‚ฌ์ดํŠธ ํƒ์ƒ‰.
  • ์ƒ‰๊ฐ ์ด์ƒ์ž: ๋ช…ํ™•ํ•œ ๋Œ€๋น„์™€ ๋น„์–ธ์–ด์  ์ปค๋ฎค๋‹ˆ์ผ€์ด์…˜์œผ๋กœ ์ •๋ณด ์ธ์ง€.

์›น ์ ‘๊ทผ์„ฑ์˜ ์ค‘์š”์„ฑ ๐Ÿ“ˆ

์›น ์ ‘๊ทผ์„ฑ์€ ๋‹จ์ˆœํžˆ ๋ฒ•์  ์š”๊ตฌ์‚ฌํ•ญ์„ ์ถฉ์กฑํ•˜๋Š” ๊ฒƒ์„ ๋„˜์–ด, ๋ชจ๋‘๋ฅผ ์œ„ํ•œ ํฌ์šฉ์  ์›น์„ ๋งŒ๋“ ๋‹ค.

1. ๋” ๋งŽ์€ ์‚ฌ์šฉ์ž์™€์˜ ์—ฐ๊ฒฐ

์ ‘๊ทผ์„ฑ์ด ์ข‹์€ ์›น์‚ฌ์ดํŠธ๋Š” ๋” ๋งŽ์€ ์‚ฌ๋žŒ์—๊ฒŒ ๋„๋‹ฌํ•  ์ˆ˜ ์žˆ๋‹ค.

์ด๋Š” ์‚ฌ์šฉ์ž ๊ธฐ๋ฐ˜์„ ํ™•์žฅํ•˜๊ณ  ๋น„์ฆˆ๋‹ˆ์Šค ์„ฑ์žฅ์—๋„ ๊ธฐ์—ฌํ•œ๋‹ค.

2. ๋ฒ•์  ์š”๊ตฌ์‚ฌํ•ญ ์ค€์ˆ˜

๋งŽ์€ ๊ตญ๊ฐ€์—์„œ **์›น ์ ‘๊ทผ์„ฑ ํ‘œ์ค€(WCAG)**์„ ๋ฒ•์ ์œผ๋กœ ์š”๊ตฌํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, ์ด๋ฅผ ์ค€์ˆ˜ํ•˜์ง€ ์•Š์œผ๋ฉด ๋ฒŒ๊ธˆ์„ ๋ฌผ๊ฑฐ๋‚˜ ๋ฒ•์  ๋ถ„์Ÿ์— ํœ˜๋ง๋ฆด ์ˆ˜ ์žˆ๋‹ค.

3. ๋” ๋‚˜์€ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜

์ ‘๊ทผ์„ฑ์ด ์ข‹์€ ์›น์‚ฌ์ดํŠธ๋Š” ๋ชจ๋“  ์‚ฌ์šฉ์ž์—๊ฒŒ ๋” ๋‚˜์€ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ์ œ๊ณตํ•œ๋‹ค.

์ด๋Š” ์‚ฌ์ดํŠธ์˜ ์‹ ๋ขฐ๋„๋ฅผ ๋†’์ด๊ณ , ์ดํƒˆ๋ฅ ์„ ์ค„์ด๋Š” ๋ฐ ๋„์›€์„ ์ค€๋‹ค.


์›น ์ ‘๊ทผ์„ฑ์˜ 4๊ฐ€์ง€ ๊ธฐ๋ณธ ์›์น™ (POUR) ๐ŸŒŸ23

W3C์˜ ์›น ์ ‘๊ทผ์„ฑ ์ด๋‹ˆ์…”ํ‹ฐ๋ธŒ(WAI)๋Š” ์›น ์ ‘๊ทผ์„ฑ์„ **4๊ฐ€์ง€ ๊ธฐ๋ณธ ์›์น™(POUR)**์œผ๋กœ ์ •์˜ํ•œ๋‹ค.

์›น ์ ‘๊ทผ์„ฑ POUR ์›์น™
์ธ์‹์˜ ์šฉ์ด์„ฑ

Perceivable
์šด์šฉ์˜ ์šฉ์ด์„ฑ

Operable
์ดํ•ด์˜ ์šฉ์ด์„ฑ

Understandable
๊ฒฌ๊ณ ์„ฑ

Robust
๋Œ€์ฒด ํ…์ŠคํŠธ
์ž๋ง‰ / ์ˆ˜์–ด
์ƒ‰์ƒ ๋Œ€๋น„
ํ‚ค๋ณด๋“œ ์ ‘๊ทผ
์ถฉ๋ถ„ํ•œ ์‹œ๊ฐ„
๋ฐœ์ž‘ ๋ฐฉ์ง€
์ฝ๊ธฐ ์‰ฌ์šด ์–ธ์–ด
์˜ˆ์ธก ๊ฐ€๋Šฅํ•œ ๋™์ž‘
์˜ค๋ฅ˜ ๋„์›€๋ง
ํ‘œ์ค€ HTML ์ค€์ˆ˜
ARIA ํ™œ์šฉ
๋ณด์กฐ ๊ธฐ์ˆ  ํ˜ธํ™˜

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 ํ•จ์ˆ˜๊ฐ€ ์ด ์—ญํ• ์„ ํ•œ๋‹ค. ์š”์•ฝํ•˜๋ฉด:

  1. ๋ชจ๋‹ฌ ๋‚ด ํฌ์ปค์Šค ๊ฐ€๋Šฅํ•œ ๋ชจ๋“  ์š”์†Œ๋ฅผ ๋ชฉ๋กํ™”ํ•œ๋‹ค.
  2. ๋งˆ์ง€๋ง‰ ์š”์†Œ์—์„œ Tab์„ ๋ˆ„๋ฅด๋ฉด ์ฒซ ์š”์†Œ๋กœ, ์ฒซ ์š”์†Œ์—์„œ Shift+Tab์„ ๋ˆ„๋ฅด๋ฉด ๋งˆ์ง€๋ง‰ ์š”์†Œ๋กœ ์ด๋™์‹œํ‚จ๋‹ค.
  3. 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) ๊ณต์‹์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

  1. RGB ๊ฐ ์ฑ„๋„์„ 0~1 ๋ฒ”์œ„๋กœ ์ •๊ทœํ™”ํ•œ ํ›„ ๊ฐ๋งˆ ๋ณด์ •์„ ์ ์šฉํ•œ๋‹ค.
  2. L = 0.2126 ร— R + 0.7152 ร— G + 0.0722 ร— B
  3. ๋Œ€๋น„์œจ = (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์กฐ)
ํ•™๊ตยท๊ต์œก๊ธฐ๊ด€ ์›น์‚ฌ์ดํŠธ ์˜๋ฌด ์ค€์ˆ˜
๋ฏผ๊ฐ„ ์‚ฌ์—…์ž ๊ถŒ๊ณ  ์ˆ˜์ค€ (์ผ๋ถ€ ์—…์ข… ์˜๋ฌดํ™” ์ถ”์„ธ)

์›น ์ ‘๊ทผ์„ฑ ์ธ์ฆ๋งˆํฌ ์ทจ๋“ ์ ˆ์ฐจ

ํ†ต๊ณผ
๋ฏธํ†ต๊ณผ
์ž์ฒด ํ‰๊ฐ€
์‹ ์ฒญ ๊ธฐ๊ด€ ์„ ์ •
์˜ˆ๋น„ ์‹ฌ์‚ฌ

์ž๋™ํ™” ๋„๊ตฌ
์ •๋ฐ€ ์‹ฌ์‚ฌ

์ „๋ฌธ๊ฐ€ + ์‚ฌ์šฉ์ž ํ‰๊ฐ€
ํ†ต๊ณผ ์—ฌ๋ถ€
์ธ์ฆ๋งˆํฌ ๋ถ€์—ฌ

์œ ํšจ๊ธฐ๊ฐ„ 1๋…„
๋ณด์™„ ํ›„ ์žฌ์‹ ์ฒญ
์œ ์ง€ ๊ด€๋ฆฌ ๋ฐ

์—ฐ๊ฐ„ ๊ฐฑ์‹ 

KWCAG 2.2 ์ฃผ์š” ๊ฒ€์‚ฌ ํ•ญ๋ชฉ (WCAG 2.1๊ณผ์˜ ์ฐจ์ด)

KWCAG๋Š” WCAG 2.1์˜ AA ์ˆ˜์ค€์„ ๊ธฐ๋ณธ์œผ๋กœ ํ•˜๋ฉด์„œ ํ•œ๊ตญ ํ™˜๊ฒฝ์— ๋งž๋Š” ํ•ญ๋ชฉ์„ ์ถ”๊ฐ€ํ–ˆ๋‹ค. ํŠนํžˆ ๋ชจ๋ฐ”์ผ ์ ‘๊ทผ์„ฑ ํ•ญ๋ชฉ์ด ๋ณด๊ฐ•๋˜์–ด ์žˆ๋‹ค.

KWCAG ํ•ญ๋ชฉ ์„ค๋ช…
์ดˆ์  ์ด๋™ (2.1.1) ๋…ผ๋ฆฌ์ ์ธ ์ˆœ์„œ๋กœ ์ดˆ์  ์ด๋™, ์ดˆ์ ์„ ๊ฐ€์ง„ ์š”์†Œ ์‹œ๊ฐ ํ‘œ์‹œ
ํ…์ŠคํŠธ ์ฝ˜ํ…์ธ ์˜ ๋ช…๋„ ๋Œ€๋น„ 4.5:1 ์ด์ƒ ๋ช…๋„ ๋Œ€๋น„ (์ผ๋ฐ˜ ํ…์ŠคํŠธ)
๋ˆ„๋ฅด๊ธฐ ๋™์ž‘ ์ง€์› ํ„ฐ์น˜ ์˜์—ญ ์ตœ์†Œ 44x44px (๋ชจ๋ฐ”์ผ)
์ธ์ฆ ์ˆ˜๋‹จ ์ธ์ฆ ์ˆ˜๋‹จ ์ œ๊ณต (์บก์ฐจ ๋“ฑ์€ ๋Œ€์•ˆ ์ˆ˜๋‹จ ๋ณ‘ํ–‰)
ํ™”๋ฉด ํ™•๋Œ€/์ถ•์†Œ ๋ชจ๋ฐ”์ผ์—์„œ 200% ํ™•๋Œ€ ์‹œ ์ฝ˜ํ…์ธ  ์†์‹ค ์—†์Œ
์šด์˜์ฒด์ œ ์ ‘๊ทผ์„ฑ ๊ธฐ๋Šฅ ์ง€์› ํ…์ŠคํŠธ ํฌ๊ธฐ, ๊ณ ๋Œ€๋น„ ๋ชจ๋“œ, ๋‹คํฌ ๋ชจ๋“œ ๋“ฑ OS ์„ค์ • ๋ฐ˜์˜

์ธ์ฆ ์‹ฌ์‚ฌ ๊ธฐ๊ด€

๊ตญ๋‚ด์—์„œ ์›น ์ ‘๊ทผ์„ฑ ์ธ์ฆ๋งˆํฌ ์‹ฌ์‚ฌ๋ฅผ ์ˆ˜ํ–‰ํ•˜๋Š” ๊ณต์ธ ๊ธฐ๊ด€์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

  • ํ•œ๊ตญ์›น์ ‘๊ทผ์„ฑํ‰๊ฐ€์„ผํ„ฐ (KAWAAB)
  • ์›น์™€์น˜ (WebWatch)
  • ํ•œ๊ตญ์žฅ์• ์ธ๋‹จ์ฒด์ด์—ฐํ•ฉํšŒ ์‚ฐํ•˜ ํ‰๊ฐ€ํŒ€
  • ์ •๋ณดํ†ต์‹ ์ ‘๊ทผ์„ฑํ–ฅ์ƒํ‘œ์ค€ํ™”ํฌ๋Ÿผ ์ง€์ • ๊ธฐ๊ด€

์ ‘๊ทผ์„ฑ ํ…Œ์ŠคํŠธ ํ”„๋กœ์„ธ์Šค ์ „์ฒด ํ๋ฆ„

๊ฐœ์„  ์‚ฌํ•ญ
๊ธฐํš/๋””์ž์ธ ๋‹จ๊ณ„
์ƒ‰์ƒ ๋Œ€๋น„ ๊ฒ€ํ† 
ํฌ์ปค์Šค ํ๋ฆ„ ์„ค๊ณ„
์ฝ˜ํ…์ธ  ๊ตฌ์กฐ ์„ค๊ณ„

์ œ๋ชฉ ๊ณ„์ธต, ๋žœ๋“œ๋งˆํฌ
๊ฐœ๋ฐœ ๋‹จ๊ณ„
์‹œ๋งจํ‹ฑ HTML ์ž‘์„ฑ
ARIA ์ ์šฉ
ํ‚ค๋ณด๋“œ ๋‚ด๋น„๊ฒŒ์ด์…˜ ๊ตฌํ˜„
ํผ ์ ‘๊ทผ์„ฑ ๊ตฌํ˜„
ํ…Œ์ŠคํŠธ ๋‹จ๊ณ„
์ž๋™ํ™” ํ…Œ์ŠคํŠธ

jest-axe / Playwright+axe
๋ธŒ๋ผ์šฐ์ € ๋„๊ตฌ

Lighthouse / WAVE / AXE
์Šคํฌ๋ฆฐ ๋ฆฌ๋” ํ…Œ์ŠคํŠธ

NVDA / VoiceOver
์‹ค์ œ ์‚ฌ์šฉ์ž ํ…Œ์ŠคํŠธ
๋ฐฐํฌ/์šด์˜
CI/CD ์ ‘๊ทผ์„ฑ ๊ฒŒ์ดํŠธ
์ •๊ธฐ ๊ฐ์‚ฌ
์‚ฌ์šฉ์ž ํ”ผ๋“œ๋ฐฑ ์ˆ˜์ง‘


์›น ์ ‘๊ทผ์„ฑ์€ ๋ชจ๋‘๋ฅผ ์œ„ํ•œ ์›น ๐ŸŒ

์›น ์ ‘๊ทผ์„ฑ์€ ๋‹จ์ˆœํ•œ ๊ทœ์ œ๊ฐ€ ์•„๋‹Œ, ๋ชจ๋“  ์‚ฌ์šฉ์ž๊ฐ€ ๋™๋“ฑํ•œ ๊ธฐํšŒ๋ฅผ ๋ˆ„๋ฆด ์ˆ˜ ์žˆ๋„๋ก ๋ณด์žฅํ•˜๋Š” ์ผ์ด๋‹ค.

์ ‘๊ทผ์„ฑ์ด ์ข‹์€ ์›น์‚ฌ์ดํŠธ๋Š” ์žฅ์• ์ธ๋งŒ์„ ์œ„ํ•œ ๊ฒƒ์ด ์•„๋‹ˆ๋‹ค. ๋…ธ์ธ ์‚ฌ์šฉ์ž, ์ผ์‹œ์  ๋ถ€์ƒ์ž, ์ €์‚ฌ์–‘ ๊ธฐ๊ธฐ ์‚ฌ์šฉ์ž, ๋А๋ฆฐ ๋„คํŠธ์›Œํฌ ํ™˜๊ฒฝ์˜ ์‚ฌ์šฉ์ž ๋ชจ๋‘์—๊ฒŒ ์ด์ ์„ ์ค€๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๋ฌด์—‡๋ณด๋‹ค, ์ข‹์€ ์ ‘๊ทผ์„ฑ์€ ์ข‹์€ UX์™€ ๊ฐ™๋‹ค.

์‹ค์ฒœ ๊ด€์ ์—์„œ ๊ถŒ์žฅํ•˜๋Š” ์šฐ์„ ์ˆœ์œ„๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

  1. ์‹œ๋งจํ‹ฑ HTML๋ถ€ํ„ฐ ์‹œ์ž‘ํ•œ๋‹ค. <button>, <label>, <nav> ๋“ฑ ์˜ฌ๋ฐ”๋ฅธ ์š”์†Œ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋Œ€๋ถ€๋ถ„์˜ ๊ธฐ๋ฐ˜์ด ํ•ด๊ฒฐ๋œ๋‹ค.
  2. ์ƒ‰์ƒ ๋Œ€๋น„์™€ ํฌ์ปค์Šค ํ‘œ์‹œ๋Š” ๋””์ž์ธ ๋‹จ๊ณ„์—์„œ ์žก๋Š”๋‹ค.
  3. ํ‚ค๋ณด๋“œ ๋„ค๋น„๊ฒŒ์ด์…˜์„ ์ˆ˜๋™์œผ๋กœ ์ง์ ‘ ํ…Œ์ŠคํŠธํ•ด ๋ณธ๋‹ค.
  4. ์Šคํฌ๋ฆฐ ๋ฆฌ๋”๋กœ ์ง์ ‘ ๋‚ด ์‚ฌ์ดํŠธ๋ฅผ ํƒ์ƒ‰ํ•ด ๋ณธ๋‹ค. ์–ด์ƒ‰ํ•จ์ด ๋А๊ปด์ง€๋Š” ์ง€์ ์ด ๊ฐœ์„  ๋Œ€์ƒ์ด๋‹ค.
  5. ์ž๋™ํ™” ํ…Œ์ŠคํŠธ๋ฅผ CI์— ํฌํ•จํ•ด ํšŒ๊ท€๋ฅผ ๋ฐฉ์ง€ํ•œ๋‹ค.

์ด๋ฅผ ํ†ตํ•ด ๋” ๋งŽ์€ ์‚ฌ์šฉ์ž์—๊ฒŒ ๋‹ค๊ฐ€๊ฐ€๊ณ , ํฌ์šฉ์ ์ธ ๋””์ง€ํ„ธ ํ™˜๊ฒฝ์„ ๋งŒ๋“ค์–ด๋ณด์ž!


๊ด€๋ จ ๊ธ€

@leekh8
๋ณด์•ˆ, ์›น ๊ฐœ๋ฐœ, Python์„ ๋‹ค๋ฃจ๋Š” ๊ธฐ์ˆ  ๋ธ”๋กœ๊ทธ