Post

React ์ ‘๊ทผ์„ฑ

๐Ÿ“Œ์›น ์ ‘๊ทผ์„ฑ

์›น ์ ‘๊ทผ์„ฑ(a11y)์€ ๋น„์žฅ์• ์ธ, ์žฅ์• ์ธ ๋ชจ๋‘๊ฐ€ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ์›น์‚ฌ์ดํŠธ๋ฅผ ๊ฐœ๋ฐœํ•˜๋Š” ๊ฒƒ์„ ๋œปํ•œ๋‹ค. React์—์„œ๋Š” ์ ‘๊ทผ์„ฑ์„ ๊ฐ–์ถ˜ ์›น์‚ฌ์ดํŠธ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋„๋ก ๋ชจ๋“  ์ง€์›์„ ํ•˜๊ณ  ์žˆ๋‹ค๊ณ  ํ•˜๋Š”๋ฐ, ์ด๋ฒˆ์—๋Š” ์ด ๋ถ€๋ถ„์„ ํƒ์ƒ‰ํ•ด๋ณด๊ณ ์ž ํ•œ๋‹ค. ๐Ÿค“

๐ŸคWAI-ARIA

WAI-ARIA(Accessible Rich Internet Applications Suite)๋Š” ์›น ์ฝ˜ํ…์ธ ์™€ UI ์š”์†Œ์˜ ์˜๋ฏธ(semantic meaning)๋ฅผ ๋ถ€์—ฌํ•˜์—ฌ, ์Šคํฌ๋ฆฐ ๋ฆฌ๋”(screen reader)์™€ ๊ฐ™์€ ๋ณด์กฐ ๊ธฐ์ˆ ์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ธ์‹ํ•˜๊ณ  ์ƒํ˜ธ์ž‘์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ๋•๋Š” ์—ญํ• ์„ ํ•œ๋‹ค.

JSX์—์„œ๋Š” ๋ชจ๋“  aria-* ์–ดํŠธ๋ฆฌ๋ทฐํŠธ๋ฅผ ์ง€์›ํ•œ๋‹ค. ๋‹ค๋งŒ react์—์„œ ๋Œ€๋ถ„์˜ property, attribute๊ฐ€ camel case (onClick ๋“ฑ)๋กœ ์ง€์›ํ•˜๋‚˜, aria-* ์˜ ๊ฒฝ์šฐ ์ผ๋ฐ˜์ ์ธ HTML๊ณผ ๊ฐ™์ด hypen-case๋กœ ์ž‘์„ฑํ•œ๋‹ค.

โœจ์ฐธ๊ณ ์‚ฌํ•ญ HTML5๋Š” semantic tag๋ฅผ ์ด์šฉํ•ด ๊ธฐ์กด์˜ aria์˜ ์—ญํ• ์„ ๋Œ€์ฒดํ•˜๊ฒŒ ๋˜์—ˆ๊ธฐ์— ๋˜๋„๋ก aria ๋Œ€์‹  sematinc HTML Tag๋ฅผ ํ™œ์šฉํ•ด ์ž‘์„ฑํ•˜๋Š”๊ฒƒ์ด ์ข‹๋‹ค.

aria ์ข…๋ฅ˜์„ค๋ช…
aria-haspopupelemeent๊ฐ€ ํŒ์—…(๋ฉ”๋‰ด, ๋ฆฌ์ŠคํŠธ๋ฐ•์Šค, ํŠธ๋ฆฌ, ๊ทธ๋ฆฌ๋“œ, ๋‹ค์ด์–ผ๋กœ๊ทธ) ๋ฅผ ํŠธ๋ฆฌ๊ฑฐํ•  ์ˆ˜ ์žˆ์Œ
aria-autocomplete์ž…๋ ฅํ•„๋“œ์— ์ž๋™์™„์„ฑ ๊ธฐ๋Šฅ์ด ์žˆ๋‹ค๊ณ  ์•Œ๋ ค ์คŒ (๊ธฐ๋Šฅ์€ ๊ฐœ๋ฐœ์ž๊ฐ€ ๊ตฌํ˜„ํ•ด์•ผ ํ•จ)
aria-ownsDOM์€ ๋ถ€๋ชจ-์ž์‹ ๊ด€๊ณ„๊ฐ€ ์•„๋‹ˆ๋‚˜, ์‹œ๊ฐ์ ์œผ๋กœ ๋ถ€๋ชจ-์ž์‹ ๊ด€๊ณ„์ผ ๋•Œ
aria-activedescendantํ˜„์žฌ ํ™œ์„ฑํ™”๋œ ํ•˜์œ„ elemeent๋ฅผ ์‹๋ณ„
aria-controlsํ•ด๋‹น element๊ฐ€ ๋‹ค๋ฅธ element์˜ ๋‚ด์šฉ์„ ์ œ์–ดํ•˜๊ณ  ์žˆ๋‹ค๋Š” ๊ฒƒ์„ ๋ช…์‹œํ•จ
aria-selectedํ•ด๋‹น element๊ฐ€ ์„ ํƒ ๋˜์—ˆ์Œ์„ ๋‚˜ํƒ€๋‚ธ๋‹ค.
aria-labelelement์˜ ์ด๋ฆ„์„ ์ •์˜. label์ด ์—†๊ฑฐ๋‚˜ ์•„์ด์ฝ˜๋งŒ ์žˆ์„ ๊ฒฝ์šฐ ์œ ์šฉํ•จ
aria-labelledbyelement์˜ ์ ‘๊ทผ์„ฑ์„ ์œ„ํ•œ ์ด๋ฆ„์„ ์ •์˜ํ•  ๋•Œ ๋‹ค๋ฅธ ์š”์†Œ๋ฅผ ์ฐธ์กฐํ•˜๋„๋ก ์„ค์ •ํ•จ
aria-expanded์š”์†Œ๊ฐ€ ํ™•์žฅ๋˜์–ด ๋ณด์ด๋Š”์ง€, ์ถ•์†Œ๋˜์–ด ๋ณด์ด์ง€ ์•Š๋Š”์ง€๋ฅผ ๋‚˜ํƒ€๋ƒ„
aria-currentํ˜„์žฌ ํ™œ์„ฑํ™”๋œ ํ•ญ๋ชฉ์„ ๋‚˜ํƒ€๋ƒ„
aria-orientation์š”์†Œ์˜ ๋ฐฉํ–ฅ ๋ช…ํ™•ํžˆ ์ง€์ • (ํ‚ค๋ณด๋“œ๋ฅผ ์–ด๋–ป๊ฒŒ ์‚ฌ์šฉํ•ด์•ผ ํ• ์ง€ ์•Œ๋ ค์คŒ)
aria-pressedํ† ๊ธ€ ๋ฒ„ํŠผ์˜ ํ˜„์žฌ ๋ˆŒ๋ ค์žˆ๋Š” ์ƒํƒœ ๋‚˜ํƒ€๋ƒ„
aria-checkedcheckbox, radio button ๋“ฑ์ด ํ˜„์žฌ ์ฒดํฌ๋œ ์ƒํƒœ์ธ์ง€ ๋‚˜ํƒ€๋ƒ„
aria-hiddenํ•ด๋‹น element๊ฐ€๊ฐ€ ์Šคํฌ๋ฆฐ ๋ฆฌ๋”์—๊ฒŒ๋„ ๋…ธ์ถœ์‹œํ‚ฌ ๊ฒƒ์ธ์ง€ ์—ฌ๋ถ€
aria-describedbyํ•ด๋‹น element์˜ ์„ค๋ช…์„ ์ œ๊ณตํ•˜๋Š” ๋‹ค๋ฅธ element์˜ ID๋ฅผ ์ง€์ •ํ•จ
aria-requiredํผ(input, select, textarea ๋“ฑ)์ด ํ•„์ˆ˜ ์ž…๋ ฅ๊ฐ’์ธ์ง€ ๋‚˜ํƒ€๋ƒ„
aria-invalid์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์‹คํŒจํ–ˆ์„ ๋•Œ ์‚ฌ์šฉ

๋‹ค๋งŒ, ๋ฌธ์„œ ๊ตฌ์กฐ, ์Šคํƒ€์ผ, ์Šคํฌ๋ฆฝํŠธ ๋“ฑ์˜ ๊ธฐ๋Šฅ์„ ์ •์˜ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉํ•˜๋Š” style, meta, html, script ์š”์†Œ์˜ ๊ฒฝ์šฐ aria๋ฅผ ์ง€์›ํ•˜์ง€ ์•Š๋Š”๋‹ค.

์œ„์˜ ์„ค๋ช…๋งŒ์œผ๋กœ ์ดํ•ดํ•˜๊ธฐ ์–ด๋ ค์šด ๋‚ด์šฉ์€ ์•„๋ž˜์— ์ •๋ฆฌํ•ด ๋‘์—ˆ๋‹ค! ๐Ÿ˜Ž

๐Ÿ”ธ aria-controls

์ด aria๋Š” ํ•ด๋‹น ์š”์†Œ๊ฐ€ ๋‹ค๋ฅธ ์š”์†Œ๋ฅผ ์ œ์–ดํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋œ๋‹ค๋Š” ๊ฒƒ์„ ๋ช…์‹œํ•œ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด,

  • ์ด ํƒญ์„ ํด๋ฆญํ•ด ํƒญ ๋‚ด์šฉ์„ ๋ฐ”๊ฟ€ ์ˆ˜ ์žˆ์Œ
  • combobox๋กœ, input ๋ฐ•์Šค๋ฅผ ํด๋ฆญํ•ด dropdown์„ ์—ด๊ฑฐ๋‚˜ ์ง์ ‘ ์ž…๋ ฅํ•  ์ˆ˜ ์žˆ์Œ

๋“ฑ์ด ์žˆ๋‹ค.

1
2
3
4
5
6
7
8
9
10
11
12
13
<div role="tablist" aria-label="์„ค์ • ํƒญ ๋ชฉ๋ก">
  <button id="tab1" role="tab" aria-selected="true" aria-controls="panel1">
    ์ผ๋ฐ˜
  </button>
  <button id="tab2" role="tab" aria-selected="false" aria-controls="panel2">
    ๊ณ ๊ธ‰
  </button>
</div>

<div id="panel1" role="tabpanel" aria-labelledby="tab1">์ผ๋ฐ˜ ์„ค์ • ๋‚ด์šฉ</div>
<div id="panel2" role="tabpanel" aria-labelledby="tab2" hidden>
  ๊ณ ๊ธ‰ ์„ค์ • ๋‚ด์šฉ
</div>

