ARIA를 배운 개발자들이 자주 하는 실수가 있습니다. ARIA의 개념은 이해했지만, 실제 프로젝트에서 어떻게 적용할지, 언제 써야 하는지 확실하지 않은 거죠.

ARIA 실전 가이드 - 코드 에디터 화면에 ARIA 속성이 강조된 메인 비주얼
ARIA 실전 가이드 - 코드 에디터 화면에 ARIA 속성이 강조된 메인 비주얼
커버 이미지(예시): ARIA 속성 적용 흐름을 상징하는 비주얼 · 제작: Nanobanana AI

“ARIA는 마지막 수단이다"라는 말이 있습니다. 먼저 시맨틱 HTML을 사용하고, HTML의 기본 기능으로 부족할 때만 ARIA를 더하는 거예요. 이 가이드는 이 원칙을 바탕으로 실제 프로젝트에서 ARIA를 효과적으로 활용하는 방법을 다룹니다.

1. ARIA의 황금률: 언제 써야 하고, 언제 쓰면 안 되는가?

1.1 ARIA를 쓰지 말아야 할 때

많은 개발자가 ARIA를 과다 사용합니다. 먼저 ARIA가 필요 없는 경우를 확인해보세요.

ARIA 사용 시 피해야 할 실수와 권장 사항을 비교한 다이어그램 - 왼쪽은 잘못된 사용법, 오른쪽은 올바른 사용법
ARIA 사용 시 피해야 할 실수와 권장 사항을 비교한 다이어그램 - 왼쪽은 잘못된 사용법, 오른쪽은 올바른 사용법
ARIA 사용 전후 비교(잘못된 예 vs 올바른 예) · 제작: Nanobanana AI
html
<!-- ❌ 나쁜 예: 불필요한 ARIA -->
<button role="button" aria-label="클릭">클릭</button>
<!-- <button>은 이미 button이므로 role 중복 -->

<!-- ❌ 나쁜 예: 시맨틱 HTML이 존재하는데 ARIA 사용 -->
<div role="heading" aria-level="1">제목</div>
<!-- <h1>제목</h1>을 쓰면 ARIA 불필요 -->

<!-- ❌ 나쁜 예: aria-label이 필요 없는 경우 -->
<button aria-label="저장">
  <i class="icon-save"></i> 저장
</button>
<!-- 시각적 텍스트가 이미 있으면 aria-label 불필요 -->

<!-- ✅ 좋은 예: 아무 ARIA 없음 (시맨틱만으로 충분) -->
<button>저장</button>
<h1>제목</h1>
<form>
  <label for="email">이메일</label>
  <input id="email" type="email">
</form>

ARIA 없이 충분한 엘리먼트들:

  • 버튼: <button>
  • 링크: <a>
  • 폼: <form>, <input>, <textarea>, <select>
  • 제목: <h1> ~ <h6>
  • 탐색: <nav>
  • 메인: <main>
  • 아티클: <article>
  • 섹션: <section>

1.2 ARIA를 써야 할 때

ARIA는 다음과 같은 경우에 필요합니다:

  1. 시맨틱 HTML이 없을 때: 커스텀 위젯이나 특수한 상황이에요
  2. 추가 정보 제공: HTML의 기본 정보만으로는 부족할 때
  3. 상태 변화 알림: 동적으로 변하는 콘텐츠를 스크린 리더에 알릴 때
html
<!-- ✅ 필요한 경우 1: 커스텀 위젯 -->
<div class="custom-tabs">
  <div role="tablist">
    <button role="tab" aria-selected="true" aria-controls="panel-1" tabindex="0">
      탭 1
    </button>
    <button role="tab" aria-selected="false" aria-controls="panel-2" tabindex="-1">
      탭 2
    </button>
  </div>
  <div id="panel-1" role="tabpanel">내용 1</div>
  <div id="panel-2" role="tabpanel" hidden>내용 2</div>
</div>

<!-- ✅ 필요한 경우 2: 추가 정보 제공 -->
<button aria-label="차트 열기">📊</button>
<!-- 아이콘만 있는 경우 aria-label이 필수 -->

