ARIA 접근성 가이드

컴포넌트 패턴

정렬 가능한 표

<table>, <caption>, <th scope>로 데이터 표의 구조를 전달하고, 열 머리글 안의 정렬 버튼과 aria-sort로 현재 정렬 상태를 알리는 방법입니다.

언제 쓰나요

행과 열로 이루어진 데이터를 보여 줄 때 표를 씁니다. 사용자가 값을 비교하려고 특정 열을 기준으로 다시 늘어놓을 수 있게 하려면 정렬 기능을 더합니다.

네이티브 표를 먼저 쓰세요

<table>, <caption>, <thead>, <th>, <td>는 각각 table, caption, rowgroup, columnheader·rowheader, cell 역할을 이미 가지고 있습니다. 화면낭독기는 이 구조를 이용해 셀을 옮길 때마다 해당 행·열 머리글을 함께 읽어 줍니다. div에 ARIA 표 역할을 일일이 붙이는 방식은 CSS 그리드 등으로 어쩔 수 없을 때만 씁니다.

다음 경우에는 다른 방법이 알맞습니다.

  • 화면 배치(레이아웃)를 위해서는 표를 쓰지 않습니다. 레이아웃은 CSS로 만듭니다. 이미 레이아웃 표가 있다면 role="presentation"으로 표 의미를 없앱니다.
  • 셀 사이를 화살표 키로 이동하며 편집·선택하는 스프레드시트형 위젯이라면 grid 패턴이 필요합니다. 정렬만 하는 표는 grid가 아니라 일반 표입니다.

구조

  • <caption>: 표의 제목입니다. 표의 접근 가능한 이름이 되고, 화면낭독기가 표에 들어갈 때 읽습니다. 정렬할 수 있다는 안내도 여기에 넣을 수 있습니다.
  • <th scope="col">: 열 머리글(columnheader). 정렬 가능한 열은 머리글 안에 <button>을 넣습니다.
  • <th scope="row">: 행 머리글(rowheader). 각 행을 대표하는 값(예: 이름)을 행 머리글로 두면, 다른 열로 이동할 때 어떤 행인지 함께 들립니다.
  • aria-sort: 현재 정렬 기준인 열 머리글(th) 하나에만 ascending(오름차순) 또는 descending(내림차순)을 붙입니다. 버튼이 아니라 th에 붙입니다.
  • 정렬 방향을 나타내는 화살표 아이콘은 장식이므로 aria-hidden="true"로 숨깁니다. 방향 정보는 aria-sort가 전달합니다.
역할과 속성 관계
<table>
  <caption>도서 목록 <span class="sr-only">(열 머리글 버튼으로 정렬할 수 있습니다)</span></caption>
  <thead>
    <tr>
      <th scope="col" aria-sort="ascending">
        <button type="button">제목 <span aria-hidden="true">▲</span></button>
      </th>
      <th scope="col">
        <button type="button">출간 연도 <span aria-hidden="true">◆</span></button>
      </th>
    </tr>
  </thead>
  <tbody>
    <tr><th scope="row">가나다 이야기</th><td>2021</td></tr>
  </tbody>
</table>

정렬 버튼 이름에 상태를 넣지 마세요

버튼 이름은 열 이름(예: “제목”)으로 둡니다. “제목, 오름차순 정렬됨”처럼 상태를 이름에 섞으면 aria-sort와 중복되어 두 번 읽히거나, 버튼을 누를 때 일어날 일(다음 방향)과 현재 상태가 헷갈립니다.

키보드 동작

정렬 가능한 표는 표 자체에 특별한 키 조작이 없습니다. 정렬 버튼은 일반 버튼처럼 동작합니다. 셀 사이 이동은 화면낭독기의 표 탐색 기능(예: NVDA·JAWS의 Ctrl + Alt + 화살표)이 담당합니다.

키보드 동작
키동작
Tab / Shift + Tab정렬 버튼 사이를 이동합니다. 표 셀은 Tab 순서에 들어가지 않습니다.
Enter / Space포커스된 열을 기준으로 정렬합니다. 같은 열을 다시 누르면 방향이 바뀝니다.