์œ„์˜ ์˜ˆ์ œ๋Š” ํƒญ์„ ๋ˆŒ๋Ÿฌ ๋‚ด์šฉ์„ ๋ณ€๊ฒฝํ•˜๋Š” ์˜ˆ์ œ๋‹ค. ์—ฌ๊ธฐ์„œ aria-controls ์™€ id๋ฅผ ํ†ตํ•ด ํ•ด๋‹น ๋ฒ„ํŠผ์ด ์–ด๋–ค ํƒญ์„ control ํ•˜๋Š”์ง€๋ฅผ ๋ช…์‹œํ•œ๋‹ค.

๐Ÿ”ธ aria-selected

ํ•ด๋‹น ์š”์†Œ๊ฐ€ ์„ ํƒ๋˜์–ด ์žˆ์Œ์„ ๋‚˜ํƒ€๋‚ด๋ฉฐ, ํŠนํžˆ tab, option, gridcell, row ๋“ฑ์—์„œ ์ฃผ๋กœ ์‚ฌ์šฉ๋œ๋‹ค. ๋˜ํ•œ ๋‘ ๊ฐœ ์ด์ƒ์˜ ์š”์†Œ๋ฅผ ์„ ํƒํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒฝ์šฐ arai-multiselectable='true'๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ํ•ด๋‹น ์š”์†Œ์˜ ์—ญํ• ์— ๋”ฐ๋ผ aria-current ๋˜๋Š” arai-checked, aria-pressed ์„ค์ •์„ ํ•  ์ˆ˜ ์žˆ๋‹ค!

์ข…๋ฅ˜์„ค๋ช…
aria-selected์„ ํƒ ๊ฐ€๋Šฅํ•œ ์š”์†Œ์—์„œ ํ˜„์žฌ ์„ ํƒ๋œ ํ•ญ๋ชฉ
ํƒญ, ๋ฆฌ์ŠคํŠธ, ํ…Œ์ด๋ธ” ๋“ฑ
aria-currentํ˜„์žฌ ์œ„์น˜, ์ง„ํ–‰ ์ค‘ ์ƒํƒœ
๋‚ด๋น„๊ฒŒ์ด์…˜ ๋“ฑ์—์„œ ํ˜„์žฌ ์œ„์น˜ ํ‘œ์‹œ ๋“ฑ
aria-checked์ฒดํฌ ์—ฌ๋ถ€
์ฒดํฌ๋ฐ•์Šค, ๋ผ๋””์˜ค ๋ฒ„ํŠผ ๋“ฑ
aria-pressed๋ฒ„ํŠผ์„ ๋ˆŒ๋ €๋Š”์ง€
ํˆด๋ฐ” ๋“ฑ

๐Ÿ”ธ aria-describedby

1
2
<button aria-describedby="trash-desc">ํœด์ง€ํ†ต์œผ๋กœ ์ด๋™</button>
<p id="trash-desc">ํœด์ง€ํ†ต์— ๋‹ด๊ธด ํŒŒ์ผ์€ 30์ผ ์ดํ›„ ์˜๊ตฌ์ ์œผ๋กœ ์‚ญ์ œ๋ฉ๋‹ˆ๋‹ค!</p>

buttonํƒœ๊ทธ์˜ aria-describedby์™€ pํƒœ๊ทธ์˜ id๊ฐ€ ์—ฐ๊ฒฐ๋˜์–ด, button๊ณผ ๊ด€๋ จ๋œ ์ค‘์š”ํ•œ ์ •๋ณด๋ฅผ ์‰ฝ๊ฒŒ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•œ๋‹ค.

์—ฌ๊ธฐ์„œ ์ค‘์š”ํ•œ ์ ์€ pํƒœ๊ทธ(์„ค๋ช…)์— aria-describedby๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ button์— ์ž‘์„ฑํ•ด์„œ, ์ถ”๊ฐ€์ ์ธ ์ •๋ณด๋ฅผ ์•ˆ๋‚ดํ•˜๋Š” ํŠน์ • element๊ฐ€ ์žˆ๋‹ค๊ณ  ์•Œ๋ ค์ค€๋‹ค๋Š” ๊ฒƒ์ด๋‹ค!

๐Ÿ”ธ aria-activedescendant

ํ‚ค๋ณด๋“œ๋กœ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•œ dropbox, list ๋“ฑ์—์„œ ํ•˜์œ„์š”์†Œ์ธ ํŠน์ • ๊ฐ’์„ ์‹๋ณ„ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. ๋”ฐ๋ผ์„œ, ์ด ๊ฐ’์€ ๋™์ ์œผ๋กœ ์—…๋ฐ์ดํŠธ ํ•ด์ฃผ์–ด์•ผ ์‚ฌ์šฉ์ž๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ธ์‹ํ•  ์ˆ˜ ์žˆ๋‹ค.

1
2
3
4
5
6
<input type="text" aria-activedescendant="option-2" />
<ul>
  <li id="option-1">์˜ต์…˜ 1</li>
  <li id="option-2">์˜ต์…˜ 2</li> //โœจํ˜„์žฌ ํ™œ์„ฑํ™” ๋œ ์š”์†Œ
  <li id="option-3">์˜ต์…˜ 3</li>
</ul>

๐Ÿ”ธ aria-current

ํ˜„์žฌ ํ™œ์„ฑํ™” ๋œ ํ•ญ๋ชฉ์„ ๋‚˜ํƒ€๋‚ด๋ฉฐ, ๋ฉ”๋‰ด, ํƒญ, ๋„ค๋น„๊ฒŒ์ด์…˜ ๋“ฑ์—์„œ ๋‹ค์–‘ํ•˜๊ฒŒ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๊ฐ’์˜ ์ข…๋ฅ˜๊ฐ€ ๋งŽ์€๋ฐ ํ‘œ๋กœ ์‚ดํŽด๋ณด์ž.

๊ฐ’์„ค๋ช…
pagenavigation์—์„œ ํ˜„์žฌ ํŽ˜์ด์ง€
stepํ”„๋กœ์„ธ์Šค ์ง„ํ–‰ ์ค‘ ํ˜„์žฌ step
locationbreadcrumb์—์„œ ์‚ฌ์šฉ์ž์˜ ํ˜„์žฌ ์œ„์น˜
dateํ˜„์žฌ ๋‚ ์งœ
timeํ˜„์žฌ ์‹œ๊ฐ„
trueํ˜„์žฌ ์„ ํƒ๋œ ํ•ญ๋ชฉ
false(default)๋น„ํ™œ์„ฑ ์ƒํƒœ
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<nav aria-label="Breadcrumb">
  <ol>
    <li>
      <a href="/home">ํ™ˆ</a>
    </li>
    <li>
      <a href="/products">์ œํ’ˆ</a>
    </li>
    <li>
      <a href="/products/shoes" aria-current="location">
        ์‹ ๋ฐœ
      </a>
    </li>
  </ol>
</nav>

๐Ÿ”ธ aria-orientation

๊ธฐ๋ณธ์ ์œผ๋กœ ํŠน์ • ์š”์†Œ๋“ค์€ ๋ฐฉํ–ฅ์ด ์ง€์ •๋˜์–ด ์žˆ์œผ๋‚˜, ๋งŒ์•ฝ ๋ฐฉํ–ฅ์ด ๋ฐ”๋€Œ์–ด ์žˆ๋Š” ์š”์†Œ๋ผ๋ฉด ์‚ฌ์šฉ์ž๊ฐ€ ํ‚ค๋ณด๋“œ๋กœ ํƒ์ƒ‰ํ•˜๊ธฐ ์‰ฝ๋„๋ก ์ด ์š”์†Œ์˜ ๋ฐฉํ–ฅ์„ ์•Œ๋ ค์ฃผ๋Š” ์—ญํ• ์„ ํ•œ๋‹ค.

์•„๋ž˜๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ์ง€์ •๋˜์–ด ์žˆ๋Š” ๋ฐฉํ–ฅ ๊ฐ’์œผ๋กœ ์ด ๋ฐฉํ–ฅ๊ณผ ๋ฐ˜๋Œ€์ผ ๋•Œ ๋ช…์‹œ์ ์œผ๋กœ ์ ์–ด์ฃผ๋ฉด ๋œ๋‹ค.

๐Ÿ”ท ๊ฐ€๋กœ

  • slider
  • tablist
  • toolbar
  • menubar

๐Ÿ”ท ์„ธ๋กœ

  • scrollbar
  • tree
  • listbox
  • menu

๐Ÿ”ธ aria-label VS aria-labelledby

๋‘ ๊ฐ€์ง€ ๋ชจ๋‘ ์›น ์ ‘๊ทผ์„ฑ์„ ๊ฐœ์„ ํ•˜๊ธฐ ์œ„ํ•ด ์ถ”๊ฐ€์ ์ธ โ€˜์ •๋ณดโ€™๋ฅผ ์ œ๊ณตํ•˜๋Š” ๊ฒƒ์€ ๋™์ผํ•˜๋‚˜ ๋ฐฉ์‹๊ณผ ์‚ฌ์šฉ ์‹œ์ ์ด ๋‹ค๋ฅด๋‹ค.

aria ์ข…๋ฅ˜์‚ฌ์šฉ ๋ฐฉ์‹
aria-labelaria์— ์ง์ ‘ ๋ฌธ์ž์—ด ์ž‘์„ฑ
aria-labelledby๋‹ค๋ฅธ ์š”์†Œ id ์ฐธ๊ณ 

๐Ÿ”ท aria-label

1
2
3
<button aria-label="๊ฒ€์ƒ‰">
  <img src="search-icon.svg" alt="" />
</button>
  • alt๋ฅผ ๊ณต๋ฐฑ์œผ๋กœ ์ฃผ์–ด ์Šคํฌ๋ฆฐ ๋ฆฌ๋”๊ฐ€ ๋ถˆํ•„์š”ํ•˜๊ฒŒ img์˜ alt๋ฅผ ์ฝ์ง€ ์•Š๋Š”๋‹ค.
  • ๋Œ€์‹ , aria-label์˜ ๊ฒ€์ƒ‰ ์„ ์ฝ์–ด ๊ฒ€์ƒ‰์„ ์œ„ํ•œ ๋ฒ„ํŠผ์ž„์„ ์ธ์ง€ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•œ๋‹ค.
1
2
3
4
<button>
  <img src="search-icon.svg" alt="" />
  <span>๊ฒ€์ƒ‰</span>
</button>
  • ์ด ๊ฒฝ์šฐ, ๊ฒ€์ƒ‰์ด๋ž€ ๊ธ€์ž๊ฐ€ button์•ˆ์— ์ž‘์„ฑ๋˜์–ด ์Šคํฌ๋ฆฐ ๋ฆฌ๋”๊ฐ€ ์ฝ๊ธฐ ๋•Œ๋ฌธ์— aria-label์„ ๊ตณ์ด ์ง€์ •ํ•  ํ•„์š” ์—†๋‹ค.

๐Ÿ”ท aria-labelledby