<!-- ✅ 필요한 경우 3: 상태 변화 알림 -->
<span aria-live="polite" aria-atomic="true" id="notifications"></span>
<script>
  // 사용자 액션 후 동적으로 메시지 추가
  document.getElementById('notifications').textContent = '저장되었습니다.';
</script>

2. ARIA 속성 실전 패턴

ARIA 역할과 속성의 계층 구조를 보여주는 다이어그램 - 위젯 역할, 라이브 리전 역할, 구조 역할을 시각적으로 표현
ARIA 역할과 속성의 계층 구조를 보여주는 다이어그램 - 위젯 역할, 라이브 리전 역할, 구조 역할을 시각적으로 표현
ARIA role 유형을 계층적으로 정리한 다이어그램 · 제작: Nanobanana AI

2.1 라벨링: aria-label vs aria-labelledby

이 두 속성은 **“이 요소가 무엇인지”**를 스크린 리더에 알려줍니다.

  • aria-label짧은 텍스트를 직접 지정할 때 유용합니다(아이콘 버튼 등).
  • aria-labelledby화면에 이미 있는 텍스트를 참조해 라벨을 연결할 때 적합합니다. 시각적 라벨과 읽히는 라벨을 일치시키는 것이 핵심입니다.
html
<!-- 1. aria-label: 간단한 라벨 제공 -->
<button aria-label="닫기">×</button>

<!-- 2. aria-labelledby: 페이지의 다른 엘리먼트를 라벨로 참고 -->
<h2 id="dialog-title">사용자 설정</h2>
<div role="dialog" aria-labelledby="dialog-title">
  <!-- 다이얼로그 내용 -->
</div>

<!-- 3. 라벨 결합: 여러 엘리먼트를 조합 -->
<span id="search-label">상품 검색</span>
<span id="search-hint">(상품명, 카테고리)</span>
<input
  aria-labelledby="search-label search-hint"
  placeholder="검색어 입력"
>

<!-- 4. 숨겨진 라벨 -->
<!-- 스크린 리더만 읽는 라벨 클래스 -->
<style>
  .sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    border: 0;
  }
</style>

<button>
  <span class="sr-only">이전 페이지로</span>
</button>

2.2 설명 및 힌트: aria-describedby

aria-describedby는 라벨과 달리 **“추가 설명”**을 연결합니다. 입력 힌트, 오류 메시지, 부가 정보를 스크린 리더가 라벨 다음에 읽도록 만들 때 사용합니다. 라벨과 설명을 구분해 전달하면 정보 구조가 명확해집니다.

html
<!-- 입력 필드에 추가 설명 -->
<label for="password">비밀번호</label>
<input
  id="password"
  type="password"
  aria-describedby="pwd-requirements pwd-strength"
