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-haspopup | elemeent๊ฐ ํ์ (๋ฉ๋ด, ๋ฆฌ์คํธ๋ฐ์ค, ํธ๋ฆฌ, ๊ทธ๋ฆฌ๋, ๋ค์ด์ผ๋ก๊ทธ) ๋ฅผ ํธ๋ฆฌ๊ฑฐํ ์ ์์ |
aria-autocomplete | ์ ๋ ฅํ๋์ ์๋์์ฑ ๊ธฐ๋ฅ์ด ์๋ค๊ณ ์๋ ค ์ค (๊ธฐ๋ฅ์ ๊ฐ๋ฐ์๊ฐ ๊ตฌํํด์ผ ํจ) |
aria-owns | DOM์ ๋ถ๋ชจ-์์ ๊ด๊ณ๊ฐ ์๋๋, ์๊ฐ์ ์ผ๋ก ๋ถ๋ชจ-์์ ๊ด๊ณ์ผ ๋ |
aria-activedescendant | ํ์ฌ ํ์ฑํ๋ ํ์ elemeent๋ฅผ ์๋ณ |
aria-controls | ํด๋น element๊ฐ ๋ค๋ฅธ element์ ๋ด์ฉ์ ์ ์ดํ๊ณ ์๋ค๋ ๊ฒ์ ๋ช ์ํจ |
aria-selected | ํด๋น element๊ฐ ์ ํ ๋์์์ ๋ํ๋ธ๋ค. |
aria-label | element์ ์ด๋ฆ์ ์ ์. label์ด ์๊ฑฐ๋ ์์ด์ฝ๋ง ์์ ๊ฒฝ์ฐ ์ ์ฉํจ |
aria-labelledby | element์ ์ ๊ทผ์ฑ์ ์ํ ์ด๋ฆ์ ์ ์ํ ๋ ๋ค๋ฅธ ์์๋ฅผ ์ฐธ์กฐํ๋๋ก ์ค์ ํจ |
aria-expanded | ์์๊ฐ ํ์ฅ๋์ด ๋ณด์ด๋์ง, ์ถ์๋์ด ๋ณด์ด์ง ์๋์ง๋ฅผ ๋ํ๋ |
aria-current | ํ์ฌ ํ์ฑํ๋ ํญ๋ชฉ์ ๋ํ๋ |
aria-orientation | ์์์ ๋ฐฉํฅ ๋ช ํํ ์ง์ (ํค๋ณด๋๋ฅผ ์ด๋ป๊ฒ ์ฌ์ฉํด์ผ ํ ์ง ์๋ ค์ค) |
aria-pressed | ํ ๊ธ ๋ฒํผ์ ํ์ฌ ๋๋ ค์๋ ์ํ ๋ํ๋ |
aria-checked | checkbox, 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
ํ์ฌ ํ์ฑํ ๋ ํญ๋ชฉ์ ๋ํ๋ด๋ฉฐ, ๋ฉ๋ด, ํญ, ๋ค๋น๊ฒ์ด์ ๋ฑ์์ ๋ค์ํ๊ฒ ํ์ฉํ ์ ์๋ค. ์ฌ์ฉํ ์ ์๋ ๊ฐ์ ์ข ๋ฅ๊ฐ ๋ง์๋ฐ ํ๋ก ์ดํด๋ณด์.
๊ฐ | ์ค๋ช |
---|---|
page | navigation์์ ํ์ฌ ํ์ด์ง |
step | ํ๋ก์ธ์ค ์งํ ์ค ํ์ฌ step |
location | breadcrumb์์ ์ฌ์ฉ์์ ํ์ฌ ์์น |
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-label | aria์ ์ง์ ๋ฌธ์์ด ์์ฑ |
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"><address></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"><aside></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><dl></code>: ์ค๋ช
๋ชฉ๋ก(Definition List)</li>
<li><code><dt></code>: ์ค๋ช
์ฉ์ด(Definition Term)</li>
<li><code><dd></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"><figure></code>
<code className="code"><figcaption></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"><abbr></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๊ณผ ๊ฐ์ ๊ธฐ๋ฅ์ ์ฌ์ฉํ๋ ๊ฒ์ด๋ค.
๐๊ตฌํ ํ๋ฆ
- ๋จผ์ Focus ๊ฐ๋ฅํ ์์๋ค์ ์ฐพ๋๋ค.
- Focus ๊ฐ๋ฅํ ์์๋ค ์ค First(์ฒซ๋ฒ์งธ ์์)์ Last(๋ง์ง๋ง ์์)๋ฅผ ์ฐพ๋๋ค.
- Modal์ด ์ด๋ฆฌ๋ฉด ๊ฐ์ฅ ๋จผ์ First๋ฅผ Focus ํด์ค๋ค.
- ๋ง์ฝ 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
์ ์ง์ํ๋ค!
๐๏ธ์ฐธ๊ณ ์ฌ์ดํธ
- https://ko.legacy.reactjs.org/docs/accessibility.html
- https://developer.mozilla.org/ko/docs/Web/HTML/Element
- https://codingeverybody.kr/html-role-%EC%86%8D%EC%84%B1%EC%9D%98-%ED%99%9C%EC%9A%A9-%EB%B0%A9%EB%B2%95/
- https://stackoverflow.com/questions/75189187/semantic-tags-vs-aria-role-why-one-or-the-other
- https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles
- https://medium.com/cstech/achieving-focus-trapping-in-a-react-modal-component-3f28f596f35b
- https://zachpatrick.com/blog/how-to-trap-focus-inside-modal-to-make-it-ada-compliant
- https://nuli.navercorp.com/tool/waiAria