id๋ฅผ ์—ฐ๊ฒฐํ•˜์—ฌ ์ด๋ฏธ ์กด์žฌํ•˜๋Š” ๋‹ค๋ฅธ element์˜ text๋ฅผ ์ด๋ฆ„์œผ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

1
2
<p id="label-id">๊ฒ€์ƒ‰ ๋ฒ„ํŠผ</p>
<button aria-labelledby="label-id"></button>

aria-labelledby ์™€ p ํƒœ๊ทธ์˜ id๋ฅผ ๋™์ผํ•œ ๊ฐ’์œผ๋กœ ์ง€์ •ํ•ด ๋‘ elements๋ฅผ ์—ฐ๊ฒฐํ•˜์—ฌ ์ด๋ฆ„์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค. ์ด ๊ฒฝ์šฐ ํ™”๋ฉด์˜ ํ…์ŠคํŠธ๋ฅผ ์žฌ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ์žฅ์ ์ด ์žˆ๋‹ค.

๐Ÿคrole

ํŠน์ • HTML ์š”์†Œ๊ฐ€ ์ˆ˜ํ–‰ํ•˜๋Š” ์—ญํ• ์„ ์ •์˜ํ•˜์—ฌ ๋ณด์กฐ๊ธฐ์ˆ ์ด ํ•ด๋‹น ์š”์†Œ์˜ ์—ญํ• ์„ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ํ•ด์„ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•œ๋‹ค. ๋ฌผ๋ก , ๋งˆํฌ์—…์„ ํ•  ๋•Œ ์‹œ๋ฉ˜ํ‹ฑ ํƒœ๊ทธ๋ฅผ ์ด์šฉํ•ด ์˜๋ฏธ๋ฅผ ๋ช…ํ™•ํžˆ ๋“œ๋Ÿฌ๋‚ด๋ฉฐ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ์ด ์ข‹์ง€๋งŒ, Aria role์„ ์‚ฌ์šฉํ•ด ๋ณด๋‹ค ๋ช…ํ™•ํ•œ ์˜๋ฏธ๋ฅผ ๋ถ€์—ฌํ•  ์ˆ˜ ์žˆ๋‹ค.

์˜ˆ๋ฅผ๋“ค์–ด <div>์™€ ๊ฐ™์€ ์‹œ๋ฉ˜ํ‹ฑ ํƒœ๊ทธ๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ, ํ•ด๋‹น ํƒœ๊ทธ๋ฅผ ์ด์šฉํ•ด ๋งŒ๋“  ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ฌด์Šจ ์—ญํ• ์„ ํ•˜๋Š”์ง€ ๋ช…์‹œํ•˜์—ฌ ์Šคํฌ๋ฆฐ ๋ฆฌ๋”๊ฐ€ ์ •ํ™•ํžˆ ํ•ด๋‹น ๋‚ด์šฉ์„ ์ธ์‹ํ•˜๊ฒŒ ๋งŒ๋“ ๋‹ค.

MDN ์—์„œ๋Š” role์˜ ์—ญํ• ์„ 6๊ฐœ๋กœ ์†Œ๊ฐœํ•˜๊ณ  ์žˆ๋‹ค.

1๏ธโƒฃ ๋ฌธ์„œ ๊ตฌ์กฐ(Document Structure) role

๋ฌธ์„œ ๊ตฌ์กฐ๋ฅผ ์„ค๋ช…ํ•˜๋Š” ์—ญํ• ์ด๋ฉฐ, ๋Œ€๋ถ€๋ถ„ ์‹œ๋งจํ‹ฑ HTML ์š”์†Œ๋กœ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ๊ถŒ์žฅํ•˜์ง€ ์•Š๋Š”๋‹ค. ๋‹ค๋งŒ presentation, toolbar, presentation ๋“ฑ ๊ธฐ๋ณธ ์‹œ๋ฉ˜ํ‹ฑ ํƒœ๊ทธ๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ ์‚ฌ์šฉ์ด ๊ฐ€๋Šฅํ•œ๋‹ค. HTML5์—์„œ ์˜๋ฏธ๋ฅผ ๊ฐ€์ง„ ์š”์†Œ๋“ค์ด ๋งŽ์•„์ง€๋ฉด์„œ, ๋Œ€๋ถ€๋ถ„์˜ ์—ญํ• ์€ ์‚ฌ์šฉ์„ ๊ถŒ์žฅํ•˜์ง€ ์•Š๋Š”๋‹ค.

โŒ ๊ถŒ์žฅํ•˜์ง€ ์•Š์Œ

role๋Œ€์‘ํ•˜๋Š” ์‹œ๋ฉ˜ํ‹ฑ ํƒœ๊ทธ์—ญํ• ์„ค๋ช… / ๋น„๊ณ 
role="article"<article>๋…๋ฆฝ์ ์ธ ์ฝ˜ํ…์ธ  ๋ธ”๋ก
role="cell"<td>ํ…Œ์ด๋ธ”์˜ ์…€
role="columnheader"<th scope="col">ํ…Œ์ด๋ธ”์˜ ์—ด ์ œ๋ชฉ
role="definition"<dfn>์šฉ์–ด๋‚˜ ๊ฐœ๋…์˜ ์ •์˜
role="figure"<figure>์ด๋ฏธ์ง€, ์ฐจํŠธ, ์ฝ”๋“œ ๋ธ”๋ก ๋“ฑ๊ณผ ํ•จ๊ป˜ ์‚ฌ์šฉ๋˜๋Š” ์„ค๋ช… ํฌํ•จ ๊ฐ€๋Šฅ
role="heading"<h1> ~ <h6>์ œ๋ชฉ์„ ๋‚˜ํƒ€๋ƒ„, ๊ณ„์ธต์  ๊ตฌ์กฐ ํ•„์š”
role="img"<img>, <picture>์ด๋ฏธ์ง€ ์ฝ˜ํ…์ธ 
role="list"<ul>, <ol>๋ชฉ๋ก์„ ๋‚˜ํƒ€๋ƒ„ (์ˆœ์„œ ์—†์Œ: <ul>, ์ˆœ์„œ ์žˆ์Œ: <ol>)
role="listitem"<li>๋ชฉ๋ก ํ•ญ๋ชฉ
role="meter"<meter>์ธก์ • ๊ฐ€๋Šฅํ•œ ๋ฒ”์œ„ ๋‚ด์˜ ๊ฐ’
role="row"<tr> with <table>ํ…Œ์ด๋ธ”์˜ ํ–‰
role="rowgroup"<thead>, <tfoot>, <tbody>ํ…Œ์ด๋ธ”์˜ ๊ทธ๋ฃนํ™”๋œ ํ–‰
role="separator"ํฌ์ปค์Šค ๋˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ์•„๋‹ˆ๋ผ๋ฉด <hr>์‹œ๊ฐ์ ์ธ ๊ตฌ๋ถ„์„ 
role="table"<table>๋ฐ์ดํ„ฐ ํ…Œ์ด๋ธ”, <thead>, <tbody> ํ™œ์šฉ ๊ฐ€๋Šฅ
role="term"<dfn>์ •์˜ ๋ชฉ๋ก์—์„œ์˜ ์šฉ์–ด (<dt>์™€ ํ•จ๊ป˜ ์‚ฌ์šฉ ๊ฐ€๋Šฅ)
role="application"-์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜, <div>, <section> ๋Œ€์ฒด ๊ฐ€๋Šฅ
role="directory"-HTML4์˜ <dir>๊ณผ ์œ ์‚ฌํ•˜๋‚˜ ์‚ฌ์šฉ๋˜์ง€ ์•Š์Œ, <ul> ๋Œ€์ฒด ๊ฐ€๋Šฅ
role="document"-๋ฌธ์„œ๋ฅผ ๋‚˜ํƒ€๋‚ด๋ฉฐ, <html> ํƒœ๊ทธ ์ž์ฒด๊ฐ€ ๋ฌธ์„œ๋ฅผ ์˜๋ฏธํ•จ
role="group"-์š”์†Œ ๊ทธ๋ฃน์„ ๋ฌถ์„ ๋•Œ ์‚ฌ์šฉ, <fieldset>, <div>, <ul> ๋Œ€์ฒด ๊ฐ€๋Šฅ

๐Ÿ›‘ ์™„์„ฑ๋„๋ฅผ ์œ„ํ•ด ์ œ์ž‘๋˜์—ˆ์œผ๋‚˜ ๊ฑฐ์˜ ์‚ฌ์šฉ๋˜์ง€ ์•Š์Œ

ํ•„์š”ํ•œ ๊ฒฝ์œ  ์œ ์šฉํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์œผ๋‚˜, ์ด ๋˜ํ•œ HTML ์‹œ๋ฉ˜ํƒ ํƒœ๊ทธ๋กœ ๋Œ€์ฒดํ•  ์ˆ˜ ์žˆ์Œ

role๋Œ€์‘ํ•˜๋Š” ์‹œ๋ฉ˜ํ‹ฑ ํƒœ๊ทธ์—ญํ• ์„ค๋ช… / ๋น„๊ณ 
role="associationlist"ย ์—ฐ๊ด€๋œ ํ•ญ๋ชฉ๋“ค์˜ ๋ฆฌ์ŠคํŠธ <dl> ๋Œ€์ฒด ๊ฐ€๋Šฅ
role="associationlistitemkey"ย ์—ฐ๊ด€๋œ ๋ฆฌ์ŠคํŠธ์—์„œ ํ•ญ๋ชฉ์˜ ํ‚ค <dt> ๋Œ€์ฒด ๊ฐ€๋Šฅ
role="associationlistitemvalue"ย ์—ฐ๊ด€๋œ ๋ฆฌ์ŠคํŠธ์—์„œ ํ•ญ๋ชฉ์˜ ๊ฐ’ <dd> ๋Œ€์ฒด ๊ฐ€๋Šฅ
role="blockquote"<blockquote>์ธ์šฉ๋ฌธ์„ ๋‚˜ํƒ€๋ƒ„
role="caption"<caption>ํ‘œ๋‚˜ ์ด๋ฏธ์ง€ ๋“ฑ์˜ ์บก์…˜(์„ค๋ช…)
role="code"<code>์ฝ”๋“œ ์กฐ๊ฐ์„ ๋‚˜ํƒ€๋ƒ„
role="deletion"<del>์‚ญ์ œ๋œ ํ…์ŠคํŠธ๋ฅผ ๋‚˜ํƒ€๋ƒ„
role="emphasis"<em>๊ฐ•์กฐ๋œ ํ…์ŠคํŠธ๋ฅผ ๋‚˜ํƒ€๋ƒ„
role="insertion"<ins>์‚ฝ์ž…๋œ ํ…์ŠคํŠธ๋ฅผ ๋‚˜ํƒ€๋ƒ„
role="paragraph"<p>๋‹จ๋ฝ์„ ๋‚˜ํƒ€๋ƒ„
role="strong"<strong>๊ฐ•ํ•œ ๊ฐ•์กฐ๋ฅผ ๋‚˜ํƒ€๋ƒ„
role="subscript"<sub>์•„๋ž˜์ฒจ์ž๋ฅผ ๋‚˜ํƒ€๋ƒ„
role="superscript"<sup>์œ„์ฒจ์ž๋ฅผ ๋‚˜ํƒ€๋ƒ„
role="time"<time>๋‚ ์งœ๋‚˜ ์‹œ๊ฐ„์„ ๋‚˜ํƒ€๋ƒ„