>
<div id="pwd-requirements" class="hint">
  최소 8자, 대문자, 숫자, 특수문자(@, #, $) 포함
</div>
<div id="pwd-strength" aria-live="polite">
  강도: 약함
</div>

<!-- 복잡한 이미지에 설명 -->
<img
  src="chart.png"
  alt="월별 판매량"
  aria-describedby="chart-desc"
>
<p id="chart-desc">
  2024년 1월부터 12월까지의 판매량.
  최고점은 7월의 15,000개, 최저점은 2월의 5,000개
</p>

<!-- 폼 필드의 에러 메시지 -->
<label for="email">이메일</label>
<input id="email" type="email" aria-describedby="email-error">
<span id="email-error" role="alert">
  올바른 이메일 형식이 아닙니다.
</span>

2.3 상태 전달: aria-checked, aria-pressed, aria-current

이 속성들은 UI의 현재 상태를 보조기술에 전달합니다. 핵심은 “시각적으로 보이는 변화(체크됨, 눌림, 현재 위치 등)를 스크린 리더도 동일하게 인지”하도록 만드는 것입니다. 네이티브 요소는 상태가 자동으로 노출되므로, 보통은 커스텀 위젯에서만 직접 관리합니다.

html
<!-- 1. 체크박스/라디오 상태 -->
<label>
  <input type="checkbox">
  약관에 동의합니다
</label>

<!-- 네이티브 체크박스/라디오는 상태를 자동 노출하므로 aria-checked를 수동 설정하지 않습니다. 커스텀 위젯일 때만 aria-checked를 관리하세요. -->

<!-- 2. 토글 버튼 상태 -->
<button
  aria-pressed="false"
  aria-label="음소거"
  id="mute-btn"
>
  🔊
</button>

<script>
  const muteBtn = document.getElementById('mute-btn');
  muteBtn.addEventListener('click', function() {
    const isPressed = this.getAttribute('aria-pressed') === 'true';
    this.setAttribute('aria-pressed', !isPressed);
    // 음소거 토글 로직
  });
</script>

<!-- 3. 현재 페이지 표시 (네비게이션) -->
<nav>
  <a href="/products">상품</a>
  <a href="/blog" aria-current="page">블로그</a> <!-- 현재 페이지 -->
  <a href="/about">소개</a>
</nav>

<!-- 4. 트리 구조의 열기/닫기 상태 -->
<button aria-expanded="false" aria-controls="submenu">
  카테고리
</button>
<ul id="submenu" hidden>
  <li><a href="#">서브메뉴 1</a></li>
  <li><a href="#">서브메뉴 2</a></li>
</ul>

<script>
  document.querySelector('button').addEventListener('click', function() {
    const isExpanded = this.getAttribute('aria-expanded') === 'true';
    this.setAttribute('aria-expanded', !isExpanded);
    document.getElementById('submenu').hidden = isExpanded;
  });
</script>

정리하면

  • aria-checked: 선택/체크 상태를 표현합니다. 체크박스, 스위치, 라디오의 커스텀 구현에 사용합니다.
  • aria-pressed: 토글 버튼의 켜짐/꺼짐 상태를 표현합니다.
  • aria-current: “현재 위치”를 알려줍니다. 보통 네비게이션에서 현재 페이지를 표시할 때 사용합니다.
  • aria-expanded: 열림/닫힘 상태를 알립니다. 아코디언, 드롭다운, 트리 등의 확장 상태를 표현합니다.

3. 라이브 리전: 동적 콘텐츠 알림

라이브 리전은 페이지를 새로고침하지 않고 콘텐츠가 변할 때 스크린 리더 사용자에게 알려줍니다.

aria-live 속성을 사용한 실시간 업데이트와 동적 알림을 시각화한 다이어그램
aria-live 속성을 사용한 실시간 업데이트와 동적 알림을 시각화한 다이어그램
라이브 리전 알림 흐름 요약 · 제작: Nanobanana AI

3.1 aria-live 속성

aria-live화면이 바뀔 때 자동으로 읽어줄 영역을 지정합니다. 페이지 전환 없이 텍스트가 바뀌는 상황(저장 완료, 에러, 검색 결과 업데이트 등)에서 스크린 리더 사용자도 변화를 즉시 인지할 수 있도록 해줍니다. 핵심은 필요한 곳에만, 최소한으로 사용하는 것입니다.

html
<!-- 1. aria-live="polite" (기본) -->
<!-- 사용자가 현재 작업을 완료한 후 변경사항 알림 -->
<div id="notifications" aria-live="polite">
  <!-- 메시지가 여기에 추가됨 -->
</div>

<!-- 2. aria-live="assertive" -->
<!-- 즉시 변경사항 알림 (남용하면 방해됨, 긴급 상황에만) -->
<div id="error-alert" aria-live="assertive">
  <!-- 즉각적인 에러 메시지 -->
</div>

<!-- 3. aria-atomic="true" -->
<!-- 변경된 부분만이 아니라 전체 콘텐츠를 읽음 -->
<div aria-live="polite" aria-atomic="true" id="search-results">
  <p>검색 결과: 23개</p>
  <ul>
    <li>상품 1</li>
    <li>상품 2</li>
  </ul>
</div>

<!-- 4. aria-relevant="additions text" -->
<!-- 어떤 변경사항을 알릴지 지정 -->
<div aria-live="polite" aria-relevant="additions text">
  <!-- 추가된 항목과 텍스트 변경만 알림 -->
</div>

주의할 점: aria-live="assertive"는 긴급 알림(에러, 보안 경고)에서만 사용하고, 대부분의 상태 메시지는 polite로 두세요. 너무 자주 알림을 보내면 사용자를 방해할 수 있거든요.

3.2 실제 사용 예제: 실시간 검색

실시간 검색처럼 입력할 때마다 결과가 바뀌는 UI는 시각적으로는 직관적이지만, 스크린 리더 사용자에게는 변화가 전달되지 않습니다. role="status"aria-live를 통해 결과 업데이트를 알려주면 동등한 피드백을 제공할 수 있습니다.

실시간 검색 기능에서 aria-live를 사용하여 검색 결과를 스크린 리더에 알리는 과정을 보여주는 예시
실시간 검색 기능에서 aria-live를 사용하여 검색 결과를 스크린 리더에 알리는 과정을 보여주는 예시
실시간 검색 결과 알림 예시 · 제작: Nanobanana AI
html
<div class="search-container">
  <input
    id="search-input"
    type="text"
    placeholder="검색..."
    aria-describedby="search-help"
  >
  <span id="search-help" class="sr-only">
    입력하면 실시간으로 검색 결과가 표시됩니다.
  </span>
</div>

<!-- 검색 결과 라이브 리전 -->
<div id="search-results" role="status">
  <!-- 결과가 여기에 표시됨 -->
</div>

<script>
  const input = document.getElementById('search-input');
  const resultsDiv = document.getElementById('search-results');

  input.addEventListener('input', function(e) {
    const query = e.target.value.trim();

    if (!query) {
      resultsDiv.innerHTML = '';
      return;
    }

    // API 호출 (예시)
    fetch(`/api/search?q=${query}`)
      .then(res => res.json())
      .then(data => {
        // 결과 업데이트 - aria-live가 변경을 감지
        resultsDiv.innerHTML = `
          <p>
            ${data.count}개 결과를 찾았습니다.
          </p>
          <ul>
            ${data.items.map(item => `<li>${item.name}</li>`).join('')}
          </ul>
        `;
      });
  });
</script>

4. 커스텀 위젯 구현

시맨틱 HTML만으로는 해결할 수 없는 복잡한 인터페이스를 만들 때 ARIA를 사용합니다.

커스텀 위젯 패턴 - 탭, 아코디언, 드롭다운, 모달의 ARIA 구현을 시각적으로 보여주는 다이어그램
커스텀 위젯 패턴 - 탭, 아코디언, 드롭다운, 모달의 ARIA 구현을 시각적으로 보여주는 다이어그램
대표 위젯 패턴 요약(탭/아코디언/드롭다운/모달) · 제작: Nanobanana AI

4.1 탭 위젯

탭은 대표적인 커스텀 위젯입니다. role="tablist", role="tab", role="tabpanel" 조합으로 구조를 만들고, 키보드 이동 규칙(좌/우 화살표, Home/End)을 구현해야 접근성이 완성됩니다.

탭 위젯의 ARIA 구조와 키보드 네비게이션을 상세히 설명하는 다이어그램
탭 위젯의 ARIA 구조와 키보드 네비게이션을 상세히 설명하는 다이어그램
탭 위젯의 role/aria 연결과 키보드 흐름 · 제작: Nanobanana AI
html
<div class="tabs">
  <div role="tablist">
    <button
      role="tab"
      aria-selected="true"
      aria-controls="panel-1"
      tabindex="0"
      id="tab-1"
    >
      기본 정보
    </button>
    <button
      role="tab"
      aria-selected="false"
      aria-controls="panel-2"
      tabindex="-1"
      id="tab-2"
    >
      상세 정보
    </button>
  </div>

  <div id="panel-1" role="tabpanel" aria-labelledby="tab-1">
    기본 정보 내용
  </div>
  <div id="panel-2" role="tabpanel" aria-labelledby="tab-2" hidden>
    상세 정보 내용
  </div>
</div>

<style>
  [role="tab"][aria-selected="false"] {
    opacity: 0.6;
  }
</style>

<script>
  const tabs = document.querySelectorAll('[role="tab"]');
  const panels = document.querySelectorAll('[role="tabpanel"]');

  tabs.forEach(tab => {
    tab.addEventListener('click', function() {
      // 모든 탭 비활성화
      tabs.forEach(t => {
        t.setAttribute('aria-selected', 'false');
        t.setAttribute('tabindex', '-1');
      });
      panels.forEach(p => p.hidden = true);

      // 클릭된 탭 활성화
      this.setAttribute('aria-selected', 'true');
      this.setAttribute('tabindex', '0');
      const panelId = this.getAttribute('aria-controls');
      document.getElementById(panelId).hidden = false;
    });

    // 키보드 네비게이션
    tab.addEventListener('keydown', function(e) {
      let nextTab;
      if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
        nextTab = this.nextElementSibling || tabs[0];
      } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
        nextTab = this.previousElementSibling || tabs[tabs.length - 1];
      } else if (e.key === 'Home') {
        nextTab = tabs[0];
      } else if (e.key === 'End') {
        nextTab = tabs[tabs.length - 1];
      }

      if (nextTab) {
        e.preventDefault();
        nextTab.click();
        nextTab.setAttribute('tabindex', '0');
        this.setAttribute('tabindex', '-1');
        nextTab.focus();
      }
    });
  });
