μ°λ¦¬λ 2λΆμ 3λΆλ₯Ό ν΅ν΄ ννν HTML κ΅¬μ‘°κ° μΌλ§λ μ€μνμ§ λ°°μ μ΅λλ€. νμ§λ§ νλμ μΉμ ν΄λ¦νκ³ , νΌμ³μ§κ³ , νμ μ΄ λ¨λ λ± μ΄ μ μμ΄ μμ§μ λλ€.
μ¬κΈ°μ λ¬Έμ κ° λ°μν©λλ€. λμ 보μ΄λ λμμΈμ λ°λμ§λ§, 보쑰 곡ν κΈ°κΈ°(μ€ν¬λ¦° 리λ λ±)λ μ΄ λ³νλ₯Ό λμΉμ±μ§ λͺ»ν λκ° λ§κΈ° λλ¬Έμ λλ€. μ€λμ μλ°μ€ν¬λ¦½νΈλ‘ λμ μΈ κΈ°λ₯μ ꡬνν λ μΉ νμ€μ μ§ν€λ νμ΄κΈ°, WAI-ARIAλ₯Ό μμλ³΄κ² μ΅λλ€.
1. WAI-ARIA, μ νμνκ°μ?
μλ°μ€ν¬λ¦½νΈλ‘ νΉμ μμλ₯Ό display: none;μμ block;μΌλ‘ λ°κΏ¨λ€κ³ κ°μ ν΄ λ΄
μλ€. λμΌλ‘λ 보μ΄μ§λ§, μ€ν¬λ¦° 리λλ μ΄ λ©λ΄κ° μ΄λ Έλμ§, λ«νλμ§, νΉμ μ§κΈ λ΄κ° λλ₯΄λ κ²μ΄ ν λ²νΌμΈμ§ μΌλ° λ²νΌμΈμ§ μ μ μμ΅λλ€.
WAI-ARIA(Web Accessibility Initiative - Accessible Rich Internet Applications)λ λ°λ‘ μ΄λ΄ λ μ¬μ©ν©λλ€. HTML νκ·Έλ§μΌλ‘λ μ€λͺ νκΈ° νλ 'μν (Role)', 'μν(State)', 'μμ±(Property)'μ μΆκ°λ‘ λͺ μν΄μ£Όλ μΌμ’ μ '보좩 μ€λͺ μ'μ λλ€.
2. μ€μ ! μ κ·Ό κ°λ₯ν ν(Tab) λ©λ΄ λ§λ€κΈ°
κ°μ₯ ννκ² μ°μ΄λ ν λ©λ΄λ₯Ό BEM λ°©λ²λ‘ κ³Ό Vanilla JS, κ·Έλ¦¬κ³ WAI-ARIAλ₯Ό κ²°ν©ν΄ ꡬνν΄ λ³΄κ² μ΅λλ€.
π HTML ꡬ쑰 (BEM + WAI-ARIA)
<div class="c-tabs" data-component="tabs">
<div class="c-tabs__list" role="tablist">
<button
type="button"
class="c-tabs__item c-tabs__item--active"
role="tab"
aria-selected="true"
aria-controls="panel-1"
id="tab-1"
>
곡μ§μ¬ν
</button>
<button
type="button"
class="c-tabs__item"
role="tab"
aria-selected="false"
aria-controls="panel-2"
id="tab-2"
tabindex="-1"
>
μ΄λ²€νΈ
</button>
</div>
<div
class="c-tabs__panel c-tabs__panel--active"
id="panel-1"
role="tabpanel"
aria-labelledby="tab-1"
>
<p>μ΅μ 곡μ§μ¬ν λ΄μ©μ
λλ€.</p>
</div>
<div
class="c-tabs__panel"
id="panel-2"
role="tabpanel"
aria-labelledby="tab-2"
hidden
>
<p>μ§ν μ€μΈ μ΄λ²€νΈκ° μμ΅λλ€.</p>
</div>
</div>
βοΈ Vanilla JS λ‘μ§ (Airbnb Style Guide μ€μ)
const initTabs = () => {
const tabContainers = document.querySelectorAll('[data-component="tabs"]');
tabContainers.forEach((container) => {
const tabs = container.querySelectorAll('[role="tab"]');
const panels = container.querySelectorAll('[role="tabpanel"]');
const switchTab = (event) => {
const targetTab = event.currentTarget;
const targetPanelId = targetTab.getAttribute('aria-controls');
tabs.forEach((tab) => {
tab.classList.remove('c-tabs__item--active');
tab.setAttribute('aria-selected', 'false');
tab.setAttribute('tabindex', '-1');
});
panels.forEach((panel) => {
panel.classList.remove('c-tabs__panel--active');
panel.setAttribute('hidden', '');
});
targetTab.classList.add('c-tabs__item--active');
targetTab.setAttribute('aria-selected', 'true');
targetTab.setAttribute('tabindex', '0');
const targetPanel = container.querySelector(`#${targetPanelId}`);
if (targetPanel) {
targetPanel.classList.add('c-tabs__panel--active');
targetPanel.removeAttribute('hidden');
}
};
tabs.forEach((tab) => {
tab.addEventListener('click', switchTab);
});
});
};
document.addEventListener('DOMContentLoaded', initTabs);
μ½λ λ°μμ λ리λ μμΈ μ€λͺ
μ μ½λλ μ¬μ©μκ° νμ ν΄λ¦νμ λ μκ°μ μΈ λ³νλΏλ§ μλλΌ λ³΄μ‘° 곡ν κΈ°κΈ°μλ μν λ³νλ₯Ό μλ²½νκ² μ λ¬νλλ‘ μ€κ³λμμ΅λλ€.
- Role λͺ
μ:
role="tablist",role="tab",role="tabpanel"μ ν΅ν΄ μ΄ κ΅¬μ‘°κ° ν μμ€ν μμ λΈλΌμ°μ μ λͺ νν μ립λλ€. - Aria μμ± μ μ΄: μλ°μ€ν¬λ¦½νΈλ‘ λ¨μν ν΄λμ€λ§ λ°κΎΈλ κ²μ΄ μλλΌ
aria-selectedκ°μtrue/falseλ‘ ν κΈν©λλ€. μ€ν¬λ¦° 리λλ μ΄ κ°μ μ½μ΄ μ¬μ©μμκ² "μ νλ¨" μ¬λΆλ₯Ό μλ €μ€λλ€. - ν¬μ»€μ€ κ΄λ¦¬: μ νλμ§ μμ νμλ
tabindex="-1"μ μ£Όμ΄ λΆνμν ν€λ³΄λ μ κ·Όμ λ§κ³ , νμ±νλ νμλ§ ν¬μ»€μ€κ° κ°λλ‘ κ΄λ¦¬νμ΅λλ€. - Hidden μμ± νμ©:
display: noneλμ HTML5 νμ€μΈhiddenμμ±μ ν κΈνμ¬ μκ°μ μΌλ‘λ ꡬ쑰μ μΌλ‘λ ν¨λμ μ¨κΉλλ€. - Vanilla JS & Airbnb Style: μΈλΆ λΌμ΄λΈλ¬λ¦¬ μμ‘΄μ±μ μ κ±°νκ³ νμ΄ν ν¨μ,
const,forEachλ± νλμ μΈ μλ°μ€ν¬λ¦½νΈ λ¬Έλ²μ μ¬μ©νμ¬ κΉλνκ³ λΉ λ₯Έ λ‘μ§μ ꡬμ±νμ΅λλ€.
λ§μΉλ©°: κΈ°μ λ³΄λ€ μ€μν κ²μ 'μ°κ²°'μ λλ€
μλ°μ€ν¬λ¦½νΈλ μΉμ μ¦κ²κ² λ§λ€μ§λ§, μλͺ» μ°λ©΄ λκ΅°κ°μκ²λ κ±°λν μ₯λ²½μ΄ λ©λλ€. WAI-ARIAλ κ·Έ μ₯λ²½μ νλ¬Όκ³ κΈ°μ κ³Ό μ¬μ©μλ₯Ό μ°κ²°ν΄μ£Όλ λ°λ»ν κ°μ΄λλΌμΈμ λλ€.
νλ €ν μ λλ©μ΄μ μ κ³ λ―ΌνκΈ° μ μ, "λ΄ μ½λκ° λ³΄μ΄μ§ μλ κ³³μμλ μΉμ νκ°?"λ₯Ό λ¨Όμ κ³ λ―Όνλ κ°λ°μκ° μ§μ§ μ€λ ₯ μλ κ°λ°μ μλκΉμ?
4λΆ ν΅μ¬ μμ½
- λμ μΈ UI λ³νλ λ°λμ
aria-selected,aria-expandedλ±μΌλ‘ μνλ₯Ό μλ €μΌ νλ€. roleμμ±μ ν΅ν΄ νκ·Έμ κΈ°λ³Έ μλ―Έλ₯Ό λμ΄μ μ€μ μν μ λΆμ¬νλ€.- λ°λλΌ μλ°μ€ν¬λ¦½νΈλ‘ ν΄λμ€μ ARIA μμ±μ λμμ μ μ΄νλ κ²μ΄ νμ€ μ½λ©μ ν΅μ¬μ΄λ€.