โญ• ์‹œ๋ฉ˜ํ‹ฑ ํƒœ๊ทธ๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์•„ ์‚ฌ์šฉ ๊ฐ€๋Šฅ

role์—ญํ•  ์„ค๋ช… / ๋น„๊ณ 
role="toolbar"๋„๊ตฌ ๋ชจ์Œ
role="tooltip"ํˆดํŒ
role="feed"์‚ฌ์šฉ์ž๊ฐ€ ์Šคํฌ๋กค ํ•  ์ˆ˜ ์žˆ๋Š” ๋ฌธ์„œ ๋ชฉ๋ก
role="math"์ˆ˜ํ•™ ์‹์„ ๋‚˜ํƒ€๋‚ผ ๋•Œ ์‚ฌ์šฉ
role="note"main ์ฝ˜ํ…์ธ ์— ๋ถ€์ˆ˜์ ์ด๊ฑฐ๋‚˜, ๋ถ€์ˆ˜์ ์ธ ๋‚ด์šฉ์„ ๋‹ด๊ณ ์žˆ๋Š” ์„น์…˜
role="presentation", role='none'๋ณด์กฐ ๊ธฐ์ˆ ์—์„œ ๋ฌด์‹œ ๋จ, ๊ธฐ๋Šฅ๊ณผ ์ƒ๊ด€์—†๋Š” ์žฅ์‹์  ์š”์†Œ

2๏ธโƒฃ ์œ„์ ฏ(Widget) role

์œ„์ ฏ ์—ญํ• ์„ ํ•˜๋Š” role์€ ์ผ๋ฐ˜์ ์ธ ์ƒํ˜ธ์ž‘์šฉ ํŒจํ„ด์„ ์ •์˜ํ•˜๋Š”๋ฐ ์‚ฌ์šฉํ•œ๋‹ค. 1๋ฒˆ์—์„œ ์‚ดํŽด๋ณธ ๋ฌธ์„œ ๊ตฌ์กฐ roler๊ณผ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ์‹œ๋ฉ˜ํ‹ฑ ํƒœ๊ทธ์™€ ๋™์ผํ•œ ์˜๋ฏธ๋ฅผ ๊ฐ€์ง„ ์š”์†Œ๋“ค์€ HTML ํƒœ๊ทธ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ๊ถŒ์žฅ๋œ๋‹ค.

๋ฌธ์„œ ๊ตฌ์กฐ role๊ณผ ๊ฐ€์žฅ ํฐ ์ฐจ์ด์ ์€ ์œ„์ ฏ role์˜ ๊ฒฝ์šฐ ์ƒํ˜ธ์ž‘์šฉ์„ ์œ„ํ•ด JS๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค.

๐Ÿงก ์œ„์ ฏ role ๋ชฉ๋ก

  • scrollbar
  • searchbox
  • separator
  • slider
  • spinbutton
  • switch
  • tab
  • tabpanel
  • treeitem

๐Ÿ’› ๋ณตํ•ฉ ์œ„์ ฏ ์—ญํ• 

  • combobox
  • menu
  • menubar
  • tablist
  • tree
  • treegrid

โŒ ๊ถŒ์žฅํ•˜์ง€ ์•Š์Œ

  • grid
  • listbox
  • radiogroup
  • button
  • checkbox
  • gridcell
  • link
  • menuitem
  • menuitemcheckbox
  • menuitemradio
  • option
  • progressbar
  • radio
  • textbox

3๏ธโƒฃ๋žœ๋“œ๋งˆํฌ(Landmark) role

๋žœ๋“œ๋งˆํฌ role์€ ์›น ํŽ˜์ด์ง€์˜ ๊ตฌ์กฐ์ ์ธ ์ •๋ณด๋ฅผ ์‹๋ณ„ํ•œ๋‹ค. ์Šคํฌ๋ฆฐ ๋ฆฌ๋”๋Š” ์ด Landmark role์„ ์‚ฌ์šฉํ•ด์„œ ํŽ˜์ด์ง€์˜ ์ค‘์š”ํ•œ ์„น์…˜์œผ๋กœ ํ‚ค๋ณด๋“œ ํƒ์ƒ‰์„ ์ œ๊ณตํ•˜๊ฒŒ ๋œ๋‹ค.

๋”ฐ๋ผ์„œ, Landmark role์ด ๋„ˆ๋ฌด ๋งŽ์œผ๋ฉด ์Šคํฌ๋ฆฐ ๋ฆฌ๋”์—์„œ ๋…ธ์ด์ฆˆ๊ฐ€ ๋ฐœ์ƒํ•ด ํŽ˜์ด์ง€์˜ ์ „์ฒด ๋ ˆ์ด์•„์›ƒ์„ ์ดํ•ดํ•˜๊ธฐ ์–ด๋ ต๊ธฐ ๋•Œ๋ฌธ์— ๋„ˆ๋ฌด ์ž์ฃผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋„๋ก ํ•œ๋‹ค.

๐Ÿ’› ์ฃผ์š” ๋žœ๋“œ๋งˆํฌ์˜ ์—ญํ• 

role๋Œ€์‘ํ•˜๋Š” ์‹œ๋ฉ˜ํ‹ฑ ํƒœ๊ทธ
role="banner"๋ฌธ์„œ <header>
role="complementary"<aside>
role="contentinfo"๋ฌธ์„œ <footer>
role="form"<form>
role="main"<main>
role="navigation"<nav>
role="region"<section>
role="search"<search>

4๏ธโƒฃ ๋ผ์ด๋ธŒ ๋ฆฌ์ „ (Live Region) role

Live Region ์—ญํ• ์€ ๋™์ ์ธ ์ฝ˜ํ…์ธ  ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ์Šคํฌ๋ฆฐ ๋ฆฌ๋”๊ฐ€ ์ธ์‹ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋•๋Š”๋‹ค.

role์„ค๋ช…
alert๊ธด๊ธ‰ ๊ฒฝ๊ณ  ๋ฉ”์„ธ์ง€
log์‹ค์‹œ๊ฐ„ ์ถ”๊ฐ€ ๋ฉ”์„ธ์ง€
marquee์ž๋™์œผ๋กœ ์›€์ง์ด๋Š” ์ฝ˜ํ…์ธ 
status์‹œ์Šคํ…œ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
timer์นด์šดํŠธ ๋‹ค์šด

๐ŸŒŸ์ถ”๊ฐ€ ์„ค๋ช… ์ฒ˜์Œ์— ์ด ๋ถ€๋ถ„์ด ํ—ท๊ฐˆ๋ ค์„œ role="alert"๊ฐ€ window.alert('๊ฒฝ๊ณ ')๊ฐ€ ๊ฐ™์€ ๊ฑด๊ฐ€? ํ•˜๊ณ  ์ƒ๊ฐํ–ˆ์—ˆ๋Š”๋ฐ, ์•„๋ž˜ ์˜ˆ์ œ๋ฅผ ๋ณด๋ฉด ์ดํ•ดํ•˜๊ธฐ ์‰ฝ๋‹ค.

1
2
3
<div role="log">์ž‘์—…์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.</div>
<div role="alert">์ž˜๋ชป ์ž…๋ ฅํ•˜์…จ์Šต๋‹ˆ๋‹ค!</div>
<div role="status">๋กœ๋“œ์ค‘์ž…๋‹ˆ๋‹ค...</div>

ํ•„์š”์— ๋”ฐ๋ผ ํ•ด๋‹น ๋‚ด์šฉ์„ ์ „๋‹ฌํ•ด์•ผ ํ•˜๋Š” element๋“ค์ด ์žˆ๋Š”๋ฐ ๊ทธ๊ฒƒ์„ ๊ตฌ์„ฑํ•  ๋•Œ ์ ์ ˆํžˆ ํ™œ์šฉํ•ด์ฃผ๋ฉด ๋œ๋‹ค.

5๏ธโƒฃ์œˆ๋„์šฐ (Window) role

window role์€ ํŒ์—…, ๋ชจ๋‹ฌ๊ณผ ๊ฐ™์ด ์ฃผ document ๋‚ด์˜ ํ•˜์œ„ window๋ฅผ ๋งํ•œ๋‹ค.

  • alertdialog
  • dialog

6๏ธโƒฃ์ถ”์ƒ์  (Abstract) role

abstract role์€ ๋ธŒ๋ผ์šฐ์ € ๋‚ด๋ถ€์ ์œผ๋กœ ๋ฌธ์„œ๋ฅผ ํ•ด์„ํ•  ๋Œ€๋งŒ ์‚ฌ์šฉํ•˜๊ณ , HTML์—์„œ ์ง์ ‘ ์‚ฌ์šฉํ•ด๋„ ํšจ๊ณผ๊ฐ€ ์—†๋‹ค. ์ฆ‰, ๊ฐœ๋ฐœ์ž๊ฐ€ ์ง์ ‘ ์ž‘์„ฑํ•  ์ผ์ด ์—†๋Š” role์ด๋‹ค.

โŒ ๊ฐœ๋ฐœ์ž๊ฐ€ ์‚ฌ์šฉํ•˜์ง€ ์•Š์Œ

  • command
  • composite
  • input
  • landmark
  • range
  • roletype
  • section
  • sectionhead
  • select
  • structure
  • widget
  • window

๐Ÿค์‹œ๋ฉ˜ํ‹ฑ ํƒœ๊ทธ

mdn ์—์„œ ์ œ๊ณตํ•˜๋Š” ํƒœ๊ทธ ๋ชฉ๋ก์„ ํ™•์ธํ•œ๋‹ค. ์•„๋ž˜๋Š” ๋‚ด๊ฐ€ ๊ธฐ์–ตํ•˜๊ณ  ์‹ถ์€ ํƒœ๊ทธ๋“ค์„ ๋ฝ‘์•„ ํ‘œ๋กœ ์ž‘์„ฑํ–ˆ๋‹ค.

โค๏ธ ๋ฌธ์„œ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ

๐Ÿ”ธ <base>

