์ ๊ทผ์ฑ์ด ์ด๋๋ ์น ํ๋ก ํธ์๋ ํ ์คํธ ์๋ํ
- #Test
- #Accessibility
- #React
- #NextJS
โThe more your tests resemble the way your software is used, the more confidence they can give you.โ
โํ ์คํธ๊ฐ ์ํํธ์จ์ด๋ฅผ ์ฌ์ฉํ๋ ๋ฐฉ์๊ณผ ๋น์ทํ ์๋ก ๋ ๋ง์ ํ์ ์ ์ป์ ์ ์์ ๊ฒ๋๋คโ
- Kent C. Dodds ํ ์คํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๊ฐ๋ฐ์
์๋ ํ์ธ์. ์๋์์์ ํ๋ก ํธ์๋ ๊ฐ๋ฐ์ ํ๋ฉด์ ํ ์คํธ์ ์ ์์ผ์ ์ ๋ํ๋ ๊นํํฌ์ ๋๋ค.
์ ์ ํ ์คํธ์์ (๋ ์, ๋ ์์ฃผ) ๋ฐฐ์ฐ๊ธฐ๋ฅผ ์ฐ๋ฉด์๋ ์ธ๊ธํ์ง๋ง. ์๋, ์๋ ํ ์คํธ๋ ๊ฐ๋ฐ์๊ฐ ๋์น๊ณ ์๋ ๋ถ๋ถ์ ๊นจ๋ซ๊ฒ ๋์์ฃผ๋ ํ์ต ๋๊ตฌ์ ๋๋ค. ํ ์คํธํ ์๊ฐ์ด ์๋ค๊ณ ๋งํ๋ ๊ฒฝ์ฐ๊ฐ ๋ง์ง๋ง, ๋ค๋ฆ๊ฒ ๋ฌธ์ ๋ฅผ ๋ฐ๊ฒฌํ ์๋ก ๋น์ฉ์ ์ปค์ง๊ณ ์ถ์๋ ์คํ๋ ค ๋ฆ์ด์ง๋๋ค. ์ผ์ ์ ์ ํํ๊ฒ ์์ธกํ๊ธฐ ์ด๋ ค์์ง๊ณ , ์คํ๋ฆฐํธ ๋ง์ง๋ง์ ์ผ๊ทผํ๊ฒ ๋ ๊ฐ๋ฅ์ฑ์ ๋์์ง์ฃ .
์ ๊ฐ ํ์ฌ์ ๋ค์ด์์ ํ ์คํธ๋ฅผ ์ ๋?ํ๊ธฐ ์์ํ์ง ์ธ ๋ฌ์ด ์ง๋ฌ์ต๋๋ค. ์๋ 1๊ฐ ๋ฐ์ ์๋ ์๋ํ ํ ์คํธ๋ ์ด์ unit + component test 98๊ฐ, e2e ํ ์คํธ๋ 35๊ฐ๋ก ๋์ด๋ฌ์ต๋๋ค. ํ์์ ํ ์คํธ๋ ์์๋ก ์ค์ํด์ ์ ๊ธฐ๋ฅ์ ๋ฌผ๋ก ์ด๊ณ , ๊ธฐ์กด ๊ธฐ๋ฅ์์๋ ๋ค์ํ ์๋ฌ๋ฅผ ๋ฐ๊ฒฌํ๊ณ ์์ฃ .
์ด๋ฒ์๋ ์ ๊ทผ์ฑ์ ํ ๋๋ก ์น UI ํ ์คํธ ์๋ํ ์ด๋ป๊ฒ ๋ฐ์ ํ๋์ง, ๊ทธ๋ฆฌ๊ณ WAI-ARIA ํ์ค์ role๊ณผ aria ์์ฑ๋ค์ ์ด์ฉํด ํ ์คํธ๋ฅผ ์์ฑํ๋ ๋ฐฉ๋ฒ์ ์๊ฐํด๋ณด๋ ค ํฉ๋๋ค.
UI ํ ์คํธ๋ ์๋ํ ํ ์ ์๋๊ฐ?
- โ์ต์ ์ ์์คํ ํ ์คํธโ ์บก์ฒ & ๋ฆฌํ๋ ์ด ์๋ํ๋ ์ฌ์ฉ์์ ์กฐ์์ ๊ธฐ๋กํ๊ณ , ๊ทธ๊ฒ์ ์ฌ์ํ๋ GUI ์์์์ ์๋ํ ํ ์คํธ๋ฅผ ๋งํฉ๋๋ค. โฆ ํ์ง๋ง ์บก์ฒ & ๋ฆฌํ๋ ์ด ์๋ํ๋ ์๋นํ ์ด์ ํ ๋ฐฉ์์ ์ํฉ๋๋ค.
[๊ฐ๋ฐ์๋ฅผ ์ํ ์ํํธ ๋ ํํธ ํ ์คํธ] ๋ค์นดํ์ ์ฃผ์ด์น
ํ ์คํธ๋ ํ์์ ํ ์คํธ์ ๊ฐ์ ์๋ ํ ์คํธ๋ฅผ ํฌํจํ์ง๋ง, ๋ณดํต์ ์๋ํ ํ ์คํธ๋ฅผ ์ด์ผ๊ธฐํฉ๋๋ค. ๋ฐฑ์๋์ ๋ฌ๋ฆฌ ํ๋ก ํธ์๋ ๊ฐ๋ฐ์์๋ ํ ์คํธ๋ฅผ ์ง์ง ์๋ ๋ถ๋ค๋ ๋ง์ต๋๋ค. ํ ์คํธ์ ๋ํ ์๋ ์ฑ ๋ค์ ์ฝ์ด๋ด๋ UI ํ ์คํธ ์๋ํ๋ ๋ถ๊ฐ๋ฅํ๊ณ , ๋นํจ์จ์ ์ด๋ผ๊ณ ํฉ๋๋ค.
๊ทธ๋ฌ๋ฉด ๊ณผ๊ฑฐ์ ํ์ฌ๋ ๋ฌด์์ด ๋ฌ๋ผ์ก๋์ง, ์ ํ์ธ๋ค์ ์ UI ํ ์คํธ๋ ๋นํจ์จ์ ์ด๋ผ ์๊ฐํ๋์ง ๊ณ ๋ฏผํด๋ณผ ํ์๊ฐ ์์ต๋๋ค.
์ ์ธํ UI, ํ๋ก ํธ์๋์ ๋ถ๋ฆฌ
๊ณผ๊ฑฐ์ UI๋ ๋ช ๋ นํ์ผ๋ก ๊ฐ๋ฐํ์ต๋๋ค. ์ง๊ธ๋ ๋ชจ๋ฐ์ผ ์ฑ ์ชฝ์ ๋ช ๋ น์ ์ธ UI๊ฐ ๋ง์ง์. UI ์ํ๋ ๋๋ฌด๋ ๋ค์ํ๋ฐ, ์ด๋ฅผ ๋์๋ค๋ฐ์ ์ผ๋ก ๋ณ๊ฒฝํ๋ค๋ณด๋ ์๋ฌ๊ฐ ๋ฐฅ ๋จน๋ฏ์ด ๋์์ต๋๋ค. ๊ทธ๋์ ์ ์ฌ๋๋ค์ Model์ View์์ ๋ถ๋ฆฌํด๋ด๋ ค๊ณ ํ์ง์. Model์ ๋ ํ ์คํธํ๊ธฐ ์ฌ์ ๊ณ , View ๋ ์ด์ด๋ ์ต๋ํ ์๊ฒ ๊ฐ์ ธ๊ฐ๋ ค ํ์ต๋๋ค. ํ ์คํธ๋ฅผ ํด๋ณด์ง ์์๋ ๋ ์ ๋๋ก ๋ปํ๊ณ ๋จ์ํ๊ณ ๋ฉ์ฒญํ UI๋ง ๋จ๊ฒจ๋๋ ค ํ์ฃ .
ํ์ง๋ง ์ธ์์ ๋ณํ์ต๋๋ค. Web์์๋ React, Vue, Svelte์ ๊ฐ์ด ์ ์ธ์ ์ธ UI๊ฐ ๋์ธํ์ต๋๋ค. MVC ํจํด์ ์ญ์ฌ ์์ผ๋ก ์ฌ๋ผ์ง๊ณ , ๋ฆฌ์กํธ๋ ์ํ๋ฅผ ํํํ๋ ์์ ํจ์๋ก์ UI๋ฅผ ์ฌ์ ์ํ์ต๋๋ค. MVC ํจํด์ ๋ฆฌ์กํธ์ ์ต์ง๋ก ๋ผ์๋ง์ถฐ์ ์ดํดํด๋ณด๋ ค ํ๋ฉด ๋ญ๊ฐ ์ ์ ํ๋ฆฌ๋ ์ด์ ์ด๊ธฐ๋ ํฉ๋๋ค.
๋ฆฌ์กํธ์ ์ ์ธ์ ์ธ UI๋ ์ฑ๋ฅ์์๋ ์ํด๋ฅผ ๋ณด์์ง๋ง (๊ฐ์ ๋์ ๋๋ฆฝ๋๋ค!), ๋ ์์ธก ๊ฐ๋ฅํ๊ณ ๋ ํ ์คํธํ๊ธฐ ์ฌ์ ์ต๋๋ค. ์ํ ๋ชจ๋ธ์ ์์ ํจ์๋ก ์ค๊ณํด์ ์ฑ์์ ๋ถ๋ฆฌํ ์๋ ์์์ฃ .
๊ทธ๋ฐ๋ฐ ๋ฌธ์ ๋ ๋ฐฑ์๋์ ํ๋ก ํธ์๋๊ฐ ๋ถ๋ฆฌ๋๋ฉด์ ์๊ฒผ์ต๋๋ค. ๋ฐฑ์๋๊ฐ ๋๋ถ๋ถ์ ๋ชจ๋ธ๊ณผ ๋ก์ง์ ๊ฐ์ ธ๊ฐ๋ฉด์, ํ๋ก ํธ์๋์๋ ๋ชจ๋ธ์ด๋ผ ๋ถ๋ฅผ๋งํ ๋ก์ง์ด ๊ฑฐ์ ๋จ์ง ์๊ฒ ๋์์ต๋๋ค. ํ๋ก ํธ๋ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์์ ๋ณด์ฌ์ฃผ๊ณ , ์ฌ์ฉ์๊ฐ ์ ๋ ฅํ ๋ฐ์ดํฐ๋ฅผ ์๋ฒ์ ๋ณด๋ด๋ ์๋์ ์ธ ์ญํ ๋ง ๋จ์์ต๋๋ค.
ํ ์คํธ ์ฑ ์์ ์ ๋ฐฐ๋๋ค์ด ํ๋ ๋ง์ ์๋ฌธ์ด ์๊น๋๋ค. ์ ๋ ํ ์คํธ๋ ๋ง์ด ํ๊ณ , UI ํ ์คํธ๋ ์๋ํํ์ง ๋ง๋ผ๋๋ฐโฆ ๊ทธ๋ฌ๋ฉด ํ๋ก ํธ์๋ ๊ฐ๋ฐ์๋ ์๋ํ ํ ์คํธ๋ฅผ ํ์ง ๋ง๋ผ๋ ๊ฒ ์๋๊ฐ์?
๋ฌผ๋ก ๊ทธ๋ ์ง ์์ต๋๋ค.
์คํ๋ ค ๋ณต์กํด์ง ๊ฒ์ UI ๋ก์ง๊ณผ ์ํ์์ต๋๋ค. ์ ๊ณ ํผ์ณ์ง๋ UI, disable, hoverํ์ ๋์๋ง ๋ณด์ด๋ UI, ๋งํฌ์ ๋ฒํผ, ์ฒดํฌ๋ฐ์ค, ์ธ๋ถ ์์กด์ฑ์ธ ์๋ฒ์์ ์ํธ์์ฉ, ๋นจ๊ฐ์๊ณผ ํ์โฆ ์ด๋ฐ ๊ฑด unit test ๋ง์ผ๋ก ๊ฒ์ฆํ๊ธฐ ์ด๋ ค์ ์ต๋๋ค.
ํ๋ก ํธ์๋ ๊ฐ๋ฐ์๋ UI๋ฅผ ํ ์คํธํด์ผ ํฉ๋๋ค. UI ํ ์คํธ๋ ๊ทธ๋ฌ๋ฉด ์ ์ด๋ ค์ธ๊น์?
css, xpath, capture & replay๋ ์ ์คํจํ๋
๋ง์ฐ์ค๋ก ๋ฒํผ ํด๋ฆญ : Button_Push, x=200, y=300
ํ์ง๋ง ๋ง์ฝ ๊ทธ ๋๊ตฌ๋ฅผ ์ฌ์ฉํด ์๋ํํ ๋ค์ ๋์ ์ํํธ์จ์ด์ UI๊ฐ ๋ณ๊ฒฝ๋๋ค๋ฉด ์ด๋ป๊ฒ ๋ ๊น์? ์์ ์์์ ๊ฐ๋ฐ์๊ฐ ๋ฒํผ์ ์์น๋ฅผ x=200, y=300์์ ์ฝ๊ฐ ์์ง์ฌ ๋ณธ๋ค๋ฉด ์ด๋จ๊น์? ๊ทธ๋ฐ ์คํฌ๋ฆฝํธ๊ฐ 100๊ฐ๋ผ๋ฉด ์ด๋ป๊ฒ ํด์ผ ํ ๊น์?
[๊ฐ๋ฐ์๋ฅผ ์ํ ์ํํธ ๋ ํํธ ํ ์คํธ] ๋ค์นดํ์ ์ฃผ์ด์น
์ ๊ฐ ์ ์ ์ผํ๋ ํ ํ์ฌ์์๋ xpath๋ css ์ ํ์๋ฅผ ๊ฐ์ง๊ณ ํ ์คํธ๋ฅผ ์์ฑํ์ต๋๋ค. playwright์์ xpath๋ก ์ฃผ๋ฌธ์์ ์ ํ๋ฒํธ๋ฅผ ์ ๋ ฅํ๋ ์ฝ๋๋ฅผ ์์ฑํด๋ณด๋ฉด ๋ค์๊ณผ ๊ฐ์ ๋ชจ์์ด ๋ฉ๋๋ค.
// ์ฃผ๋ฌธ์์ ์ ํ๋ฒํธ ์
๋ ฅ
await page.locator('//*[@id="main"]/div/section[1]/div[3]/label/div/div[1]/input').fill('01011112222')
xpath๋ dom tree์์ ํด๋น element๋ก ๊ฐ๋ ๊ฒฝ๋ก์ธ๋ฐ์. ์ด๋ฌ๋ฉด ์ค๊ฐ์ tree๊ฐ ์กฐ๊ธ๋ง ๋ณํด๋ test๊ฐ ๊นจ์ง๋๋ค. ์คํํธ์ ์์ ์ฃผ๋ฌธ์์๋ ๋งค์ผ ๊ฐ์ด UI์ ์๋ก์ด ์์๊ฐ ์ถ๊ฐ๋๊ณ ์ฌ๋ผ์ง์ง์. ์ฝ๋๊ฐ ์ง๊ด์ ์ด์ง๋ ์์์ ์ฃผ์๋ ๋ฌ์์ค์ผ ํ์ต๋๋ค.
์ด๋ฐ ๋ฌธ์ ๋ฅผ ๊ฒช์ผ๋ ๋ณดํต์ xpath๊ฐ ์๋๋ผ css๋ก ํ ์คํธ๋ฅผ ํ์ต๋๋ค. ๋ค์์ฒ๋ผ ๋ง์ด์ฃ .
// ์ฃผ๋ฌธ์์ ์ ํ๋ฒํธ ์
๋ ฅ
await page.locator('input.phone-input').fill('01011112222')
ํ์ง๋ง ์ ํฌ ์ฃผ๋ฌธ์์๋ ์ ํ๋ฒํธ๋ฅผ ์ ๋ ฅํ๋ ํ๋๊ฐ 3๊ฐ๋ ์์ต๋๋ค. ์ฃผ๋ฌธ์ ์ ๋ณด, ๋ฐฐ์ก์ง ์ ๋ณด, ์ด๋ฒคํธ ์ ๋ณด๊ฐ ๊ทธ๊ฒ์ธ๋ฐ์. ์๋ฅผ ๋ค์ด ํ์์ด์ ๊ณ ๊ฐ ๋ถ์ด ์น๊ตฌ์๊ฒ ์ ๋ฌผ์ ํ๋ค๋ฉด ์ฃผ๋ฌธ์๋ ๋ถ๋ชจ์ด์ง๋ง, ๋ฐฐ์ก ์ ๋ณด๋ ์น๊ตฌ์ ์ ํ๋ฒํธ๋ก, ์ด๋ฒคํธ๋ ๋ณธ์ธ์ ์ ํ๋ฒํธ๋ก ์๋ชจํ ์๋ ์๊ฒ ์ฃ . ์ด๋ฌ๋ฉด ๊ฐ์ input์ด 3๊ฐ ์์ผ๋ css ์ ํ์๊ฐ ๊ฒน์น ๊ฒ๋๋ค. ๊ทธ๋ฌ๋ฉด ๋ค์์ฒ๋ผ ๊ณ ์ณ์ค์ผ๊ฒ ์ฃ ?
// ์ฃผ๋ฌธ์ ์ ๋ณด์ ์ ํ๋ฒํธ ์
๋ ฅ
await page.locator('section.customer-info-section input.phone-input').fill('01011112222')
์ด๋ ๊ฒ classname ์ถฉ๋์ ๊ณ ํต์ ๊ฒช๋ ์ฌ๋๋ค์, classname์ ์๋์ผ๋ก ๋ง๋ค์ด์ฃผ๋ css module์ด๋ styled component, tailwind ์ ๊ฐ์ ๋ค์ํ ๋์์ ์ฐพ๊ธฐ ์์ํ์ต๋๋ค. ๋ฌธ์ ๋ ์ด๋ฐ ๋์์ ์ฌ์ฉํ๋ฉด ํ ์คํธ๊ฐ ๋ ์ด๋ ค์์ง๋ค๋ ์ ์ด์์ด์.
์๋ฅผ ๋ค์ด ์ ํฌ๋ styled component๋ฅผ ์ฌ์ฉํ๋๋ฐ, ๋ฌด์์๋ก ์์ฑ๋ classname ์ ๋ค์์ฒ๋ผ ์ดํดํ๊ธฐ ์ด๋ ต๊ฒ ์๊ฒผ์ต๋๋ค.
<div data-size='h48' data-state='focused' class='sc-781e12f9-3 bjGNWZ'>
<div class='sc-781e12f9-4 bHoUeX'>
<p id=':r2:' class='sc-781e12f9-2 ebNRsq'>
*ํด๋์ ํ (-์ ์ธ)
</p>
<input aria-labelledby=':r2:' type='tel' placeholder='ํด๋์ ํ๋ฅผ ์
๋ ฅํด์ฃผ์ธ์' value='' />
</div>
<div class='sc-781e12f9-5 fYyzVC'></div>
</div>
์ด๋ฅผ playwright locator๋ก ๊ฐ์ ธ์ค๋ ค ํ๋ฉด ๋ค์์ฒ๋ผ ๋๊ฒ ์ฃ . ์ฝ๊ธฐ๋ ์ด๋ ต์ง๋ง, css๋ฅผ ์กฐ๊ธ์ด๋ผ๋ ๋ฐ๊พธ๋ฉด ํ ์คํธ๊ฐ ๊นจ์ง ๊ฒ๋๋ค.
// ์ฃผ๋ฌธ์ ์ ๋ณด์ ์ ํ๋ฒํธ ์
๋ ฅ
await page.locator('sc-781e12f9-2 input[type=tel]').fill('01011112222')
๊ณผ๊ฑฐ์ ์ฐ๋ GUI ํ๋ ์์ํฌ๋ค์ ์ฌ์ ๋ ๋ ๋ชปํ๋ฉด ๋ชปํ์ง, ๋ ๊ด์ฐฎ์ง๋ ์์์ต๋๋ค. ๋งค๋ฒ ์ธ์์ ์ผ๋ก test id๋ฅผ ๋ฌ์์ฃผ๊ฑฐ๋, ํ๋ฉด์์ ์ด๋ค ์์น์ ์๋์ง ์ขํ๋ฅผ ์ง์ ํด์ ํด๋ฆญํ๋ ์์ด์๋๋ฐ์. ๊ทธ๋์ ์ ํ ์คํฐ๋ค์ ๋น์ ์ ์ฒด๋ค์ด ํ๋ณดํ๋ UI ํ ์คํธ ์๋ํ ๋๊ตฌ๋ฅผ ๋ฏฟ์ง ๋ชปํ๊ฒ ๋ ๊ฒ์ด์์ฃ .
์ด๋ฐ ํ ์คํธ ๋ฐฉ์๋ค์ ์ฌ๋์๊ฒ ์ง๊ด์ ์ด์ง ๋ชปํฉ๋๋ค. ์ฌ์ฉ์์ ์ ์ฅ์ด ์๋๋ผ, ๊ธฐ์ ์ ์ธ ๋ฉด์์ ํ ์คํธํ๊ธฐ ๋๋ฌธ์โฆ ๊ธฐ์ ์ด ๋ฐ๋๋ฉด ํ ์คํธํ๋ ๋ฐฉ์๋ ๋ฐ๋์ด์ผ ํ์ฃ User Interface๋ฅผ ํ ์คํธํ๋ ๊ฒ์ธ๋ฐ๋ ๋ง์ด์ฃ !
ํ์ง๋ง ํ ์คํธ์์ ์๋งจํฑํ ์ ๊ทผ์ฑ ํ์ค์ ์ด์ฉํ๋ฉด์ ์ด์ผ๊ธฐ๊ฐ ๋ฌ๋ผ์ก์ต๋๋ค.
role๊ณผ accessible name์ ์ด์ฉํด์ ํ ์คํธํ๊ธฐ
์ต๊ทผ web์ ๋ฌผ๋ก ์ด๊ณ ์๋๋ก์ด๋๋ IOS์์๋ UI ๊ฐ๋ฐ์ ํ ๋ ์ ๊ทผ์ฑ ๊ธฐ์ ์ ์ด์ฉํฉ๋๋ค. ๊ทธ๋ฌ๋ฉด ์ ๊ทผ์ฑ ๊ธฐ์ ์ ๋ฌด์์ด๊ธธ๋, UI ํ ์คํธ์ ๋์์ด ๋์์๊น์? ์๊ฐ ์ฅ์ ์ธ ๋ถ๋ค์ด ์ด์ฉํ๋ ์คํฌ๋ฆฐ ๋ฆฌ๋๊ฐ ์ฝ๊ธฐ ์ฌ์ด ์ฝ๋๋, ํ ์คํธ ์๋ํ ๋๊ตฌ๋ก๋ ๊ฒ์ฌํ๊ธฐ ์ฝ๊ธฐ ๋๋ฌธ์ ๋๋ค.
์ ๊ทผ์ฑ ๊ธฐ์ ์ ๋ค์ํ ์ธก๋ฉด์ด ์์ง๋ง, ์ฌ๊ธฐ์๋ ํนํ ์ ์๋ ฅ์๋ฅผ ํฌํจํ ์๊ฐ ์ฅ์ ์ธ ๋ถ๋ค์ด ์ด์ฉํ๋ ์คํฌ๋ฆฐ ๋ฆฌ๋๋ฅผ ์ด์ผ๊ธฐํฉ๋๋ค.
์๊ฐ ์ฅ์ ์ธ ๋ถ๋ค์ UI๋ฅผ ๋์ผ๋ก ์ฝ๊ธฐ ์ด๋ ค์ํฉ๋๋ค. ๊ทธ๋์ ์คํฌ๋ฆฐ ๋ฆฌ๋๋ผ๋ ๊ธฐ๊ณ๊ฐ ํ๋ฉด์ ์ฝ์ด์ฃผ๋๋ฐ์. ๊ธฐ๊ณ๊ฐ UI๋ฅผ ์ฝ๊ธฐ ์ํด์๋ UI์ ์๋งจํฑํ ์๋ฏธ๋ค์ ์ ์ ์์ด์ผ ํ๊ณ , ์ฌ๊ธฐ์๋ ํ์ค์ด ์์ด์ผ ํฉ๋๋ค.
์๋ฅผ ๋ค์ด ์ด๋ค ๊ฐ๋ฐ์๊ฐ div์ onClick์ ๋ฌ์๋๊ณ ๋ฒํผ์ด๋ผ๊ณ ํ๋ค๊ณ ํฉ์๋ค. ๊ทธ๋ฌ๋ฉด ์ด๊ฒ ๋ฒํผ์ธ์ง ๊ทธ๋ฅ div์ธ์ง, ์๋๋ฉด ๋งํฌ์ธ์ง, ์ฒดํฌ๋ฐ์ค์ธ์งโฆ ๊ธฐ๊ณ๋ ์ ์๊ฐ ์์ต๋๋ค.
๊ทธ๋์ Accessible Rich Internet Applications ์ ์ค์ฌ์ ARIA ํ์ค์ด ๋ฑ์ฅํ์ต๋๋ค. ARIA๋ ์น ์ฝํ ์ธ ๊ฐ ๋ชจ๋์๊ฒ ์ ๊ทผ ๊ฐ๋ฅํ๊ฒ ํ ์ ์๋๋ก, element๊ฐ ์ด๋ค ์๋ฏธ๋ฅผ ๊ฐ์ง๊ณ ์๋์ง ๋ํ๋ผ ์ ์๊ฒ ํ์ต๋๋ค.
๊ทธ๋ฐ๋ฐ! ํ
์คํธ ์๋ํ ๋๊ตฌ๋ ๊ฐ์ ์ ๋ณด๋ฅผ ์ด์ฉํ ์ ์์ต๋๋ค. ์๋ฅผ ๋ค์ด <button> ๊ตฌ๋งคํ๊ธฐ </button>
๊ฐ์ ์์๊ฐ ์๋ค๋ฉด, ์คํฌ๋ฆฐ ๋ฆฌ๋๋ โ๊ตฌ๋งคํ๊ธฐ, ๋ฒํผโ์ด๋ผ๊ณ ์ฝ์ด์ค๋๋ค. ๊ทธ๋ฌ๋ฉด ์๋ํ ๋๊ตฌ์๊ฒ๋ โ๊ตฌ๋งคํ๊ธฐ ๋ฒํผ์ ์ฐพ์์ ํด๋ฆญํด!โ ํ๊ณ ๋ช
๋ นํ ์ ์์ฃ .
์ด ๋ฐฉ์์ Kent C dodds ์จ๊ฐ testing-library๋ฅผ ํตํด ์ฒ์ ๊ฐ๋ฐํ๊ณ , playwright ๋ฑ๋ ๋ฐ์ ๋ค์์ต๋๋ค. playwright๋ก๋ ๋ค์์ฒ๋ผ ์ธ ์ ์์ต๋๋ค.
await page.getByRole('button', { name: '๊ตฌ๋งคํ๊ธฐ' }).click();
์ง๊ด์ ์ด์ง ์๋์? ์์ ์ด์ผ๊ธฐํ๋ ์ฃผ๋ฌธ์ ์ ๋ณด๋ ๋ค์์ฒ๋ผ ์ง๊ด์ ์ผ๋ก ์ธ ์ ์์ต๋๋ค.
await page.getByRole('region', { name: '์ฃผ๋ฌธ์ ์ ๋ณด' })
.getByPlaceholder('*ํด๋์ ํ').fill('01011112222');
์ด๋ฐ ์ ๊ทผ์ฑ ์์๋ ํน์ ํ๋ ์์ํฌ๋ css, dom tree ๊ตฌ์กฐ ๋ฑ์ ์์กดํ์ง ์์ต๋๋ค. ์ ๊ฐ ์ฃผ๋ฌธ์์ ์ด๋ค ์ปดํฌ๋ํธ๋ฅผ ์ถ๊ฐํ๊ฑฐ๋, ์์น๋ฅผ ๋ณ๊ฒฝํ๋๋ผ๋ ์ด ํ
์คํธ๋ ๊นจ์ง์ง ์์ ๊ฒ๋๋ค. ์ฃผ๋ฌธ์ ์ ๋ณด
section์ *ํด๋์ ํ
๋ผ๋ placeholder๋ฅผ ๊ฐ์ง input๋ง ์๋ค๋ฉด ๋ง์ด์ฃ .
div์ role์ ์ค์ semantic html์ ๋ง๋ค๊ธฐ
์ด๋ฌํ role์ ๋๋ถ๋ถ semantic html์ ๋์๋ฉ๋๋ค. ์๋ฅผ ๋ค์ด button์ button role์ ๊ฐ์ง๊ณ ์. input[type=checkbox]๋ checkbox role์ ๊ฐ์ง๊ณ ์. section ์ region role์ ๊ฐ์ง๋ ์์ ๋๋ค.
๊ทธ๋ฐ๋ฐ ํ์ฌ๋ ๋์์ด๋๊ฐ ์ํ๋ ์คํ์ผ๋ก ์ปค์คํ ํ๊ณ ์ถ๋ค๊ฑฐ๋, browser์ ๋ฐ๋ผ์๋ css reset์ด ์ ์๋ํ์ง ์๋๋ค๋ ์์ ์ด์ ๋ก semantic role ๋์ div ๋ฑ์ ์ด์ฉํด ์ง์ ์ปดํฌ๋ํธ๋ฅผ ๊น๋ ๊ฒฝ์ฐ๊ฐ ์์ต๋๋ค. ์ ํฌ ํ์ฌ๋ ๊ทธ๋ฐ๋ฐ์. div์ onClick์ ๋ฌ์๋ ์ปดํฌ๋ํธ๋ button role์ด ์์ด์ ์๋งจํฑํ์ง๋ ์๊ณ , ํ ์คํธ ๋๊ตฌ๋ ์ฐพ์ง ๋ชปํฉ๋๋ค. ๊ทธ๋ฌ๋ฉด button tag๋ฅผ ๊ผญ ์จ์ผ ํ๋ ๊ฑธ๊น์?
๋คํํ๋ ํ๋ฒํ div๋ role์ ์ ํฉํ๊ฒ ๋ฌ์์ฃผ๊ณ , role์ ๋ง๋ ๋์์ ๊ตฌํํด์ฃผ๋ฉด semanticํ html์ฒ๋ผ ๋์ฐ ๋ฐ์ ์ ์์ต๋๋ค. ์ด๋ ๊ฐ role์ ํด์คํ๋ mdn ๋ฌธ์์ ์น์ ํ๊ฒ ๋์ ์์ด์, ๋ณด๊ณ ๋ฐ๋ผํ๊ธฐ๋ง ํ๋ฉด ๋์ง์.
๋ค์์ ๊ฐ๋จํ๊ฒ ๊ตฌํํด๋ณธ ์ปค์คํ Button ์ปดํฌ๋ํธ์ ์์ ๋๋ค. ์ค๋ฌด์์ ์ฐ๋ ๊ฒ๋ณด๋ค๋ ํต์ฌ๋ง ๋จ๊ฒผ๊ณ ํ์ ๋ ์๋ตํ์ต๋๋ค.
function Button({ children, onClick, disabled=false }){
function handleCommand(event){
if (disabled){
return;
}
if (
event instanceof KeyboardEvent &&
event.key !== "Enter" &&
event.key !== " "
) {
return;
}
onClick()
}
return (
<div tabindex={0} role="button" aria-disabled={disabled} onClick={handleCommand} onKeyDown={handleCommand}>
{children}
</div>
)
}
aria-label ๋ก ์ ๊ทผ ๊ฐ๋ฅํ ์ด๋ฆ ๋ถ์ฌ์ฃผ๊ธฐ
ํํธ ์์ ์ ํ๋ฉด์ ํ ์คํธ๋ฅผ ์ง๋ค๋ณด๋ฉด ์ ๊ทผ๊ฐ๋ฅํ ์ด๋ฆ(accessible name)์ด ๋ฌด์์ธ์ง ๋ชจํธํ ๊ฒฝ์ฐ๋ ๋ง๋ฉ๋๋ค. ํนํ icon ๋ฒํผ์ ์ฌ๋ก๋ถํฐ ํ๋์ฉ ์ดํด๋ณด๊ฒ ์ต๋๋ค.
์ ์ฌ์ง์ ๋ณด์๋ฉด ์ด๋ฏธ์ง์ ์์ด์ฝ๋ค์ด ๋ณด์ ๋๋ค. ์คํฌ๋ฆฐ๋ฆฌ๋๋ ํ ์คํธ ๋๊ตฌ, ๊ฒ์์์ง๊ณผ ๊ฐ์ ๊ธฐ๊ณ๋ ์ด๋ฏธ์ง ์ธ์ AI๋ผ๊ณ ํด๋ ์ด๋ฐ ์์ด์ฝ์ ์ด๋ฏธ์ง๋ฅผ ํ์ ํ๊ธฐ ์ด๋ ต์ต๋๋ค. ์คํฌ๋ฆฐ๋ฆฌ๋๋ก ์ฝ์ด๋ณด๋ฉด ์ด๋ฏธ์ง ํ์ผ๋ช ์ ์ฝ์ด์ฃผ๊ฑฐ๋, โ๋ผ๋ฒจ์ด ์ง์ ๋์ง ์์โ ๊ฐ์ ๋ง๋ง ๋ฐ๋ณตํด์ ๋์ค์ง์.
<img src='https://store-dev-contents.frommyarti.com/store_goods/image/393/d21e0c09-f03d-45ac-a41e-6883a2f69d31_1687221845859'/>
๊ทธ๋ฌ๋ฉด โ๋ผ๋ฒจโ์ด๋ ๋ฌด์์ผ๊น์? ๋ผ๋ฒจ์ ์ด๋ ๊ฒ ํ
์คํธ๊ฐ ์๋ ์ํธ์์ฉ ๊ฐ๋ฅํ ๋งํฌ๋ ๋ฒํผ ๋ฑ์ ๋ฌ์์ฃผ๋ ์ด๋ฆ์ ๋งํฉ๋๋ค. ์๋ฅผ ๋ค์ด ๋ค์๊ณผ ๊ฐ์ด aria-label
attribute๋ฅผ ์ด์ฉํด์ ๋ฌ์์ค ์ ์์ต๋๋ค.
<Button aria-label='์ ์ฒด ๋ฉ๋ด' onClick={() => {
showMenu();
}}>
<Icon src={menuIcon} />
</Button>
๊ทธ๋ฌ๋ฉด ์คํฌ๋ฆฐ ๋ฆฌ๋๋ ๋ฌผ๋ก ์ด๊ณ , ํ ์คํธ ๋๊ตฌ์ ๊ฒ์ ์์ง ๋ก๋ด๋ ์ด๊ฒ ๋ฉ๋ด ๋ฒํผ์ด๋ผ๋ ๊ฑธ ์์๋ณผ ์ ์์ต๋๋ค.
๋ฒจ์ ์ด๋ฏธ์ง ๋์ ์ ํ ์คํธ๋ฅผ ๋์๋ ์๊ด ์๊ฒ ๋ฃ์ด์ค์ผ ํฉ๋๋ค. ํ ์คํธ์ฉ์ผ๋ก ํด๋์ค ๋ช ์ฒ๋ผ ๋ฃ์ด๋๋ ๊ฒฝ์ฐ๊ฐ ์๋๋ฐ ๊ทธ๋ฌ๋ฉด ์ ๋ฉ๋๋ค. i18n ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๋ฑ์ ์ด์ฉํ ๋ฒ์ญ์ด ์๋ค๋ฉด, ์ฌ์ฉ์๊ฐ ์ฌ์ฉํ๋ ์ฌ์ดํธ์ ์ธ์ด์ ๋ง๊ฒ ๋ฃ์ด์ค์ผ ํฉ๋๋ค.
// ์ด๋ ๊ฒ ํ๋ฉด ์ ๋ฉ๋๋ค. X
<Button aria-label='test menu button alt' onClick={() => {
showMenu();
}}>
<Icon src={menuIcon} />
</Button>
// ์ฌ์ฉ์ ์ธ์ด์ ๋ง๊ฒ ๋ฒ์ญ ํฉ๋๋ค. O
const { t } = useTranslation('common');
<Button aria-label={t('header-menu-alt'))} onClick={() => {
showMenu();
}}>
<Icon src={menuIcon} />
</Button>
section์ด๋ list์ aria-labelledby์ aria-current ์ด์ฉํ๊ธฐ
๊ฐ์ ์ด๋ฆ์ ๊ฐ์ง ์ปดํฌ๋ํธ๊ฐ ์ฌ๋ฟ ์๋ ๊ฒฝ์ฐ๋ ์์ต๋๋ค. ์ด๋ฐ ๊ฒฝ์ฐ์๋ ์ฌ๋ฌ section์ด๋ list๋ก ๋๋ ์ค์ผ ํ๋๋ฐ์.
์๋ฅผ ๋ค์ด ์ ํฌ ํ์๊ฐ์ ํ์ด์ง๋ ์ฌ๋ฌ ๋จ๊ณ(multi step)๋ก ์ด๋ฃจ์ด์ ธ ์์ต๋๋ค. ๊ฐ section ๋ง๋ค โํ์ธโ์ด๋ผ๋ ๋๊ฐ์ ์ด๋ฆ์ ๊ฐ์ง ๋ฒํผ์ด ๋ฐ๋ณต๋ฉ๋๋ค. ์ฌ์ฉ์์๊ฒ๋ ํ์ฌ ์คํ ๋ง ์๊ฐ์ ์ผ๋ก ๋ณด์ด์ง๋ง, ์ค์ ๋ก๋ ๋ชจ๋ step์ด ๋ณด์ด์ง ์๊ฒ ๋ ๋ ๋์ด์์ต๋๋ค.
ํ์ธ ๋ฒํผ์ ํด๋ฆญํ๋ผ๊ณ ๋ง ํ๋ฉด ํ ์คํธ ๋๊ตฌ๋ ์ด ์ค์ ์ด๋ค ๋ฒํผ์ ํด๋ฆญํด์ผ ํ๋์ง ๋ชจ๋ฆ ๋๋ค.
// Error: page.getByRole('button', { name: 'ํ์ธ' }) resolved to 4 elements:
await page.getByRole('button', { name: 'ํ์ธ' }).click();
์ด๋ฐ ๋์๋ ๊ฐ section์ ์ ๋ชฉ์ id๋ฅผ ๋ฌ์์ฃผ๊ณ , aria-labelledby๋ฅผ ์จ์ ์ฐ๊ฒฐํด์ฃผ๋ฉด ๋ฉ๋๋ค. ๊ทธ๋ฌ๋ฉด region role์ ๊ฐ์ง๊ฒ ๋๊ณ , section ์์์ button์ ์ฐพ์ ์ ์์ต๋๋ค.
<section aria-labelledby='policy-section-title'>
<h2 id='policy-section-title'>๋ง๋์ ๋ฐ๊ฐ์์. ๊ฐ์
์ฝ๊ด์ ํ์ธํด์ฃผ์ธ์.</h2>
...
<Button>ํ์ธ</Button>
</section>
<section aria-labelledby='email-section-title'>
<h2 id='email-section-title'>๊ฐ์
ํ์ค ์ด๋ฉ์ผ ์ฃผ์๋ฅผ ์
๋ ฅํด์ฃผ์ธ์</h2>
...
<Button>ํ์ธ</Button>
</section>
...
await page.getByRole('region', { name: '๊ฐ์
ํ์ค ์ด๋ฉ์ผ ์ฃผ์' })
.getByRole('button', { name: 'ํ์ธ' })
.click();
ํ์ง๋ง ์ด๋ ๊ฒ ํ๋ฉด section์ ๊ตฌ๋ถํ ์๋ ์์ง๋ง, ํ์ฌ ์ฌ์ฉ์๊ฐ ๋ณด๊ณ ์๋ section์ด ๋ฌด์์ธ์ง๋ ์ ์ ์์ต๋๋ค. ์ฌ์ฉ์์ ํ์ฌ ์์น๋ aria-current๋ก ์ค์ ํ๊ณ , ๊ทธ ์ธ์ ํ์ด์ง์๋ focusํ ์ ์๊ฒ focus lock์ ๊ฑธ์ด์ค ์ ์์ต๋๋ค. ์ด ๊ฒฝ์ฐ์๋ aria-current='step'
attribute๋ฅผ ๋ฃ์ด์ฃผ๋ฉด ๋ฉ๋๋ค. ์ด๋ ๋ค์ ๋จ๊ณ๋ก ํน์ ๋ค๋ก ๊ฐ๋ step ๊ฐ์ ์ด๋์ ํ
์คํธํ ๋ ์ ์ฉํฉ๋๋ค.
<section aria-labelledby='policy-section-title' aria-current={isCurrent ? 'step' : undefined}>
<h2 id='policy-section-title'>๋ง๋์ ๋ฐ๊ฐ์์. ๊ฐ์
์ฝ๊ด์ ํ์ธํด์ฃผ์ธ์.</h2>
...
<Button>ํ์ธ</Button>
</section>
await expect(page.getByRole('region', { name: '๊ฐ์
์ฝ๊ด' })).toHaveAttribute('aria-current', 'step');
aria-expanded๋ก ์ ๊ณ ํผ์น ์ํ๋ฅผ ์ ๋ฌํ๊ธฐ
๋ ์ฌ๋ฏธ์๋ ๊ฒฝ์ฐ๊ฐ ์ ๊ณ ํผ์ณ์ง๋ ์์๋ค์ ๋๋ค. ํ๋ฉด์ด ์ข์ ๋ ๋งค์ฐ ๊ธธ๊ณ ๋ง์ ์ ๋ณด๋ฅผ ๊ฐ์ถฐ๋๋ ์ฉ๋๋ก ์ฌ์ฉํ๋๋ฐ์. ๋ฌธ์ ๋ ์ด๋ฐ ์์๋ค์ด ์ ํ์ ธ ์๋์ง, ํผ์น ์ ์๋์ง ์๊ฐ ์ฅ์ ์ธ์ ๋ฌผ๋ก ์ด๊ณ ๊ธฐ๊ณ๋ค๋ ์ ์ ์๋ค๋ ๊ฒ๋๋ค. ํ ์คํธ๊ฐ ์๋ ๊ฒ์ด๋ฉด ๋ชจ๋ฅด์ง๋ง, ๋์๋ง ๋ณด์ด์ง ์์ ๋ฟ์ด์ง ์ค์ ๋ก๋ ์จ๊ฒจ์ง ์ฑ๋ก ๋ ๋๋์ผ ํ๋ ๊ฒฝ์ฐ๋ค์ด ์์ต๋๋ค. transition ์ ๋๋ฉ์ด์ ์ ์ํด์๋ผ๊ฑฐ๋, tab์ ํตํด ์ ํ์ง ์ปดํฌ๋ํธ ์์ ์์์ ์ ๊ทผ ๊ฐ๋ฅํด์ผ ํ๋ ๊ฒฝ์ฐ๋, ๋ณด์ด์ง ์๋ ๋ด์ฉ์ด ๊ฒ์์ ๊ฐ๋ฅํด์ผ ํ๋ ๊ฒฝ์ฐ ๋ฑ์ด ๊ทธ๋ ์ง์.
์ด๋ฐ ๊ฒฝ์ฐ aria-expanded๋ฅผ ์ด์ฉํด์ ์ปดํฌ๋ํธ๊ฐ ํผ์ณ์ ธ์๋์ง ์๋ ค์ฃผ๊ณ , ํ ์คํธํ ์ ์์ต๋๋ค.
<InfoButton
aria-expanded={isOpen}
aria-controls="delivery-info"
onClick={() => {
setIsOpen((old) => !old)
}}
type="button">
๋ฐฐ์ก์ ๋ณด
<Arrow />
</InfoButton>
<div id="delivery-info" aria-hidden={!isOpen}>
...
</div>
aria ์์ฑ์ ์์์ ์ธ props์ ๋ฌ๋ฆฌ dom์ ๋ ๋๋๊ธฐ ๋๋ฌธ์, ๋ค์์ฒ๋ผ ํ ์คํธ๋ ๊ฐ๋ฅํฉ๋๋ค.
const showMoreButton = page.getByRole('button', { name: '๋ฐฐ์ก์ ๋ณด' });
// ๋ซํ ์์
await expect(showMoreButton).toHaveAttribute('aria-expanded', 'false');
// ํด๋ฆญํ๋ฉด
await showMoreButton.click();
// ์ด๋ฆผ
await expect(showMoreButton).toHaveAttribute('aria-expanded', 'true');
// ๋ค์ ํด๋ฆญํ๋ฉด
await showMoreButton.click();
// ๋ซํ
await expect(showMoreButton).toHaveAttribute('aria-expanded', 'false');
๋ฌผ๋ก ํ ์คํธ๊ฐ ์คํ์ผ๊น์ง ํ ์คํธํด์ฃผ์ง ์์ต๋๋ค๋ง. ์ด๋ ๊ฒ ๋ง๋ aria ์์ฑ์ ์ด์ฉํด์ ์กฐ๊ฑด๋ถ ์คํ์ผ์ ๊ฑธ์ด์ฃผ๋ฉด, css์ ์ฐ๊ฒฐ์ ์ข ๋ ์ฝ๊ฒ ํ์ ํ ์ ์์ต๋๋ค. ์๋ฅผ ๋ค์ด ์์ ์ ๋ณด ์ปดํฌ๋ํธ๋ ํผ์ณ์ง๋ฉด ์ฐ์ธก์ ํ์ดํ๊ฐ 180๋ ํ์ ํ๋๋ฐ์. ์ด๋ ๋ค์์ฒ๋ผ ์ฝ๊ฒ ์ง์ ํ ์ ์์ต๋๋ค. ์ ํฌ๋ styled ์ปดํฌ๋ํธ๋ฅผ ์ฌ์ฉํ๊ณ ์์ง๋ง, ๋ค๋ฅธ css framework๋ค๋ aria ๋ฅผ ์ด์ฉํ ์คํ์ผ๋ง์ ์ง์ํ๋ ์ฐพ์๋ณด์๊ธฐ ๋ฐ๋๋๋ค. (์ ๋ ํ์ฌ์ ๋ค์ด์ค๊ธฐ ์ ๊น์ง ์ฃผ๋ก tailwind๋ฅผ ์ฌ์ฉํ์ต๋๋ค.)
export const ArccordionButton = styled(ClickableComponent)`
// ... ์๋ต
&[aria-expanded='true'] ${ArrowIcon} {
transform: rotateZ(180deg);
}
`;
ํค๋๋ฆฌ์ค ์ปดํฌ๋ํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ด์ฉํ๊ธฐ
ํํธ์ผ๋ก๋ Tab์ด๋ Select, Dropdown, Datepicker ์ฒ๋ผ ๋งค์ฐ ๋ณต์กํ ์ปดํฌ๋ํธ๋ฅผ ์ง์ ๊ตฌํํ๊ธฐ๋ ์ด๋ ต์ต๋๋ค. ๊ทธ๋ฌ๋ค๋ณด๋ ์ ๊ทผ์ฑ๋ ํ ์คํธ๋ ํฌ๊ธฐํ๋ ๊ฒฝ์ฐ๊ฐ ๋ง์๋ฐ์. ๊ฒฐ๊ตญ ์ ๋ง์ ์๋ฌ๊ฐ ๋ฐ์ํด์ ๊ณ ์น๋ ค ํ๋ฉด, ํ ์คํธ๊ฐ ์๊ธฐ ๋๋ฌธ์ ๊ธฐ์กด์ ์ ์๋ํ๋ ๊ธฐ๋ฅ์ ๋ง๊ฐํธ๋ฆฌ๊ธฐ๋ ํฉ๋๋ค.
์ด๋ฐ ๋์๋ Radix UI, Ariakit, ๊ทธ๋ฆฌ๊ณ ์ต๊ทผ์ chakra ui ๊ฐ๋ฐ์๊ฐ ๋ง๋ Ark UI ๊ณผ ๊ฐ์ด ์ ๋ง๋ ํค๋๋ฆฌ์ค ์ปดํฌ๋ํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ด์ฉํ๋ฉด ์ฝ๊ฒ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์์ต๋๋ค.
ํค๋๋ฆฌ์ค ์ปดํฌ๋ํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ๋ก์ง๋ง ๊ตฌํ๋์ด ์๊ณ , ์คํ์ผ์ ํ์ฌ์ ์ ํ์ ๋ง๊ฒ ์ปค์คํ ํ ์ ์๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค์ ๋๋ค. ๋ค๋ค ์ ๊ทผ์ฑ์ ์ผ๋์ ๋๊ณ Aria๋ฅผ ์ ๊ตํ๊ฒ ๋ฌ์๋์์ ๋ฟ๋ง ์๋๋ผ, ํค๋ณด๋ ์ ๊ทผ์ด๋, focus ๊ด๋ฆฌ, ํ๋ฉด ํฌ๊ธฐ์ ๋ฐ๋ฅธ ์ฒ๋ฆฌ ๋ฑ์ ์ ๊ตํ๊ณ ์ธ๋ฐํ๊ฒ ๊ตฌํํด๋์์ต๋๋ค.
์๋ฅผ ๋ค์ด ์ ํฌ ํ์์๋ ์ด ์ค์ ๊ฐ์ฅ ์ฑ์ํ Radix๋ฅผ ๋์ ํด์ ์ฌ์ฉ ์ค์ธ๋ฐ์. ์ํ์ ์ต์ ์ ์ ํํ๋ Select ์ปดํฌ๋ํธ๋ combobox์ option role์ ๊ฐ์ง๊ณ ์์ด์, ๋ค์์ฒ๋ผ ์ง๊ด์ ์ผ๋ก ํ ์คํธํ ์ ์์์ต๋๋ค. ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๊ธฐ๋ณธ๊ฐ ์ค์ ์ ํฌ๊ฐ ์ํ๋ ๊ฒ๊ณผ ๋ค๋ฅธ ๋์์ ์ปค์คํ ํ๊ธฐ๋ ์ฌ์ ๊ณ ์.
// e2e/goods.ts
test('์ผ์ฒดํ ์ต์
์ ์ ํํ๊ณ ์ญ์ ํ ์ ์๋ค', async ({ page }) => {
await page.goto(์ผ์ฒดํ_์ต์
_์ํ_์์ธ_๋งํฌ);
await page.getByRole('combobox', { name: '์ต์
์ ์ ํํ์ธ์' }).click();
await page.getByRole('option', { name: 'Night .version' }).click();
// x ๋ฒํผ์ ํด๋ฆญ
await page.getByRole('button', { name: '์ ํํ ๋ฏธ๋์จ๋ฒ 2์ง : Over The Moon ํฌ์นด ์จ๋ฒ Day .version ๋นผ๊ธฐ' }).click();
await expect(page.getByRole('button', { name: '์ ํํ ๋ฏธ๋์จ๋ฒ 2์ง : Over The Moon ํฌ์นด ์จ๋ฒ Day .version ๋นผ๊ธฐ' })).not.toBeVisible();
});
์๋ํ๊ณ ๊ฒ์ฆํ๋ ๋ฌธ์๋ก์ ํ ์คํธ
์ด๋ฌํ ์ ๊ทผ์ฑ์ ์ธ์ด๋ ์ธ๊ฐ๊ณผ ๊ธฐ๊ณ ์ฌ์ด์ ์ฝ์ํ ๊ณต์ฉ์ด์๋ ๊ฐ์ต๋๋ค. ๊ทธ๋์ ๊ธฐํ์๋ ์๊ตฌ์ฌํญ ์ ์์์ ๋์จ ๋ด์ฉ์ ๊ฑฐ์ ๊ทธ๋๋ก ํ ์คํธ๋ก ์ฎ๊ธธ ์ ์์ต๋๋ค. ๊ฑฐ๊พธ๋ก ๊ธฐํ์ ๋ฑ์ ๋ชจํธํ๊ฒ ์ ์๋ ๋ถ๋ถ์ ๋ช ํํ๊ฒ ํ ์ ์๊ณ ์.
์๋ฅผ ๋ค์ด โ๋ก๊ทธ์ธํ์ง ์์ผ๋ฉด ์ฅ๋ฐ๊ตฌ๋ ํ์ด์ง์ ๋ค์ด๊ฐ ์ ์๋ค.โ๋ ์ ์ฑ ์ด ์๋ค๊ณ ํฉ์๋ค. ๊ทธ๋์ โ๋ก๊ทธ์ธ ํ์ง ์์ ๊ฒฝ์ฐ ๋ก๊ทธ์ธ์ ์๊ตฌํ๋ ์ฐฝ์ ๋์ด๋คโ, โํ์ธ ๋ฒํผ์ ๋๋ฅด๋ฉด ๋ก๊ทธ์ธ ํ์ด์ง๋ก ์ด๋ํ๋คโ, โ์ทจ์๋ฅผ ๋๋ฅด๋ฉด ๋ค๋ก๊ฐ๊ธฐ ๋๋คโ ๊ฐ์ ์์ผ๋ก ๊ตฌ์ฒด์ ์ธ ๊ธฐํ์ด ๋์์ต๋๋ค.
์ด๋ฅผ ์ ๊ทผ์ฑ์ ์ธ์ด๋ฅผ ์ด์ฉํ ํ ์คํธ๋ก ์ฎ๊ธฐ๋ฉด ๋ค์๊ณผ ๊ฐ์ต๋๋ค. ๊ฝค ์ง๊ด์ ์ด์ฃ ? ์ด๋ ์ฌ์ฉ์๊ฐ ์ ํ์ ์ธ์ํ๋ ๊ณ์ฝ ์กฐ๊ฑด์ด๋ผ๊ณ ํด์, โ์ธ์ ํ ์คํธโ๋ผ๊ณ ๋ ๋ถ๋ฆ ๋๋ค.
// ์ํ ๋ชฉ๋ก ํ์ด์ง์์
await page.goto(`${CLIENT_HOST}/goods/product`);
// ์ฅ๋ฐ๊ตฌ๋ ํ์ด์ง๋ก ์ด๋ํ๋ฉด
await page.getByRole('link', { name: '์ฅ๋ฐ๊ตฌ๋' }).click();
// ๋ก๊ทธ์ธ์ ์๊ตฌํ๋ ๊ฒฝ๊ณ ์ฐฝ์ด ๋ฌ๋ค
const alertDialog = page.getByRole('alertdialog', { name: '๋ก๊ทธ์ธ' });
// ๊ฒฝ๊ณ ์ฐฝ ์์ ์๋ ํ์ธ ๋ฒํผ์ ๋๋ฅด๋ฉด
await alertDialog.getByRole('button', { name: 'ํ์ธ' }).click();
// ๋ก๊ทธ์ธ ํ์ด์ง๋ก ์ด๋ํ๊ณ , ๋ก๊ทธ์ธ ํค๋๊ฐ ๋ณด์ธ๋ค.
await expect(page.getByRole('heading', { name: '๋ก๊ทธ์ธ' })).toBeVisible()
์ด๋ฐ ํ ์คํธ๋ ์ฝ๋๋ฅผ ์ง๊ธฐ ์ ๋ถํฐ ํ ์คํธ๋ฅผ ๋ฏธ๋ฆฌ ์ง๋ ์๋ ์๊ณ ์. ํ ์คํธ๋ฅผ ์ง๋ ์ฌ๋๊ณผ, ๊ฐ๋ฐ ํ๋ ์ฌ๋์ ๋ถ๋ฆฌํ ์๋ ์์ต๋๋ค. ๋๋ก๋ ์ด๋ ๊ฒ ๋ง๋ ํ ์คํธ๋ฅผ ๊ธฐํ์ ๋๊ณผ ๊ณต์ ํ๋ฉด์, ๊ธฐํ์์ ๋น ์ง ์ผ์ด์ค๋ฅผ ๋ ผ์ํ๊ธฐ๋ ํฉ๋๋ค. ๋ฌด์๋ณด๋ค๋ ๋๋ ค๋ณผ ์ ์๋ ๋ฌธ์์ด๊ธฐ๋ ํ๊ณ ์!
test์ ํจ๊ป ์์ ๊ฐ ์๊ฒ ๋ฆฌํฉํ ๋งํ๊ธฐ
test๋ ๊ฐ๋ฐ ์ ์ ๋ฏธ๋ฆฌ ์์ฑํด๋๋ ๊ฒ ๊ฐ์ฅ ๋์์ด ๋ฉ๋๋ค. ๊ฐ๋ฐํ๋ฉด์ ์์๋ก ํ์ธํ ์๋ ์๊ณ ์. ๊ธฐํ์์์ ๋น ํธ๋ฆฐ ๊ณณ์ด ์๋์ง ๊น๋จน์ง ์๊ณ ํ์ธํ ์๋ ์์ฃ .
ํ์ง๋ง ๊ธฐ๋ฅ์ ์์ฑํ ๋ค์๋ test์ ์ญํ ์ ๋๋์ง ์์ต๋๋ค. ์ฝ๋๋ฅผ ๋ฆฌํฉํ ๋งํ๊ฑฐ๋ ์์ ํ๋ฉด์, ๊ธฐ์กด์ ๋ง๋ค์ด์ง ๊ธฐ๋ฅ์ด ๋ง๊ฐ์ง์ง ์์๋์ง ํ์ธํด์ฃผ๊ธฐ ๋๋ฌธ์ธ๋ฐ์. ์ด๋ฅผ ํ๊ท ํ ์คํธ๋ผ ํฉ๋๋ค.
ํ๊ท ํ ์คํธ๋ ํนํ ์์ ์ ์ธ ๊ฒ ์ค์ํฉ๋๋ค. ํ ์คํธ๊ฐ ๋ง์์ง ์๋ก ์ ์ง๋ณด์ ๋ถ๋ด๋ ์ปค์ง๊ธฐ ๋๋ฌธ์ด์ฃ . ์ฝ๋๋ฅผ ํ ์ค ๊ณ ์น ๋ ํ ์คํธ ์ฝ๋๋ ์ ๋ฐฑ ์ค ๊ณ ์ณ์ผ ํ๋ค๋ฉดโฆ ๊ตฌํ ์ฝ๋๋ณด๋ค ํ ์คํธ ์๋ํ ์ฝ๋๋ฅผ ๊ณ ์น๋๋ฐ ๋ ๋ง์ ์๊ฐ์ ์ฐ๊ฒ ๋ ๊ฒ๋๋ค.
์ต๊ทผ์ Header ์ปดํฌ๋ํธ๋ฅผ ๋ฆฌํฉํ ๋งํ๋ฉด์, ์ํ ์ ๋ ฌ์ด ๋์ง ์๋ ๋ฒ๊ทธ๊ฐ ์์์ต๋๋ค. ๋ค์์ฒ๋ผ ์๊ธด ํ ์คํธ ์ฝ๋๊ฐ ์ด ๋ฒ๊ทธ๋ฅผ ์ก์์คฌ๋๋ฐ์. ํ ์คํธ DB์์ ๊ฐ์ฅ ๋น์ผ ์ํ์ด๊ณ , ๊ธฐ๋ณธ ์ ๋ ฌ์์๋ ๋งจ ์์์ ๋ณด์ฌ์ง๋ 42,000์ ์ง๋ฆฌ ์ํ์ด ๋ณด์ด์ง ์์์ผ ํ๋ค๋ ๊ฐ๋จํ ๋ด์ฉ์ ๋๋ค. ์ค์ ์ฌ์ฉ์๊ฐ ์ฑ์ ์ฌ์ฉํ๋ ๊ฒ๊ณผ ๊ฐ์ ๋ฐฉ์์ผ๋ก ์ ๋ ฌ ๋ฒํผ์ ํด๋ฆญํ๊ณ , ๋ฎ์ ๊ฐ๊ฒฉ ์ ์ต์ ์ ์ ํํ๋ ์์ผ๋ก ๋์ด ์์ฃ .
test('์ ๋ ฌํ ์ ์๋ค', async ({ page }) => {
// given
await page.goto(`${CLIENT_HOST}/product`, { waitUntil: 'networkidle' });
await expect(page.getByRole('link', { name: '42,000์' })).toBeVisible();
// when
await page.getByRole('button', { name: '์ ๋ ฌ' }).click();
await page.getByRole('option', { name: '๋ฎ์ ๊ฐ๊ฒฉ์' }).click();
// then
await expect(page.getByRole('link', { name: '42,000์' })).not.toBeVisible();
});
์ด ํ ์คํธ๋ ๊ฐ๊ฑดํฉ๋๋ค. ์ํ ํ์ด์ง๋ inline-block์ผ๋ก ๋์ด ์๋ ๊ฒ์, grid๋ก ๋ฐ๊พธ๊ธฐ๋ ํ๊ณ ์. CSR์ ํ๋ ๊ฒ์ SSG๋ก ๋ฐ๊พธ๋ ค๊ณ ์๋ ์ค์ด๊ธฐ๋ ํ๊ณ ์. ์ด๋ฏธ์ง๋ img ํ๊ทธ์์ Next image๋ก ๋ชจ๋ ๋ฐ๊พธ๋ ๊ฐ๋ ๋์ ๋ฆฌํฉํ ๋ง์ ๊ฑฐ์ณ์์ง๋ง. ์ค์ ์ฌ์ฉ์ ๋์์ ์๋ฌด ๋ฌธ์ ๊ฐ ์์๊ธฐ ๋๋ฌธ์, ํ ์คํธ๋ ์ ํต๊ณผํ์ต๋๋ค.
๊ทธ๋ฐ๋ฐ ์ด๋ ๋ โฆ Header์ ๊ตฌํ ๋ฐฉ์์ ๋ฐ๊ฟจ๋๋, ํ ์คํธ๊ฐ ๊นจ์ก์ต๋๋ค. ํค๋๋์ ์๋ฌด ์๊ด ์์ด๋ณด์ด๋๋ฐ ์ด์งธ์? ๊ฐ๋จํ๊ฒ ์์ธ์ ๋งํ์๋ฉด ์๋ฑํ๊ฒ๋ ์์ ์๊ฐ Header๋ ๋ฌด๊ดํ useEffect์ ์์กด์ฑ ๋ฐฐ์ด์ ๊ฑด๋๋ ธ๊ธฐ ๋๋ฌธ์ด์์ต๋๋ค. page๋ฅผ ๋ฒ์ด๋๋ฉด ์งํ ์ค์ธ ์์ฒญ์ cancelํ๋ ์ฝ๋์๋๋ฐ์. eslint disable ๋์ด ์๋ ๊ฒ ๋ง์์ ์ ๋ค์ด์, lint ๊ฐ ์ํค๋๋๋ก ์์กด์ฑ ๋ฐฐ์ด์ cancel๋ฅผ ๋ฃ์์ต๋๋ค. ์ํ๊น๊ฒ๋ cancel๋ ์ฐธ์กฐ๊ฐ ์์ ์ ์ธ ํจ์๊ฐ ์๋์๊ณ ์, ์ ๋ ฌ ๋ฐฉ์์ ๋ฐ๊ฟ ๋๋ง๋ค ์์ฒญ์ ์ทจ์ ์์ผ๋ฒ๋ ธ๋ ๊ฒ์ด์์ต๋๋ค.
๊ตฌ์ฒด์ ์ธ ์ด์ ๊ฐ ๋ฌด์์ด์๋ , ์ฝ๋ ๋ฆฌ๋ทฐ์ด๋ค์ ์ด๋ฐ ๋ถ์์ฉ์ ์๊ฐํ์ง ๋ชปํ์ต๋๋ค. Header๋ฅผ ๋ณ๊ฒฝํ PR์ ์ฝ๋๋ฆฌ๋ทฐ๋ฅผ ํต๊ณผํ์ง๋งโฆ ๋จธ์ง ๋๊ธฐ ์ง์ ์ e2e๊ฐ ์ด ์๋ฌ๋ฅผ ๊ฒ๊ฑฐํด์ฃผ์์ฃ .
ํ ์คํธ๋ ๊ทธ ์ธ์๋ ๋ง์ ๋ฒ๊ทธ๋ฅผ ๋ฏธ๋ฆฌ ์ก์์ฃผ์์ต๋๋ค. ์ฃผ๋ฌธ ์๋ฃ ํ์ด์ง์ ๋ค์ด์ค๋ฉด ์ํ ํ์ด์ง๋ก ๋ ์๊ฐ๋ฒ๋ฆฌ๋ ๋ฒ๊ทธ. ์ฃผ๋ฌธ์ ํ์ด์ง์ ๋ค์ด๊ฐ์๋ง์ ์ฌ์ดํธ๊ฐ ์ฃฝ์ด๋ฒ๋ฆฌ๋ ๋ฒ๊ทธ ๋ฑ๋ฑโฆ e2e๊ฐ ๊นจ์ง๋ ๊ฒฝ์ฐ๋ ๋ณดํต ์ ๊ทผ์ฑ์ด ์๋ชป๋์ด ์๋ ๊ฒ์ ์ฌ๋ฐ๋ฅด๊ฒ ๋ฐ๋ก์ก์ ๊ฒฝ์ฐ ๋ฟ์ด์์ต๋๋ค. ๊ธฐํ์ ์์ฒด๊ฐ ๋ฐ๋๊ธฐ ์ ๊น์ง๋, ํ๊ท ํ ์คํธ๊ฐ ๊ธฐํ์์ ์ ์๋ ๋์๋ค์ ์ฌ์ฉ์ ์ ์ฅ์์ ๋งค PR๊ณผ ๋ฐฐํฌ๋ง๋ค ์ฝ๋๋ฅผ ์ง์ผ์ฃผ๊ฒ ์ง์.
ํ ์คํธ ์ปดํ์ผ๋ฌ : UI ํ ์คํธ๋ฅผ ๋ ์ฝ๊ณ ์ง๊ด์ ์ธ ์ธ์ด๋ก ์งค ์๋ ์์๊น?
์ฌ์ ํ ์์ฌ์ด ์ ์ ์ด๋ฌํ ํ ์คํธ๋ ๊ฒฐ๊ตญ QA ์์ง๋์ด๋ ๊ฐ๋ฐ์๊ฐ ์์ฑํด์ผ ํ๋ค๋ ๊ฒ์ ๋๋ค. ๋ฌผ๋ก ๊ธฐํ์ ๋ถ๋ค ์ค์ ์ฝ๋ฉ์ ๋ฐฐ์์ ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํด์ฃผ์๋ ๋ถ๋ค๋ ์์ต๋๋ค๋ง, ์ฌ๋ฌ๋ชจ๋ก ์ด๋ ค์ด ์ผ์ ๋๋ค.
๊ทธ๋์ ์ ๋ ์ต๊ทผ์ ํ ์คํธ ์ปดํ์ผ๋ฌ๋ผ๋ ๊ฑธ ๋ง๋ค์ด๋ณด๊ณ ์์ต๋๋ค. yaml ํ์ผ์ ํ๊ตญ์ด๋ก ๊ฐ๊ฒฐํ๊ฒ ๋ช ์ธ๋ฅผ ์์ฑํ๋ฉด, ํ ์คํธ ์ฝ๋๋ก ๋ณํํด์ฃผ๋ ๋๊ตฌ์ธ๋ฐ์. ์ด ์ญ์ ํ ์คํธ๊ฐ ์ ๊ทผ์ฑ์ ๊ธฐ๋ฐํ์ง ์์๋ค๋ฉด ๋ง๋ค๊ธฐ ์ด๋ ค์ ์ ๊ฒ๋๋ค.
์ ๊ทผ์ฑ์ ํ ์คํธ๋ ๊ฒ์ ์์ง ์ต์ ํ ๊ฐ์ ์ด๋์ด ์๋๋ผ๋, ์ ๊ทผ์ฑ์ ์ผํ๋ชฐ ๋ฑ์ ์ฌ์ ์๋ค์ด ๋ฒ์ผ๋ก ์ง์ผ์ผํ๋ ์ฌํญ์ ๋๋ค๋ง. ์ ํฌ ํ์ฌ๋ฅผ ๋น๋กฏํด ๋ง์ ์ ์ฒด๋ค์ด ์ด๋ฌํ ์ฌ์ค์ ์ ๋ชจ๋ฅด๊ณ ์๊ณ , ํ๊ณ ์ถ์ด๋ ์ด๋ ค์ํ๋ ๊ฒฝ์ฐ๋ ๋ง์ต๋๋ค. ํ์ง๋ง ์ ๊ทผ์ฑ์ ์๊ฐ ์ฅ์ ์ธ ๋ถ๋ค๋ง์ ์ํ ๊ฒ์ ์๋๋๋ค. ์ ๊ทผ์ฑ์ ๋ชจ๋๋ฅผ ์ํ ๊ฒ์ด๊ณ ์ ๋ง๋ ์๋ํ ํ ์คํธ๋ ์ฌ๋ฌ๋ถ์ ์ผ์ ๋ ํธํ๊ฒ ๋์์ค ๊ฒ๋๋ค.
์ ํฌ ํ๋ ๋ถ์กฑํ ๊ฒ ๋ง์ง๋ง, ๋ ธ๋ ฅํ๊ณ ๊ฐ์ ํ๊ณ ์์ต๋๋ค. ์์ผ๋ก๋ ํ ์คํธ ๋ฟ๋ง ์๋๋ผ ๋ค์ํ ์ด์ผ๊ธฐ๋ฅผ ํ์ด๋ณด๋ ค ํฉ๋๋ค. ๋ ๋ต์ฃ .