도입 스토리
"주문 내역 표를 스크린리더로 읽으니까 숫자만 나열되네요." 김개발이 말했습니다.
NVDA로 표를 탐색하면 이렇게 들렸습니다.
"12345. 20260101. 노트북. 1500000. 완료."
어느 것이 주문번호이고, 어느 것이 날짜인지 알 수 없었습니다.
"<th> 요소와 scope 속성, caption이 없어서예요." 박멘토가 설명했습니다. "제대로 된 표는 스크린리더가 셀을 읽을 때 열 제목과 행 제목을 함께 읽어줘요."
핵심 개념 설명
기본 접근성 표 구조
<table> <!-- 표 제목 --> <caption>2026년 1월 주문 내역</caption> <thead> <tr> <th scope="col">주문번호</th> <th scope="col">주문일</th> <th scope="col">상품명</th> <th scope="col">금액</th> <th scope="col">상태</th> </tr> </thead> <tbody> <tr> <td>12345</td> <td>2026.01.01</td> <td>노트북 Pro</td> <td>1,500,000원</td> <td>배송 완료</td> </tr> <tr> <td>12346</td> <td>2026.01.05</td> <td>무선 키보드</td> <td>89,000원</td> <td>배송 중</td> </tr> </tbody></table>
scope="col"이 있으면 스크린리더가 각 셀을 읽을 때 "주문번호 열 12345", "주문일 열 2026.01.01" 등으로 열 제목을 함께 읽어줍니다.
scope 속성
| 값 | 의미 | 사용 위치 |
|---|---|---|
col |
열 제목 | <th>에서 해당 열 전체의 제목 |
row |
행 제목 | <th>에서 해당 행 전체의 제목 |
colgroup |
열 그룹 제목 | <colgroup>을 사용하는 경우 |
rowgroup |
행 그룹 제목 | <thead>, <tbody>, <tfoot> 구분 |
<!-- 행 제목이 있는 표 (시간표 같은 형태) --><table> <caption>부서별 분기 매출</caption> <thead> <tr> <th scope="col">부서</th> <th scope="col">1분기</th> <th scope="col">2분기</th> <th scope="col">3분기</th> </tr> </thead> <tbody> <tr> <th scope="row">영업팀</th> <!-- 행 제목 --> <td>120만원</td> <td>145만원</td> <td>160만원</td> </tr> <tr> <th scope="row">개발팀</th> <td>—</td> <td>—</td> <td>—</td> </tr> </tbody></table>
복잡한 표 — headers 속성
행과 열이 중첩되는 복잡한 표는 headers 속성으로 직접 연결합니다.
<table> <caption>2026년 영업팀 지역별/분기별 매출</caption> <thead> <tr> <th id="th-blank"></th> <th id="th-q1" colspan="2">1분기</th> <th id="th-q2" colspan="2">2분기</th> </tr> <tr> <th id="th-region">지역</th> <th id="th-q1-a">목표</th> <th id="th-q1-b">실적</th> <th id="th-q2-a">목표</th> <th id="th-q2-b">실적</th> </tr> </thead> <tbody> <tr> <th id="th-seoul" scope="row">서울</th> <td headers="th-q1 th-q1-a th-seoul">500만</td> <td headers="th-q1 th-q1-b th-seoul">520만</td> <td headers="th-q2 th-q2-a th-seoul">600만</td> <td headers="th-q2 th-q2-b th-seoul">580만</td> </tr> </tbody></table>
레이아웃 테이블과 데이터 테이블 구분
레이아웃 목적으로 <table>을 사용하면 스크린리더가 표로 읽어 혼란이 생깁니다. 레이아웃에는 CSS Flexbox/Grid를 사용하고, 테이블은 실제 표 데이터에만 씁니다.
<!-- 잘못된 예: 레이아웃 목적 테이블 --><table> <tr> <td><img src="photo.jpg" alt="사진"></td> <td><p>설명 텍스트</p></td> </tr></table><!-- 올바른 예: CSS Flexbox --><div style="display: flex; gap: 1rem;"> <img src="photo.jpg" alt="사진"> <p>설명 텍스트</p></div>
레이아웃 목적으로 테이블을 꼭 써야 한다면 role="presentation"으로 보조기기에서 표 역할을 제거합니다.
<table role="presentation"> <!-- 보조기기가 표로 인식하지 않음 --></table>
정렬 가능한 테이블
열 제목을 클릭해 정렬하는 테이블에 aria-sort를 사용합니다.
<thead> <tr> <th scope="col" aria-sort="ascending" <!-- 오름차순 정렬 중 --> > <button type="button"> 이름 <span aria-hidden="true">↑</span> </button> </th> <th scope="col" aria-sort="none" <!-- 정렬 없음 --> > <button type="button">날짜</button> </th> </tr></thead>
aria-sort 값: ascending (오름차순), descending (내림차순), none (정렬 없음), other (기타).
단계별 실습
따라하기: 테이블 접근성 확인
NVDA에서 표를 탐색합니다.
- Ctrl+Alt+방향키: 셀 단위 이동
- T: 다음 테이블로 이동
스크린리더가 각 셀을 읽을 때 열/행 제목이 함께 읽히는지 확인합니다.
변형하기: 반응형 테이블 접근성
작은 화면에서 표를 카드 형태로 변환할 때 접근성을 유지합니다.
/* 작은 화면에서 테이블 열을 데이터 레이블로 변환 */@media (max-width: 640px) { table, thead, tbody, tr, th, td { display: block; } thead tr { position: absolute; top: -9999px; left: -9999px; } td::before { content: attr(data-label); font-weight: bold; display: block; }}
<!-- data-label 속성으로 레이블 제공 --><tr> <td data-label="주문번호">12345</td> <td data-label="주문일">2026.01.01</td> <td data-label="금액">1,500,000원</td></tr>
정리와 확인
핵심 내용 요약
<caption>: 표 제목 (첫 번째 자식 요소)scope="col/row": 열/행 제목 지정headers속성: 복잡한 표에서 셀-제목 직접 연결role="presentation": 레이아웃 테이블에서 표 의미 제거aria-sort: 정렬 가능한 테이블의 현재 정렬 상태
확인 문제
문제 1. <th> 없이 <td>만 있는 표를 스크린리더로 탐색하면 어떤 문제가 생기는가?
각 셀을 읽을 때 값만 읽히고 어느 열/행에 속하는지 알 수 없습니다.
"12345, 2026.01.01, 1500000"처럼 숫자 나열이 됩니다.
문제 2. <caption> 대신 <h2> + 일반 <p> 텍스트로 표 제목을 표시하면 안 되는 이유는?
<caption>은 표와 프로그래밍적으로 연결됩니다.
스크린리더가 표로 이동할 때 "2026년 1월 주문 내역, 표, 5열 2행"처럼
제목과 표 정보를 함께 알려줍니다.
PART 06을 마쳤습니다. 다음 PART에서는 실제 자주 쓰는 UI 컴포넌트들 — 버튼, 토글, 진행 표시줄, 알림 배지 등의 접근성 구현을 다룹니다.