HTML ๋ฌธ์„œ์—์„œ ๊ธฐ๋ณธ URL์„ ์„ค์ •ํ•˜๋Š”๋ฐ ์‚ฌ์šฉ๋˜๋Š” ํƒœ๊ทธ๋กœ, ๋ฌธ์„œ์˜ ๋ชจ๋“  ์ƒ๋Œ€ URL์— ์‚ฌ์šฉํ•  ๊ธฐ๋ณธ url์„ ์ง€์ •ํ•œ๋‹ค.

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html lang="ko">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>base ํƒœ๊ทธ</title>
    <base href="https://www.example.com/" target="_blank" />
  </head>
  <body>
    <a href="page1.html">Page 1</a>
  </body>
</html>

์œ„์˜ ์˜ˆ์ œ์—์„œ base ํƒœ๊ทธ๋ฅผ ํ†ตํ•ด ๋ชจ๋“  ์ƒ๋Œ€ ๊ฒฝ๋กœ๋ฅผ https://www.example.com์œผ๋กœ ์„ค์ •ํ•ด, body ํƒœ๊ทธ ์•ˆ์— ์ž‘์„ฑ๋˜์–ด ์žˆ๋Š” page1.html์€ https://www.example.com/page1๋กœ ํ•ด์„๋œ๋‹ค.

๐Ÿงก ์ฝ˜ํ…์ธ  ์„น์…˜

๐Ÿ”ธ <address>

๊ฐ€๊นŒ์šด HTML ์š”์†Œ์˜ ์‚ฌ๋žŒ, ๋‹จ์ฒด, ์กฐ์ง ๋“ฑ์— ๋Œ€ํ•œ ์—ฐ๋ฝ์ฒ˜ ์ •๋ณด๋ฅผ ๋‚˜ํƒ€๋‚ธ๋‹ค.

1
2
3
4
5
6
7
8
9
10
11
<div>
  <h1>
    <code className="code">&lt;address&gt;</code>
  </h1>
  <div>
    <p>์ด๋ฉ”์ผ, ์ „ํ™”๋ฒˆํ˜ธ, URL, ์ฃผ์†Œ๋“ฑ ์—ฐ๋ฝ์ฒ˜ ์ •๋ณด๋ฅผ ๋‚˜ํƒ€๋‚ผ ์ˆ˜ ์žˆ์–ด์š”.</p>
    <address className="text-gray-400">
      <p>์ „ํ™”๋ฒˆํ˜ธ: <a href="tel:+010-1234-5678">+82 (010) 1234-5678</a></p>
    </address>
  </div>
</div>

๐Ÿ”ธ <aside>

๋ฌธ์„œ์˜ ์ฃผ์š” ๋‚ด์šฉ๊ณผ ๊ฐ„์ ‘์ ์œผ๋กœ๋งŒ ์—ฐ๊ด€๋œ ๋ถ€๋ถ„์„ ๋‚˜ํƒ€๋‚ด๋ฉฐ, ์ฃผ๋กœ ์‚ฌ์ด๋“œ๋ฐ” ํ˜น์€ ์ฝœ์•„์›ƒ ๋ฐ•์Šค์— ์‚ฌ์šฉํ•œ๋‹ค.

์ฃผ์ œ์„ค๋ช…
์•”์‹œ์  ARIA ์—ญํ• complementary
๊ฐ€๋Šฅํ•œ ARIA ์—ญํ• feed none note presentation region search
1
2
3
4
5
6
7
8
9
<div>
  <h1>
    <code className="code">&lt;aside&gt;</code>
  </h1>
  <aside>
    <p>โœจ์ฝœ์•„์›ƒ</p>
    <p>main ์ฝ˜ํ…์ธ ์™€ ์—ฐ๊ด€๋œ ์ฝœ์•„์›ƒ ๋‚ด์šฉ์ด ๋“ค์–ด๊ฐ‘๋‹ˆ๋‹ค.</p>
  </aside>
</div>

๐Ÿ’› ํ…์ŠคํŠธ ์ฝ˜ํ…์ธ 

๐Ÿ”ธ <dd> <dl> <dt>

  • <dl> (Definition List): ์„ค๋ช… ๋ชฉ๋ก์„ ๋‚˜ํƒ€๋‚ด๋Š” ์ปจํ…Œ์ด๋„ˆ ์—ญํ• ์„ ํ•œ๋‹ค.

  • <dt> (Definition Term): ์„ค๋ช…ํ•  ์šฉ์–ด(ํ•ญ๋ชฉ)๋ฅผ ์ •์˜ํ•œ๋‹ค.

  • <dd> (Definition Description): ํ•ด๋‹น ์šฉ์–ด์— ๋Œ€ํ•œ ์„ค๋ช…์„ ์ œ๊ณตํ•œ๋‹ค.

๐ŸŒŸ์ถ”๊ฐ€ ์„ค๋ช… ํŽ˜์ด์ง€์—์„œ ๋“ค์—ฌ์“ฐ๊ธฐ๋ฅผ ํ•˜๊ธฐ ์œ„ํ•œ ๋ชฉ์ ์œผ๋กœ <dl> (๋˜๋Š” <ul>) ์š”์†Œ๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ๋ง์ž! ์ž‘๋™ํ•  ์ˆ˜ ์žˆ์ง€๋งŒ, ์›๋ž˜ ๋ชฉ์ ์„ ํ๋ฆฌ๋ฉฐ ์ข‹์€ ๋ฐฉ๋ฒ•์ด ์•„๋‹ˆ๋‹ค.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<div>
  <section>
    <h2>HTML ์„ค๋ช… ๋ชฉ๋ก ํƒœ๊ทธ</h2>
    <ul>
      <li><code>&lt;dl&gt;</code>: ์„ค๋ช… ๋ชฉ๋ก(Definition List)</li>
      <li><code>&lt;dt&gt;</code>: ์„ค๋ช… ์šฉ์–ด(Definition Term)</li>
      <li><code>&lt;dd&gt;</code>: ์„ค๋ช… (Definition Description)</li>
    </ul>
  </section>

  <section>
    <h2>์ž์ฃผ ๋ฌป๋Š” ์งˆ๋ฌธ</h2>
    <dl>
      <dt>๋ฐฐ์†ก ๋ฌธ์˜</dt>
      <dd>์ฃผ๋ฌธ ํ›„ ํ‰๊ท  2~3์ผ ์ด๋‚ด์— ๋ฐฐ์†ก๋ฉ๋‹ˆ๋‹ค.</dd>

      <dt>๊ตํ™˜ ๋ฌธ์˜</dt>
      <dd>์ƒํ’ˆ ์ˆ˜๋ น ํ›„ 7์ผ ์ด๋‚ด์— ๊ตํ™˜์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.</dd>

      <dt>๋ฐ˜ํ’ˆ ๋ฌธ์˜</dt>
      <dd>๋ฐ˜ํ’ˆ ์‹ ์ฒญ์€ ๋ฐฐ์†ก ์™„๋ฃŒ ํ›„ 7์ผ ์ด๋‚ด์— ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.</dd>
    </dl>
  </section>
</div>

๐Ÿ”ธ <figure>, <figcaption>

  • <figure> : ์ด๋ฏธ์ง€, ๋‹ค์ด์–ด๊ทธ๋žจ, ์ฐจํŠธ ๋“ฑ๊ณผ ๊ฐ™์€ ๋…๋ฆฝ์ ์ธ ์ฝ˜ํ…์ธ ๋ฅผ ํ‘œํ˜„ํ•œ๋‹ค.

  • <figcaption> : ๋ถ€๋ชจ <figure>์— ํฌํ•จ๋œ ์ฝ˜ํ…์ธ ์— ๋Œ€ํ•œ ์„ค๋ช…์ด๋‚˜ ๋ฒ”๋ก€๋ฅผ ๋‚˜ํƒ€๋‚ธ๋‹ค. ๋”ฐ๋ผ์„œ, figcaption์€ figure์˜ ์ž์‹์ด์–ด์•ผ ํ•œ๋‹ค.

1
2
3
4
5
6
7
8
9
10
11
<div>
  <h1 className="flex gap-2">
    <code className="code">&lt;figure&gt;</code>
    <code className="code">&lt;figcaption&gt;</code>
  </h1>
  <figure>
    // chart.js๋กœ ๋งŒ๋“  ์ฐจํŠธ ์ปดํฌ๋„ŒํŠธ
    <ChartComponent />
    <figcaption>๊ฐ€์งœ ๋ฐ์ดํ„ฐ๋กœ ๋งŒ๋“  chart.js ๋ฐ” ์ฐจํŠธ</figcaption>
  </figure>
</div>

์Šคํฌ๋ฆฐ ๋ฆฌ๋”๋Š” figcaption์„ ์ฝ์–ด์ฃผ๊ธฐ ๋•Œ๋ฌธ์— ์‹œ๊ฐ์žฅ์• ์ธ์ด ํ•ด๋‹น ์ฐจํŠธ๊ฐ€ ๋ฌด์Šจ ์ฐจํŠธ์ธ์ง€ ์ดํ•ดํ•˜๊ธฐ ์‰ฌ์šฐ๋ฉฐ, SEO ์ตœ์ €๊ณ ํ•˜์—๋„ ๋„์›€์ด ๋œ๋‹ค.

๐ŸŒŸ์ถ”๊ฐ€ ์„ค๋ช… figcaption์€ ๋ Œ๋”๋ง ๋˜์–ด ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ณด์—ฌ์ง„๋‹ค. ๋งŒ์•ฝ ํ•ด๋‹น ๋‚ด์šฉ์„ ์Šคํฌ๋ฆฐ ๋ฆฌ๋”๋ฅผ ์œ„ํ•ด ์ž‘์„ฑํ•˜์˜€๋‹ค๋ฉด tailwind์˜ sr-only๋ฅผ ์‚ฌ์šฉํ•ด ์Šคํฌ๋ฆฐ ๋ฆฌ๋”์—๊ฒŒ๋งŒ ๋ณด์ด๊ณ  ๋‹ค๋ฅด์‚ฌ์šฉ์ž๋“ค์—๊ฒŒ๋Š” ์‹œ๊ฐ์ ์œผ๋กœ ์ˆจ๊ธธ ์ˆ˜ ์žˆ๋‹ค.

๐Ÿ’š ์ธ๋ผ์ธ ํ…์ŠคํŠธ ์‹œ๋ฉ˜ํ‹ฑ

๐Ÿ”ธ <abbr>

์ค„์ž„๋ง์„ ๋‚˜ํƒ€๋‚ผ ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ํƒœ๊ทธ๋กœ, ์Šคํฌ๋ฆฐ ๋ฆฌ๋”์—๊ฒŒ ์ถ”๊ฐ€ ์ •๋ณด๋ฅผ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ๋‹ค. ์„ ํƒ ์†์„ฑ์ธ title ์„ ์‚ฌ์šฉํ•ด ํ•ด๋‹น ์ค„์ž„๋ง์˜ ์˜๋ฏธ๋ฅผ ์ž‘์„ฑํ•˜๋ฉด, Hover ํ–ˆ์„ ๋•Œ ์ž‘์„ฑ ํ•ด ๋‘” ์˜๋ฏธ๊ฐ€ ํˆดํŒ์œผ๋กœ ํ‘œ์‹œ๋œ๋‹ค.