</script>

4.2 아코디언 위젯

아코디언은 펼침/닫힘 상태가 핵심입니다. aria-expanded로 상태를 노출하고, 각 패널을 role="region"aria-labelledby로 연결하면 스크린 리더에서 구조가 명확해집니다.

html
<div class="accordion">
  <div class="accordion-item">
    <button
      aria-expanded="false"
      aria-controls="section-1"
      id="accordion-1"
      class="accordion-btn"
    >
      자주 묻는 질문 1
    </button>
    <div
      id="section-1"
      role="region"
      aria-labelledby="accordion-1"
      hidden
      class="accordion-content"
    >
      <p>답변 내용 1</p>
    </div>
  </div>

  <div class="accordion-item">
    <button
      aria-expanded="false"
      aria-controls="section-2"
      id="accordion-2"
      class="accordion-btn"
    >
      자주 묻는 질문 2
    </button>
    <div
      id="section-2"
      role="region"
      aria-labelledby="accordion-2"
      hidden
      class="accordion-content"
    >
      <p>답변 내용 2</p>
    </div>
  </div>
</div>

<style>
  .accordion-content {
    max-height: 0;
    overflow: hidden;
    transition: max-height 0.3s ease;
  }

  .accordion-content:not([hidden]) {
    max-height: 500px;
  }

  button[aria-expanded="true"]::after {
    content: '▼';
  }

  button[aria-expanded="false"]::after {
    content: '▶';
  }