셀에 tabindex를 주지 마세요

데이터 셀에 tabindex="0"을 주면 키보드 사용자가 셀마다 Tab을 눌러야 해서 오히려 불편합니다. 화면낭독기 사용자는 표 탐색 기능으로 셀을 읽습니다. 셀 단위 포커스 이동이 꼭 필요하다면 grid 패턴을 씁니다.

필요한 ARIA

필요한 ARIA
역할·속성붙이는 곳설명
table 등 표 역할-네이티브 표 요소가 이미 가지고 있으므로 따로 쓰지 않습니다.
aria-sort정렬 기준 열의 thascending / descending. 한 번에 한 머리글에만 둡니다. 정렬 기준이 아닌 열은 속성을 빼는 것이 좋습니다. other는 오름·내림이 아닌 정렬 방식일 때 씁니다.
button머리글 안 정렬 컨트롤네이티브 <button>을 씁니다. 이름은 열 이름입니다.
aria-live 또는 status표 밖의 상태 메시지 영역선택 사항. 정렬 후 “제목 기준 오름차순으로 정렬했습니다”를 알립니다. 일부 화면낭독기는 aria-sort 변경을 바로 읽지 않으므로 보완 수단으로 씁니다.
aria-describedbytable선택 사항. 표 아래의 주석이나 정렬 안내 문장을 연결합니다.

직접 해 보기

위는 div로 흉내 낸 잘못된 구현이고, 아래는 네이티브 표에 정렬 버튼을 넣은 올바른 구현입니다. 아래 표에서 Tab으로 머리글 버튼에 이동해 Enter를 눌러 보세요. 실시간 코드에서 aria-sort가 옮겨 가는 모습을 볼 수 있습니다.

잘못된 구현: div로 만든 표

제목출간 연도가격(원)
파도의 기록201916000
가을 정원202314500
나무의 시간202118000

머리글을 마우스로 클릭하면 정렬되지만, 키보드로는 닿을 수 없고 표 구조와 정렬 상태가 전달되지 않습니다.

올바른 구현

도서 목록 (열 머리글 버튼으로 정렬할 수 있습니다)
가을 정원202314,500
나무의 시간202118,000
파도의 기록201916,000

전체 코드

비권장비권장: 머리글 텍스트에 클릭만 연결
<table>
  <tr>
    <td onclick="sortBy(0)"><b>제목 ▲</b></td>
    <td onclick="sortBy(1)"><b>출간 연도</b></td>
  </tr>
  <tr><td>가을 정원</td><td>2023</td></tr>
</table>
권장권장: th 안의 버튼 + aria-sort
<table>
  <caption>도서 목록</caption>
  <thead>
    <tr>
      <th scope="col" aria-sort="ascending">
        <button type="button">제목<span aria-hidden="true">▲</span></button>
      </th>
      <th scope="col">
        <button type="button">출간 연도<span aria-hidden="true"></span></button>
      </th>
    </tr>
  </thead>
  <tbody>
    <tr><th scope="row">가을 정원</th><td>2023</td></tr>
  </tbody>
</table>
HTML
<table id="books">
  <caption>도서 목록 <span class="sr-only">(열 머리글 버튼으로 정렬할 수 있습니다)</span></caption>
  <thead>
    <tr>
      <th scope="col" aria-sort="ascending">
        <button type="button" data-type="text">제목<span class="icon" aria-hidden="true"></span></button>
      </th>
      <th scope="col">
        <button type="button" data-type="number">출간 연도<span class="icon" aria-hidden="true"></span></button>
      </th>
      <th scope="col">
        <button type="button" data-type="number">가격(원)<span class="icon" aria-hidden="true"></span></button>
      </th>
    </tr>
  </thead>
  <tbody>
    <tr><th scope="row">가을 정원</th><td>2023</td><td data-value="14500">14,500</td></tr>
    <tr><th scope="row">나무의 시간</th><td>2021</td><td data-value="18000">18,000</td></tr>
    <tr><th scope="row">파도의 기록</th><td>2019</td><td data-value="16000">16,000</td></tr>
  </tbody>