1
2
3
4
5
6
7
8
<div>
  <h1>
    <code className="code">&lt;abbr&gt;</code>
  </h1>
  <p>
    <abbr title="Cascading Style Sheets">CSS</abbr>๋Š” ์–ด๋–ค ๊ฒƒ์˜ ์ค„์ž„๋ง์ผ๊นŒ์š”?
  </p>
</div>

๐Ÿ”ธ <data>

๐Ÿ”ธ <dfn>

๐Ÿ”ธ <wbr>

๐Ÿงก ์–‘์‹

์‚ฌ์šฉ์ž๊ฐ€ ์ง์ ‘ ์ž…๋ ฅ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ์š”์†Œ๋“ค์ด๋‹ค. ์‚ฌ์šฉํ•˜๋Š” HTML์„ ์‚ดํŽด๋ณด๊ธฐ ์œ„ํ•ด ์ž‘์„ฑํ•œ ์˜ˆ์ œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

๐Ÿ”ธ <fieldset> & <legend>

๊ด€๋ จ๋œ ํผ ์š”์†Œ๋ฅผ ๊ทธ๋ฃน์œผ๋กœ ๋ฌถ๊ณ , ํ•ด๋‹น ๊ทธ๋ฃน์˜ ์ œ๋ชฉ์ด๋‚˜ ์„ค๋ช…์„ ์ž‘์„ฑํ•˜๋Š”๋ฐ ์‚ฌ์šฉํ•œ๋‹ค.

์Šคํฌ๋ฆฐ ๋ฆฌ๋”๋Š” ๋จผ์ € legend์— ์ ํžŒ ๊ทธ๋ฃน์˜ ์ œ๋ชฉ์„ ์ฝ์Œ์œผ๋กœ์„œ, ์‚ฌ์šฉ์ž๊ฐ€ ํ•ด๋‹น form ์š”์†Œ๊ฐ€ ๋ฌด์—‡์„ ์œ„ํ•œ ์š”์†Œ์ธ์ง€ ๋ฐ”๋กœ ์ธ์‹ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•œ๋‹ค.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<fieldset className="border border-blue-500 rounded-md p-4">
  <legend className="font-bold text-blue-500">ํŒ€์› ์ •๋ณด</legend>
  <label className="block mt-2">
    ์ง๋ฌด
    <select name="job" className="w-full p-2 border rounded">
      <optgroup label="๊ฐœ๋ฐœ์ž">
        <option value="์‹œ์Šคํ…œ ๊ฐœ๋ฐœ์ž">์‹œ์Šคํ…œ ๊ฐœ๋ฐœ์ž</option>
        <option value="ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ž">ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ž</option>
        <option value="๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ์ž">๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ์ž</option>
      </optgroup>
      <optgroup label="๊ธฐํš์ž">
        <option value="์ฝ˜ํ…์ธ  ๊ธฐํš์ž">์ฝ˜ํ…์ธ  ๊ธฐํš์ž</option>
        <option value="์„œ๋น„์Šค ๊ธฐํš์ž">์„œ๋น„์Šค ๊ธฐํš์ž</option>
      </optgroup>
      <optgroup label="๋””์ž์ด๋„ˆ">
        <option value="UX ๋””์ž์ด๋„ˆ">UX ๋””์ž์ด๋„ˆ</option>
        <option value="UI ๋””์ž์ด๋„ˆ">UI ๋””์ž์ด๋„ˆ</option>
      </optgroup>
      <option value="๊ธฐํƒ€">๊ธฐํƒ€</option>
    </select>
  </label>
</fieldset>

๐Ÿ”ธ <datalist>

์‚ฌ์šฉ์ž๊ฐ€ ์ง์ ‘ ์ž…๋ ฅํ•  ์ˆ˜ ์žˆ๋Š” input๊ณผ ์ž๋™ ์™„์„ฑ ๊ฐ€๋Šฅํ•œ list๋ฅผ ์ œ๊ณตํ•œ๋‹ค. select์™€ ๋‹ค๋ฅธ์ ์€ ์‚ฌ์šฉ์ž๊ฐ€ ๊ฐ’์„ ์ž…๋ ฅํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ์ ์œผ๋กœ, ๋ธŒ๋ผ์šฐ์ €๋Š” datalist์˜ option์„ dropdown์œผ๋กœ ๋ณด์—ฌ์ค€๋‹ค.

๐ŸŒŸ์ด๋•Œ datalist์™€ input์€ id์™€ list๋กœ ์—ฐ๊ฒฐ๋œ๋‹ค.

1
2
3
4
5
6
7
8
9
10
<label className="block mt-2">
  ์†Œ์† ํŒ€
  <input list="teamList" name="team" className="w-full p-2 border rounded" />
  <datalist id="teamList">
    <option value="๊ฐœ๋ฐœ์‹ค" />
    <option value="๊ธฐํš์‹ค" />
    <option value="๋””์ž์ธ์‹ค" />
    <option value="๊ธฐํƒ€" />
  </datalist>
</label>

โœจ์ฐธ๊ณ ์‚ฌํ•ญ NVDA์™€ Firefox๋“ฑ์˜ ์ผ๋ถ€ ๋ธŒ๋ผ์šฐ์ €๋Š” ์Šคํฌ๋ฆฐ ๋ฆฌ๋”์—๊ฒŒ ํ•ด๋‹น ํŒ์—… ๋‚ด์šฉ์„ ์•Œ๋ฆฌ์ง€ ์•Š๋Š”๋‹ค.

๐Ÿ’™ ๋Œ€ํ™”ํ˜• ์š”์†Œ

์ƒํ˜ธ์ž‘์šฉ ๊ฐ€๋Šฅํ•œ ์‚ฌ์šฉ์ž ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๋งŒ๋“ค๋•Œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

๐Ÿ”ธ <details> JS ์—†์ด๋„ ํ† ๊ธ€ ๊ฐ€๋Šฅํ•œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค. ์ด๋•Œ ๋ฐ˜๋“œ์‹œ <summary> ํƒœ๊ทธ๋ฅผ ์‚ฌ์šฉํ•ด ํด๋ฆญํ•  ์ œ๋ชฉ(๋ ˆ์ด๋ธ”)์„ ์ง€์ •ํ•ด์•ผ ํ•œ๋‹ค.

๊ธฐ๋ณธ์ ์œผ๋กœ, ์ดˆ๊ธฐ ์ƒํƒœ๋Š” ๋‹ซํžŒ ์ƒํƒœ๋‹ค.

1
2
3
4
<details>
  <summary>๋”๋ณด๊ธฐ</summary>
  <p>ํด๋ฆญํ•˜๋ฉด ์ด ๋‚ด์šฉ์ด ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค!</p>
</details>

๋งŒ์•ฝ ์ดˆ๊ธฐ ์ƒํƒœ์—์„œ ์—ด๋ฆฐ ์ƒํƒœ๋กœ ๋‘๊ณ  ์‹ถ๋‹ค๋ฉด open ์†์„ฑ์„ ์‚ฌ์šฉํ•œ๋‹ค.

1
2
3
4
<details open>
  <summary>๋”๋ณด๊ธฐ</summary>
  <p>์ฒ˜์Œ๋ถ€ํ„ฐ ์ด ๋‚ด์šฉ์ด ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค!</p>
</details>

๐Ÿ”ธ <summary> <details> ํƒœ๊ทธ์˜ ํด๋ฆญ ๊ฐ€๋Šฅํ•œ ์ œ๋ชฉ ์—ญํ• ๋กœ, <details> ์•ˆ์—์„œ ์ฒซ ๋ฒˆ์งธ ์š”์†Œ๋กœ ๋“ฑ์žฅํ•ด์•ผ ํ•œ๋‹ค. ์˜ˆ์‹œ๋Š” ์œ„์˜ <details>๋ฅผ ์ฐธ๊ณ ํ•˜์ž.

๐Ÿ”ธ <dialog> ๋ชจ๋‹ฌ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•œ๋‹ค. JS์˜ .showModal() ๊ณผ .show() ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์กฐ์ž‘ํ•  ์ˆ˜ ์žˆ๋‹ค. ๊ธฐ๋ณธ์ ์œผ๋กœ ํ™”๋ฉด์—๋Š” ์ˆจ๊ฒจ์ ธ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ํ•„์š”ํ•  ๋•Œ๋งŒ ํ‘œ์‹œ๋œ๋‹ค.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<dialog id="myDialog">
  <p>๋ชจ๋‹ฌ์ž…๋‹ˆ๋‹ค!</p>
  <button id="closeDialog">๋‹ซ๊ธฐ</button>
</dialog>

<button id="openDialog">๋ชจ๋‹ฌ ์—ด๊ธฐ</button>

<script>
  const dialog = document.getElementById("myDialog")
  document.getElementById("openDialog").addEventListener("click", () => {
    dialog.showModal() // ๋ชจ๋‹ฌ ์˜คํ”ˆ
  })
  document.getElementById("closeDialog").addEventListener("click", () => {
    dialog.close() // ๋ชจ๋‹ฌ ๋‹ซ๊ธฐ
  })
</script>

react๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
"use client"

import { useEffect, useRef, useState } from "react"
import { AnimatePresence, motion } from "framer-motion"

export default function Dialog() {
  const [isOpen, setIsOpen] = useState<boolean>(false)
  const dialogRef = useRef<HTMLDialogElement | null>(null)

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

    if (isOpen) {
      //showModal์€ ์ด๋ฏธ html์˜ ๋‚ด์žฅ ๋ฉ”์„œ๋“œ์ด๊ธฐ ๋•Œ๋ฌธ์— ๋”ฐ๋กœ function์„ ๋งŒ๋“ค ํ•„์š”๊ฐ€ ์—†์Œ
      dialog.showModal()
    } else {
      dialog.close()
    }
  }, [isOpen])

  const closeModal = () => setIsOpen(false)
  const openModal = () => setIsOpen(true)

  return (
    <>
      <button
        className="mt-4 p-2 bg-blue-500 text-white rounded-md"
        onClick={openModal}
      >
        ์—ด๊ธฐ
      </button>
      <AnimatePresence>
        {isOpen && (
          <motion.div
            className="fixed inset-0 bg-black/30 backdrop-blur-sm flex items-center justify-center"
            initial=
            animate=
            exit=
            onClick={closeModal}
          >
            <motion.dialog
              ref={dialogRef}
              className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-10 p-4 bg-white rounded-lg"
              onClick={(e) => e.stopPropagation()}
            >
              <p>์—ฌ๊ธฐ์— ๋‹ค์ด์–ผ๋กœ๊ทธ ๋‚ด์šฉ์ด ๋“ค์–ด๊ฐ‘๋‹ˆ๋‹ค.</p>
              <button
                onClick={closeModal}
                role="button"
                className="mt-4 p-2 bg-red-500 text-white rounded-md hover:bg-red-500/70 cursor-pointer"
              >
                ๋‹ซ๊ธฐ
              </button>
            </motion.dialog>
          </motion.div>
        )}
      </AnimatePresence>
    </>
  )
}