</style>

<script>
  document.querySelectorAll('.accordion-btn').forEach(btn => {
    btn.addEventListener('click', function() {
      const isExpanded = this.getAttribute('aria-expanded') === 'true';
      this.setAttribute('aria-expanded', !isExpanded);
      document.getElementById(this.getAttribute('aria-controls')).hidden = isExpanded;
    });
  });
</script>

4.3 드롭다운 메뉴

드롭다운은 토글 버튼 + 목록 구조입니다. 일반적인 내비게이션이라면 단순한 button + ul 패턴이 가장 안전합니다. 필요할 때만 복잡한 menu 역할을 사용하세요.

html
<div class="dropdown">
  <button
    id="menu-btn"
    aria-expanded="false"
    aria-controls="menu-list"
  >
    메뉴
  </button>

  <ul id="menu-list" hidden aria-labelledby="menu-btn">
    <li><a href="#">옵션 1</a></li>
    <li><a href="#">옵션 2</a></li>
    <li><a href="#">옵션 3</a></li>
  </ul>
</div>

<script>
  const btn = document.getElementById('menu-btn');
  const menu = document.getElementById('menu-list');
  const items = menu.querySelectorAll('a');

  btn.addEventListener('click', () => {
    const isExpanded = btn.getAttribute('aria-expanded') === 'true';
    btn.setAttribute('aria-expanded', !isExpanded);
    menu.hidden = isExpanded;

    if (!isExpanded) {
      items[0].focus();
    }
  });

  // Escape 키로 메뉴 닫기
  menu.addEventListener('keydown', (e) => {
    if (e.key === 'Escape') {
      btn.click();
      btn.focus();
    }
  });

  // 외부 클릭으로 메뉴 닫기
  document.addEventListener('click', (e) => {
    if (!e.target.closest('.dropdown')) {
      menu.hidden = true;
      btn.setAttribute('aria-expanded', 'false');
    }
  });