</table>
<p class="sr-only" role="status" id="sort-status"></p>
JavaScript
var table = document.getElementById('books');
var status = document.getElementById('sort-status');
var tbody = table.tBodies[0];
var headers = table.querySelectorAll('thead th');
var LABEL = { ascending: '오름차순', descending: '내림차순' };

function value(row, i, type) {
  var cell = row.children[i];
  var v = cell.getAttribute('data-value') || cell.textContent.trim();
  return type === 'number' ? Number(v) : v;
}

function sortBy(i) {
  var th = headers[i];
  var btn = th.querySelector('button');
  var type = btn.dataset.type;
  var dir = th.getAttribute('aria-sort') === 'ascending' ? 'descending' : 'ascending';

  var rows = Array.from(tbody.rows).sort(function (a, b) {
    var x = value(a, i, type), y = value(b, i, type);
    var r = type === 'number' ? x - y : String(x).localeCompare(String(y), 'ko');
    return dir === 'ascending' ? r : -r;
  });
  rows.forEach(function (row) { tbody.appendChild(row); });

  // aria-sort는 정렬 기준 열 하나에만
  headers.forEach(function (h) { h.removeAttribute('aria-sort'); });
  th.setAttribute('aria-sort', dir);

  // aria-sort 변경을 바로 읽지 않는 환경을 위한 보완 안내
  status.textContent = btn.textContent.trim() + ' 기준 ' + LABEL[dir] + '으로 정렬했습니다.';
}

headers.forEach(function (th, i) {
  var btn = th.querySelector('button');
  if (btn) btn.addEventListener('click', function () { sortBy(i); });
});
CSS: 상태를 속성 선택자로 표시
th[aria-sort="ascending"] .icon::before  { content: "▲"; }
th[aria-sort="descending"] .icon::before { content: "▼"; }
th:not([aria-sort]) .icon::before        { content: "↕"; opacity: .45; }

정렬 표시를 aria-sort 속성 선택자로 그리면 화면 표시와 보조기술에 전달되는 상태가 어긋날 일이 없습니다.

화면낭독기에서는

화면낭독기 안내 예시 (표현은 화면낭독기·브라우저·버전에 따라 조금씩 다릅니다)
상황들리는 안내 (예)
표로 이동도서 목록 (열 머리글 버튼으로 정렬할 수 있습니다), 표, 4행 3열
정렬 기준 열의 버튼에 포커스제목, 열 머리글, 오름차순 정렬됨, 버튼
“출간 연도” 버튼을 눌러 정렬출간 연도 기준 오름차순으로 정렬했습니다. (상태 메시지)
표 탐색으로 “가격” 열의 셀로 이동가격(원), 나무의 시간, 18,000 (열·행 머리글과 함께 읽힘)
잘못된 구현(div)에 도달제목 출간 연도 가격(원) 파도의 기록 2019 16000 … (표 구조 없이 텍스트만 이어서 읽힘)

점검 체크리스트

  • 데이터 표를 <table>로 만들었고, 레이아웃에는 표를 쓰지 않았습니다.
  • <caption>으로 표 제목을 주었습니다.
  • 열 머리글은 <th scope="col">, 행 머리글은 <th scope="row">입니다.
  • 정렬 컨트롤은 th 안의 <button>이며, 이름은 열 이름입니다.
  • aria-sort는 현재 정렬 기준인 th 하나에만 있고, 정렬할 때마다 옮겨집니다.
  • 정렬 방향 아이콘은 aria-hidden="true"이고, 색이 아닌 모양으로도 방향을 구분할 수 있습니다.
  • 정렬 후 포커스가 누른 버튼에 그대로 남습니다.
  • (선택) 정렬 결과를 상태 메시지로 알립니다.
  • 좁은 화면에서도 표가 가로 스크롤 영역 안에서 읽힙니다(WCAG 1.4.10).

관련 WCAG 성공 기준

참고 자료