๐Ÿคtabindex

์ž๋™์œผ๋กœ ์‚ฌ์šฉ์ž๊ฐ€ ํ‚ค๋ณด๋„์˜ tabํ‚ค๋กœ ์ƒํ˜ธ์ž‘์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์š”์†Œ๊ฐ€ ๋ช‡๊ฐ€์ง€ ์žˆ๋‹ค.

  • <input>
  • <select>
  • <textarea>
  • <button>
  • <a>
  • <details>
  • <iframe>
  • <object>
  • <area>

๋ฐ˜๋ฉด์—, h1, div, span๋“ฑ์€ ์ž๋™์œผ๋กœ ์ƒํ˜ธ์ž‘์šฉํ•  ์ˆ˜ ์—†๋Š”๋ฐ, ๋งŒ์•ฝ ํ•ด๋‹น ์š”์†Œ๋“ค์— ์‚ฌ์šฉ์ž๊ฐ€ ์ ‘๊ทผํ•ด์•ผ ํ•œ๋‹ค๋ฉด tabindex๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

๋ฐ˜๋Œ€๋กœ ์ƒํ˜ธ์ž‘์šฉ์ด ๋˜๋Š” ์š”์†Œ์ง€๋งŒ, ์ƒํ˜ธ์ž‘์šฉ์„ ๋ง‰๊ณ  ์‹ถ๋‹ค๋ฉด, tabindex="-1"์„ ์ž‘์„ฑํ•˜๋ฉด ๋œ๋‹ค.

๋˜ํ•œ, ์‚ฌ์šฉ์ž๊ฐ€ ์ƒํ˜ธ์ž‘์šฉํ•˜๋Š” ์ˆœ์„œ๋ฅผ ์กฐ์ •ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ๋งŒ์•ฝ tabindex์— ์–‘์ˆ˜๋ฅผ ์ฃผ์–ด tab ์ˆœ์„œ๋ฅผ ์กฐ์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.

๐ŸคClick๊ณผ Key event

<div> onClick={fn} />๊ณผ ๊ฐ™์€ ์ด๋ฒคํŠธ๋ฅผ ์ฃผ์—ˆ์„ ๋•Œ, ์ž์œ ๋กญ๊ฒŒ ๋งˆ์šฐ์Šค๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๋Š” ์‚ฌ๋žŒ๋“ค์€ ์ด ์ด๋ฒคํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์–ด๋ ต๊ธฐ ๋•Œ๋ฌธ์—, onKeyDown, onKeyUp, onKeyPress๋“ฑ์˜ ์ด๋ฒคํŠธ๋ฅผ ํ•จ๊ป˜ ์ž‘์„ฑํ•˜๋„๋ก ํ•œ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด ์•„๋ž˜์™€ ๊ฐ™์ด ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const AccessibleDiv = () => {
  const handleClick = () => {
    alert("divํƒœ๊ทธ์˜ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜์…จ์Šต๋‹ˆ๋‹ค!")
  }

  const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
    if (event.key === "Enter" || event.key === " ") {
      event.preventDefault()
      handleClick()
    }
  }

  return (
    <div
      tabIndex={0}
      role="button"
      onClick={handleClick}
      onKeyDown={handleKeyDown}
      className="w-full bg-green-500 px-2 text-white center-flex text-center break-keep py-2 rounded-lg cursor-pointer"
    >
      ๋งˆ์šฐ์Šค๋กœ ํด๋ฆญํ•˜๊ฑฐ๋‚˜, ํ‚ค๋ณด๋“œ๋กœ ์ƒํ˜ธ์ž‘์šฉ ํ•ด๋ณด์„ธ์š”!
    </div>
  )
}

์œ„์˜ ์˜ˆ์ œ๋Š” ์ผ๋ถ€๋Ÿฌ div๋ฅผ ์‚ฌ์šฉํ–ˆ๋Š”๋ฐ, ํ•ด๋‹น ์˜ˆ์ œ์—์„œ๋Š” ์•„๋ž˜์™€ ๊ฐ™์€ ์ ‘๊ทผ์„ฑ์„ ์œ„ํ•œ ์š”์†Œ๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ์—ˆ๋‹ค. โœ”๏ธ tabIndex={0} โ†’ ํ‚ค๋ณด๋“œ ํฌ์ปค์Šค๊ฐ€ ๊ฐ€๋Šฅํ•˜๋„๋ก ํ•จ โœ”๏ธ role="button" โ†’ ์ด ์š”์†Œ๊ฐ€ ๋ฒ„ํŠผ ์—ญํ• ์„ ํ•œ๋‹ค๋Š” ๊ฒƒ์„ ์ •ํ™•ํžˆ ์ „๋‹ฌ โœ”๏ธ onKeyDown โ†’ Enter ๋ฐ Space ํ‚ค ์ž…๋ ฅ์„ ๊ฐ์ง€

โœจ์ฐธ๊ณ ์‚ฌํ•ญ ๋งŒ์•ฝ tabIndex๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค๋ฉด ํ‚ค๋ณด๋“œ๋กœ Div๋ฅผ focus ํ•  ์ˆ˜ ์—†๋‹ค๋Š” ๊ฒƒ์— ์ฃผ์˜ํ•˜์ž!

๋‹ค๋งŒ ๊ฐ€๋Šฅํ•œ buttonํƒœ๊ทธ์™€ ๊ฐ™์€ ์‹œ๋ฉ˜ํ‹ฑ ํƒœ๊ทธ๋ฅผ ์“ฐ๋Š” ๊ฒƒ์ด ์ข‹๋‹ค.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const AccessibleButton = () => {
  const handleClick = () => {
    alert("๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜์…จ์Šต๋‹ˆ๋‹ค!")
  }

  const handleKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>) => {
    if (event.key === "Enter" || event.key === " ") {
      event.preventDefault()
      handleClick()
    }
  }

  return (
    <button
      onClick={handleClick}
      onKeyDown={handleKeyDown}
      className="w-full bg-blue-500 text-white center-flex text-center break-keep py-2 rounded-lg cursor-pointer"
    >
      ๋งˆ์šฐ์Šค๋กœ ํด๋ฆญํ•˜๊ฑฐ๋‚˜, ํ‚ค๋ณด๋“œ๋กœ ์ƒํ˜ธ์ž‘์šฉ ํ•ด๋ณด์„ธ์š”!
    </button>
  )
}

๐ŸคHover์™€ Key event

Click ์ฒ˜๋Ÿผ Hover ๋˜ํ•œ ๋งˆ์šฐ์Šค๋ฅผ ์ด์šฉํ•ด ๋ฐœ์ƒ์‹œํ‚ค๋Š” ์ด๋ฒคํŠธ์ด๊ธฐ ๋•Œ๋ฌธ์—, mouseOver, mouseOut๋˜ํ•œ ํ‚ค๋ณด๋“œ๋กœ๋„ ํ•ด๋‹น ์ด๋ฒคํŠธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ฌ ์ˆ˜ ์žˆ๋„๋ก ํ•ด์•ผ ํ•œ๋‹ค.

๊ฐ๊ฐ์˜ ์ด๋ฒคํŠธ ๋Œ€์‘์€ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ํ•  ์ˆ˜ ์žˆ๋‹ค.

๐Ÿ”ธonMouseOver โ†”๏ธ onFocus ๐Ÿ”ธonMouseOut โ†”๏ธ onBlur

์ด ์—์ œ์—์„œ๋Š” hover ํ–ˆ์„ ๋•Œ div์˜ color๊ฐ€ ๋ณ€๊ฒฝ๋˜๋„๋ก ํ–ˆ๋‹ค. ์ด๋•Œ Mouse์ด๋ฒคํŠธ์™€ Key ์ด๋ฒคํŠธ๋ฅผ ๋Œ€์‘ํ•˜์—ฌ, key๋กœ ์ ‘๊ทผํ•˜๋Š” ๊ฒฝ์šฐ ํ•ด๋‹น ์š”์†Œ๊ฐ€ focus ๋˜์—ˆ์„ ๋•Œ color๋ฅผ ๋ณ€๊ฒฝํ•ด ์ฃผ์—ˆ๋‹ค.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const AccessibleHoverDiv = () => {
  const [isHovered, setIsHovered] = useState<boolean>(false)

  return (
    <div
      onMouseOver={() => setIsHovered(true)}
      onMouseOut={() => setIsHovered(false)}
      onFocus={() => setIsHovered(true)}
      onBlur={() => setIsHovered(false)}
      tabIndex={0}
      role="button"
      className={`w-full px-2 text-white text-center break-keep py-2 rounded-lg cursor-pointer ${
        isHovered ? "bg-yellow-500" : "bg-red-500"
      }`}
    >
      ๋งˆ์šฐ์Šค๋กœ ํ˜ธ๋ฒ„ํ•˜๊ฑฐ๋‚˜, ํ‚ค๋ณด๋“œ๋กœ ์ƒํ˜ธ์ž‘์šฉ ํ•ด๋ณด์„ธ์š”!
    </div>
  )
}

๐ŸคFocus Trap

์ ‘๊ทผ์„ฑ์„ ์œ„ํ•ด ๋˜ ์‹œ๋„ํ•ด๋ณผ ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์€ ๋ฌด์—‡์ธ๊ฐ€ ๊ฒ€์ƒ‰ํ•˜๋˜ ์ค‘ ๋ฐœ๊ฒฌํ•œ Focus trap! Focus trap์ด๋ž€, ๋ชจ๋‹ฌ, ํŒ์—…์„ ์‚ฌ์šฉํ•  ๋•Œ ํ•ด๋‹น ์š”์†Œ ๋‚ด์—์„œ ํฌ์ปค์Šค๋ฅผ ๊ฐ€๋‘์–ด(trap) ์‚ฌ์šฉ์ž์˜ focus๊ฐ€ ๊ทธ ์š”์†Œ ๋ฐ–์œผ๋กœ ์ด๋™ํ•˜์ง€ ์•Š๋„๋ก ํ•ด์ฃผ๋Š” ๊ฒƒ์„ ๋งํ•œ๋‹ค.