</script>

5. 모달/다이얼로그 패턴

모달은 포커스 관리가 가장 중요합니다. 열릴 때 포커스를 모달 안으로 이동하고, 닫힐 때 원래 위치로 복귀해야 합니다. aria-modal="true"로 보조기술에 “이 영역이 현재 상호작용 대상”임을 알립니다.

모달 다이얼로그의 포커스 관리와 포커스 트랩을 시각화한 다이어그램 - 포커스 이동 경로와 aria-modal 속성을 표시
모달 다이얼로그의 포커스 관리와 포커스 트랩을 시각화한 다이어그램 - 포커스 이동 경로와 aria-modal 속성을 표시
모달 포커스 관리 흐름(열기/닫기/복귀) · 제작: Nanobanana AI
html
<button id="open-modal">모달 열기</button>

<div
  id="modal"
  role="dialog"
  aria-labelledby="modal-title"
  aria-modal="true"
  hidden
>
  <div class="modal-content">
    <h2 id="modal-title">중요한 정보</h2>

    <p>여기는 모달의 내용입니다.</p>

    <button id="close-modal">닫기</button>
  </div>
</div>

<style>
  #modal:not([hidden]) {
    display: flex;
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, 0.5);
  }

  .modal-content {
    margin: auto;
    background: white;
    padding: 2rem;
    border-radius: 8px;
  }
</style>

<script>
  const modal = document.getElementById('modal');
  const openBtn = document.getElementById('open-modal');
  const closeBtn = document.getElementById('close-modal');
  let previousFocus = null;

  openBtn.addEventListener('click', () => {
    previousFocus = document.activeElement;
    modal.hidden = false;
    // 모달 밖 요소만 inert 처리해 포커스가 밖으로 나가지 않게 함
    const siblings = Array.from(document.body.children).filter(el => el !== modal);
    siblings.forEach(el => (el.inert = true));

    // 포커스를 모달로 이동
    const focusableElements = modal.querySelectorAll(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    );
    focusableElements[0]?.focus();

    // 모달 내에서만 포커스 이동 가능 (포커스 트래핑)
    focusableElements[focusableElements.length - 1].addEventListener('keydown', (e) => {
      if (e.key === 'Tab' && !e.shiftKey) {
        e.preventDefault();
        focusableElements[0].focus();
      }
    });

    focusableElements[0].addEventListener('keydown', (e) => {
      if (e.key === 'Tab' && e.shiftKey) {
        e.preventDefault();
        focusableElements[focusableElements.length - 1].focus();
      }
    });
  });

  closeBtn.addEventListener('click', () => {
    modal.hidden = true;
    const siblings = Array.from(document.body.children).filter(el => el !== modal);
    siblings.forEach(el => (el.inert = false));
    previousFocus?.focus();
  });

  // Escape 키로 닫기
  document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape' && !modal.hidden) {
      closeBtn.click();
    }
  });
</script>

6. ARIA와 접근성 검증

6.1 흔한 ARIA 실수

ARIA는 강력하지만, 잘못 쓰면 접근성을 오히려 악화시킵니다. 아래 실수들은 현장에서 가장 자주 발생하는 패턴들이니, 코드 리뷰나 QA에서 꼭 확인하세요.

html
<!-- ❌ 실수 1: 역할 오용 -->
<div role="button" onclick="alert('클릭')">클릭하세요</div>
<!-- 문제: 키보드 포커스 없음, 키보드 네비게이션 불가 -->