๋งŒ์•ฝ ์‚ฌ์šฉ์ž๊ฐ€ ๋ชจ๋‹ฌ์„ ์—ด๊ณ  ํƒ์ƒ‰ํ•  ๋•Œ, ํฌ์ปค์Šค๊ฐ€ ๋ชจ๋‹ฌ์˜ ๊ฒฝ๊ณ„๋ฅผ ๋„˜์–ด ๋‚˜๊ฐ€๊ฒŒ ๋˜๋ฉด ์‚ฌ์šฉ์ž๋Š” ๋ชจ๋‹ฌ ๋‚ด์—์„œ ํŠน์ • ์•ก์…˜์„ ์ทจํ•˜๊ณ  ์‹ถ์–ด๋„, ํ˜„์žฌ ํฌ์ปค์Šค๋œ ์š”์†Œ๋ฅผ ์ฐพ์ง€ ๋ชปํ•˜๊ฒŒ ๋˜์–ด ์›ํ•˜๋Š” ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๊ธฐ ์–ด๋ ค์›Œ์งˆ ์ˆ˜ ์žˆ๋‹ค. ์ด๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด Focus trap๊ณผ ๊ฐ™์€ ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด๋‹ค.

๐Ÿ”‘๊ตฌํ˜„ ํ๋ฆ„

  1. ๋จผ์ € Focus ๊ฐ€๋Šฅํ•œ ์š”์†Œ๋“ค์„ ์ฐพ๋Š”๋‹ค.
  2. Focus ๊ฐ€๋Šฅํ•œ ์š”์†Œ๋“ค ์ค‘ First(์ฒซ๋ฒˆ์งธ ์š”์†Œ)์™€ Last(๋งˆ์ง€๋ง‰ ์š”์†Œ)๋ฅผ ์ฐพ๋Š”๋‹ค.
  3. Modal์ด ์—ด๋ฆฌ๋ฉด ๊ฐ€์žฅ ๋จผ์ € First๋ฅผ Focus ํ•ด์ค€๋‹ค.
  4. ๋งŒ์•ฝ Last์— Focusํ•œ ๊ฒฝ์šฐ, ์‚ฌ์šฉ์ž๊ฐ€ tab์„ ํ•˜๋ฉด ๊ฐ•์ œ๋กœ First๋ฅผ Focusํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค€๋‹ค.

๐Ÿ’ป์˜ˆ์ œ

๊ตฌํ˜„์€ ์–ด๋ ต์ง€ ์•Š๋‹ค! ์ดํ•ดํ•˜๊ธฐ ์ข‹๊ฒŒ ๋ฏธ๋ฆฌ ๋ช‡๊ฐ€์ง€๋ฅผ ์ •๋ฆฌํ•ด๋ณด๋ฉด, ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

  • activeElement์€ document์—๋Ÿฌ focus๋˜์–ด ํ™œ์„ฑํ™” ๋œ ๊ฐœ์ฒด๋ฅผ ๋งํ•œ๋‹ค.
  • div๋ฅผ ์ด์šฉํ•ด modal(dialog)๋ฅผ ๋งŒ๋“ค์–ด ์ฃผ์—ˆ๊ธฐ ๋•Œ๋ฌธ์— role์€ dialog๋ผ๊ณ  ๋ช…์‹œํ–ˆ๋‹ค.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
const FocusTrap = () => {
  const [isOpen, setIsOpen] = useState<boolean>(false)
  const modalRef = useRef<HTMLDivElement | null>(null)

  // focus ๊ฐ€๋Šฅํ•œ ์š”์†Œ๋“ค
  const focusableElements = `button, [href], input, select, textarea, iframe, [tabindex]:not([tabindex="-1"])`

  // ๋ชจ๋‹ฌ ์—ด๊ณ  ๋‹ซ๊ธฐ trigger
  const openModal = () => setIsOpen(true)
  const closeModal = () => setIsOpen(false)

  // Mouse๋กœ ๋ชจ๋‹ฌ ๋‹ซ๊ธฐ
  const handleClick = () => {
    openModal()
  }

  // KeyDown์œผ๋กœ ๋ชจ๋‹ฌ ์—ด๊ณ  ๋‹ซ๊ธฐ
  const handleKeyDown = (event: React.KeyboardEvent) => {
    if (event.key === "Enter" || event.key === " ") {
      event.preventDefault()
      openModal()
    } else if (event.key === "Escape") {
      closeModal()
    }
  }

  // Focus Trap
  useEffect(() => {
    if (!isOpen || !modalRef.current) return

    const focusableContent =
      modalRef.current.querySelectorAll(focusableElements)
    if (!focusableContent.length) return

    const firstElement = focusableContent[0] as HTMLElement
    const lastElement = focusableContent[
      focusableContent.length - 1
    ] as HTMLElement

    const handleTabKey = (event: KeyboardEvent) => {
      if (event.key !== "Tab") return

      if (event.shiftKey) {
        // Shift + Tab์€ ๊ฑฐ๊พธ๋กœ ์ด๋™ํ•จ
        // Shift + Tab: ์ฒซ ๋ฒˆ์งธ ์š”์†Œ์—์„œ ๋งˆ์ง€๋ง‰ ์š”์†Œ๋กœ ์ด๋™
        if (document.activeElement === firstElement) {
          event.preventDefault()
          lastElement.focus()
        }
      } else {
        // Tab: ๋งˆ์ง€๋ง‰ ์š”์†Œ์—์„œ ์ฒซ ๋ฒˆ์งธ ์š”์†Œ๋กœ ์ด๋™
        if (document.activeElement === lastElement) {
          event.preventDefault()
          firstElement.focus()
        }
      }
    }

    document.addEventListener("keydown", handleTabKey)

    return () => {
      document.removeEventListener("keydown", handleTabKey)
    }
  }, [isOpen])

  // ๋ชจ๋‹ฌ์ด ์—ด๋ฆฌ๋ฉด ํฌ์ปค์Šค ๊ฐ€๋Šฅํ•œ ์ฒซ ๋ฒˆ์งธ ์š”์†Œ์— ์ž๋™์œผ๋กœ ํฌ์ปค์Šค
  useEffect(() => {
    if (isOpen && modalRef.current) {
      const focusableContent =
        modalRef.current.querySelectorAll(focusableElements)
      const firstElement = focusableContent[0] as HTMLElement

      firstElement?.focus()
    }
  }, [isOpen])

  return (
    <>
      <div
        onClick={handleClick}
        onKeyDown={handleKeyDown}
        tabIndex={0}
        role="button"
        className="w-full bg-purple-500 px-2 text-white center-flex text-center break-keep py-2 rounded-lg cursor-pointer"
      >
        ๋งˆ์šฐ์Šค๋กœ ํด๋ฆญํ•˜๊ฑฐ๋‚˜, ํ‚ค๋ณด๋“œ๋กœ ์ƒํ˜ธ์ž‘์šฉ ํ•ด์„œ ๋ชจ๋‹ฌ์„ ์—ด์–ด๋ณด์„ธ์š”!
      </div>
      <AnimatePresence>
        {isOpen && (
          <motion.div
            aria-hidden="true" // ๋‹จ์ˆœํžˆ ์‹œ๊ฐ์  ๋””์ž์ธ์„ ์œ„ํ•œ ๊ฒƒ์ด๋ฏ€๋กœ ์Šคํฌ๋ฆฐ ๋ฆฌ๋”๊ฐ€ ์ฝ์ง€ ์•Š๋„๋ก ํ•จ
            className="fixed inset-0 bg-black/30 backdrop-blur-sm flex items-center justify-center"
            initial=
            animate=
            exit=
            onClick={closeModal}
          >
            <motion.div
              role="dialog" // div์ด๊ธฐ ๋•Œ๋ฌธ์— role๋กœ ์—ญํ•  ๋ช…์‹œ
              aria-modal="true"
              aria-labelledby="dialog_label"
              aria-describedby="dialog_desc"
              ref={modalRef}
              className="absolute top-1/2 left-1/2 w-[300px] -translate-x-1/2 -translate-y-1/2 z-10 p-4 bg-white rounded-lg"
              onClick={(e) => e.stopPropagation()}
            >
              <h2
                id="dialog_label"
                className="text-lg text-center font-bold mb-1	"
              >
                a11y
              </h2>
              <p id="dialog_desc" className="break-keep text-center">
                a11y ์‹ค์Šต์ค‘! ์™„๋ฃŒ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ๋ชจ๋‹ฌ์ด close ๋ฉ๋‹ˆ๋‹ค.๐Ÿ˜Š
              </p>
              <div className="flex gap-2">
                <button
                  role="button"
                  className="mt-4 p-2 w-1/2 bg-white focus:ring-4 focus:ring-yellow-400 text-black border-gray-200 border rounded-md hover:bg-gray-100 cursor-pointer"
                >
                  ์ทจ์†Œ
                </button>
                <button
                  onClick={closeModal}
                  role="button"
                  className="mt-4 p-2 w-1/2 bg-black focus:ring-4 focus:ring-yellow-400 text-white rounded-md hover:bg-black/60 cursor-pointer"
                >
                  ์™„๋ฃŒ
                </button>
              </div>
            </motion.div>
          </motion.div>
        )}
      </AnimatePresence>
    </>
  )
}

์ฐธ๊ณ ๋กœ, ๋‚˜๋Š” ์™„๋ฃŒ ๋ฒ„ํŠผ์„ black์„ ๋กœ ์ง€์ •ํ•ด๋‘์—ˆ๋Š”๋ฐ, outline์˜ ๊ธฐ๋ณธ ์ƒ‰์ƒ์ด black์ด๋ผ focus๋˜์–ด๋„ ๋ˆˆ์— ์ž˜ ๋„์ง€ ์•Š์•˜๋‹ค. ์ฒ˜์Œ์—” outline ์ž์ฒด ์ปฌ๋Ÿฌ๋ฅผ ๋ณ€๊ฒฝํ• ๊นŒ ํ–ˆ๋Š”๋ฐ, tailwind์—์„œ ์ œ๊ณตํ•˜๋Š” ring๊ธฐ๋Šฅ์„ ์ด์šฉํ•ด focus ๋˜์—ˆ์„ ๋•Œ ํ•ด๋‹น ์š”์†Œ๊ฐ€ bg-black ์ด์–ด๋„ focus ๋˜์—ˆ์Œ์„ ์‚ฌ์šฉ์ž๊ฐ€ ์‰ฝ๊ฒŒ ์ธ์‹ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ–ˆ๋‹ค.

โœจ์ฐธ๊ณ ์‚ฌํ•ญ ์œ„์—์„œ ์‚ดํŽด๋ณธ <dialog>๋Š” ์ž๋™์œผ๋กœ ESC๋กœ ๋ชจ๋‹ฌ Close์™€ Focus trap์„ ์ง€์›ํ•œ๋‹ค!

๐Ÿ—‚๏ธ์ฐธ๊ณ  ์‚ฌ์ดํŠธ

This post is licensed under CC BY 4.0 by the author.