<!-- ✅ 올바른 방법 -->
<button onclick="alert('클릭')">클릭하세요</button>

<!-- ❌ 실수 2: 시각적 라벨 없이 aria-label만 사용 -->
<input aria-label="검색" placeholder="검색어 입력">
<!-- 문제: 화면에는 라벨이 없고, 보조기술에만 라벨이 전달됨 -->

<!-- ✅ 올바른 방법 -->
<label for="search">검색</label>
<input id="search" placeholder="검색어 입력">

<!-- ❌ 실수 3: 부정확한 라이브 리전 -->
<div aria-live="assertive" id="log">
  로그 메시지들...
</div>
<!-- 문제: 모든 메시지를 읽음, 성능 저하 -->

<!-- ✅ 올바른 방법 -->
<div aria-live="polite" aria-atomic="false" id="notification">
  <!-- 최신 메시지만 추가 -->
</div>

<!-- ❌ 실수 4: 역순 접근 가능 엘리먼트들 -->
<div role="tablist">
  <div role="tab" tabindex="0">첫 번째</div>
  <div role="tab">두 번째</div> <!-- tabindex 없음 -->
  <div role="tab" tabindex="0">세 번째</div>
</div>
<!-- 문제: 일관성 없는 포커스 순서 -->

<!-- ✅ 올바른 방법: 첫 탭만 tabindex="0", 나머지는 -1 -->
<div role="tablist">
  <div role="tab" tabindex="0">첫 번째</div>
  <div role="tab" tabindex="-1">두 번째</div>
  <div role="tab" tabindex="-1">세 번째</div>
</div>

6.2 ARIA 검증 체크리스트

언제 ARIA를 쓸까?

  • 시맨틱 HTML로는 표현 불가능한가?
  • 커스텀 위젯인가?
  • 스크린 리더 사용자에게 중요한 정보인가?

ARIA 사용 시 확인사항

  • role, aria-label/labelledby, aria-describedby 모두 설정했는가?
  • 키보드 네비게이션이 작동하는가?
  • 상태 변화를 aria-* 속성으로 반영하는가?
  • 자동 포커스 관리가 있는가? (예: 모달 열 때)
  • aria-live를 과도하게 사용하지는 않았는가?

테스트

  • 스크린 리더 (NVDA, JAWS, VoiceOver) 테스트
  • 키보드만으로 모든 기능 접근 가능한가?
  • axe DevTools, Lighthouse 검증
  • WAI-ARIA 패턴 라이브러리와 비교

7. ARIA 사용 팁과 베스트 프랙티스

다양한 커스텀 위젯의 키보드 네비게이션 패턴을 정리한 가이드 - 탭, 메뉴, 아코디언, 드롭다운의 키보드 단축키
다양한 커스텀 위젯의 키보드 네비게이션 패턴을 정리한 가이드 - 탭, 메뉴, 아코디언, 드롭다운의 키보드 단축키
위젯별 키보드 조작 요약 · 제작: Nanobanana AI

7.1 role 사용 팁

role은 시맨틱이 없는 요소에 의미를 부여하기 위한 장치입니다. 네이티브 요소에 role을 중복하면 혼란을 줄 수 있으니, “필요할 때만” 사용하는 것이 원칙입니다.

html
<!-- 1. 네이티브 엘리먼트에는 역할을 중복하지 않기 -->
<button>좋음</button>
<div role="button">필요할 때만 사용</div>

<!-- 2. 역할 변경보다 엘리먼트 교체가 나음 -->
<!-- ❌ 안 좋음 -->
<div id="button" role="button">클릭</div>
<script>
  document.getElementById('button').setAttribute('role', 'link');
</script>

<!-- ✅ 좋음 -->
<button>클릭</button>

<!-- 3. 복합 역할은 명시적으로 -->
<div role="tablist">
  <div role="tab">탭 1</div>
  <div role="tabpanel">콘텐츠 1</div>
</div>

7.2 ARIA와 CSS

ARIA 상태를 스타일과 연결하면 시각적 상태와 접근성 상태를 일치시킬 수 있습니다. 예를 들어 aria-pressed="true"일 때 색상을 변경하면 사용자 경험이 더 일관됩니다.

css
/* ARIA 상태로 스타일링 - 접근성 정보와 시각 표현 동기화 */
button[aria-pressed="true"] {
  background-color: #4A90E2;
  color: white;
}

button[aria-pressed="false"] {
  background-color: #f0f0f0;
  color: #333;
}

/* 포커스 표시 */
[role="button"]:focus {
  outline: 3px solid #4A90E2;
  outline-offset: 2px;
}

/* 라이브 리전 변경 시각화 (선택사항) */
[aria-live="polite"] {
  border-left: 3px solid #27AE60;
  padding-left: 0.5rem;
}

/* 숨겨진 탭 스타일 */
[role="tabpanel"][hidden] {
  display: none;
}

7.3 성능 고려사항

라이브 리전은 작은 변경에도 큰 읽기 비용이 발생할 수 있습니다. 한 번에 많은 업데이트를 넣거나 과도하게 메시지를 누적하면 성능과 사용자 경험이 모두 나빠집니다. 가능한 배치 처리와 최신 상태 유지 전략을 쓰세요.

javascript
// ❌ 나쁜 예: 모든 변경사항을 aria-live에 추가
const notifications = document.getElementById('notifications');
for (let i = 0; i < 1000; i++) {
  notifications.innerHTML += `<p>알림 ${i}</p>`;
}

// ✅ 좋은 예: 배치 업데이트
const notifications = document.getElementById('notifications');
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
  const p = document.createElement('p');
  p.textContent = `알림 ${i}`;
  fragment.appendChild(p);
}
notifications.appendChild(fragment);

// ✅ 좋은 예: 최신 메시지만 유지
const notification = document.getElementById('notification');
function showNotification(message) {
  notification.innerHTML = message;
  // 5초 후 제거
  setTimeout(() => {
    notification.innerHTML = '';
  }, 5000);
}

8. 실제 프로젝트에서의 ARIA

8.1 라이브러리와 프레임워크

많은 현대적 프레임워크가 ARIA 지원을 내장하고 있어요:

javascript
// React 예제
function Tabs() {
  const [selected, setSelected] = useState(0);

  return (
    <div role="tablist">
      {tabs.map((tab, i) => (
        <button
          key={i}
          role="tab"
          aria-selected={i === selected}
          aria-controls={`panel-${i}`}
          onClick={() => setSelected(i)}
        >
          {tab.label}
        </button>
      ))}
      {tabs.map((tab, i) => (
        <div
          key={i}
          id={`panel-${i}`}
          role="tabpanel"
          aria-labelledby={`tab-${i}`}
          hidden={i !== selected}
        >
          {tab.content}
        </div>
      ))}
    </div>
  );
}

8.2 ARIA 패턴 라이브러리

WAI-ARIA Authoring Practices는 공식 패턴 모음입니다. 바퀴를 다시 발명하지 말고 검증된 패턴을 참고하세요:

  • 버튼, 링크 패턴
  • 폼 입력 패턴
  • 네비게이션 패턴
  • 탭, 아코디언, 메뉴
  • 다이얼로그, 알럿 박스
  • 그리드, 테이블 패턴

결론

ARIA는 강력한 도구지만, 책임감 있게 사용해야 합니다. 몇 가지만 기억해주세요:

  1. 먼저 시맨틱 HTML 사용: HTML의 기본 기능을 최대한 활용하세요
  2. HTML이 부족할 때만 ARIA 추가: 꼭 필요한 곳에만 사용하세요
  3. 키보드 네비게이션 필수: 모든 기능이 키보드만으로 작동해야 합니다
  4. 스크린 리더로 테스트: 실제 사용자 경험을 확인하세요
  5. 포커스 관리와 상태 추적: 동적 변화를 사용자에게 알려주세요

접근 가능한 인터페이스는 모든 사용자에게 더 좋은 경험을 제공합니다. ARIA를 올바르게 사용하면 보조 기술 사용자뿐만 아니라 모든 사용자가 이익을 얻을 수 있어요.