Skip to content
This page has been auto-translated and may contain errors.View in English

테이블

docs.scrimba.com

어떤 정보는 그 자체로 격자 형태를 띱니다. 열차 시간표, 요금제, 순위표, 영양 정보 표를 떠올려 보세요. 이런 정보는 모두 행과 열로 이루어져 있고, 그 의미는 항목들이 어떻게 줄을 맞춰 배열되는지에서 나옵니다. HTML에는 바로 이런 콘텐츠를 위한 전용 요소들이 마련되어 있으며, 이를 사용하면 화면에서도, 페이지를 음성으로 읽어주는 소프트웨어에서도 격자를 제대로 읽어낼 수 있습니다.

테이블의 기본

**테이블(table)**은 스프레드시트와 같은 모양의 행과 열로 이루어진 격자입니다. 테이블은 네 가지 요소로 만듭니다. <table>은 격자 전체를 감쌉니다. 각 <tr>은 하나의 행입니다(tr은 table row의 줄임말입니다). 행 안에서 각 <td>는 하나의 데이터 셀입니다(td는 table data입니다). 어떤 셀이 자신이 속한 열이나 행에 이름을 붙이는 역할을 한다면 <td> 대신 <th>를 사용합니다(th는 table header입니다). 브라우저는 이 셀을 굵게, 가운데 정렬로 표시해 데이터와 구분되도록 해줍니다.

테이블은 행 단위로 만들며, 열 단위로 만들지 않습니다. <table>이 컨테이너이고, 각 <tr>이 하나의 행이며, 셀들은 페이지에 나타나는 순서대로 행 안에 배치됩니다. 데이터 셀은 <td>이고, **헤더 셀**은 <th>입니다. 열을 나타내는 별도의 요소는 존재하지 않습니다. 열은 모든 행이 자신의 셀을 같은 순서로 유지함으로써 자연스럽게 생겨나는 결과이므로, 어느 한 행이라도 순서가 어긋나면 격자 전체가 맞지 않게 됩니다.

<th><td>의 차이는 단순히 외형의 차이가 아닙니다. <th>는 그 셀이 속한 행이나 열의 제목임을 나타냅니다. 브라우저 기본 스타일인 굵게, 가운데 정렬은 그중 눈에 보이는 절반일 뿐입니다. 나머지 절반은 다른 소프트웨어가 읽어낼 수 있는 의미입니다.

테이블의 소스 코드는 행 우선(row-major) 방식입니다. 한 번에 한 행씩 작성하며, 각 셀이 속하는 열은 오로지 그 행 안에서의 위치로 결정됩니다. 브라우저는 이 행 단위 소스를 읽어 2차원 격자를 구성하고, 순서를 세어가며 셀을 열에 대응시킵니다. 마크업 어디에도 열의 이름이 명시되지 않으므로, 행들이 얼마나 일관되게 작성되었는지에 따라 격자가 맞아 들어가는 정도가 결정됩니다.

<th><td>의 구분은 스타일이 아니라 의미의 구분입니다. <th>(헤더 셀)는 브라우저와, 페이지를 읽는 보조 기술(화면을 볼 수 없는 사람들을 위해 페이지를 음성으로 읽어주는 스크린 리더 같은 소프트웨어)에게 이 셀이 주변 데이터의 레이블이라는 것을 알려줍니다. 기본적으로 굵고 가운데 정렬되는 모습은 부수적인 것일 뿐입니다. CSS로 그 스타일을 걷어내더라도 헤더는 여전히 "제목"이라는 의미를 유지하는데, 그 의미가 요소 자체에 담겨 있고 외형에 달려 있지 않기 때문입니다. 다른 방식으로 격자를 그리는 대신 진짜 테이블을 써야 하는 이유가 바로 여기에 있습니다.

html
<table>
  <tr>
    <th>Day</th>
    <th>Opening hours</th>
  </tr>
  <tr>
    <td>Monday</td>
    <td>8:00 to 18:00</td>
  </tr>
  <tr>
    <td>Saturday</td>
    <td>9:00 to 14:00</td>
  </tr>
</table>

맨 위에 헤더 셀 두 개가 있고, 그 아래에 데이터 행 두 개가 있습니다. 브라우저는 이 격자를 그려서 각 열을 내용에 맞는 크기로 맞추고, 두 개의 <th> 셀을 굵게 렌더링합니다.

Juno테이블의 기본 네 가지 요소로 격자를 만듭니다. 전체를 감싸는 <table>, 각 행을 나타내는 <tr>, 셀을 나타내는 <td>, 제목 셀을 나타내는 <th>입니다. 제목 셀은 굵게, 가운데 정렬로 표시되어 데이터와 구분됩니다. 한 번에 한 행씩 만들면 열은 저절로 맞춰집니다.
Juno테이블의 기본<tr>로 테이블을 행 단위로 배치하며, 열은 모든 행이 셀 순서를 동일하게 유지함으로써 만들어집니다. 제목에는 <th>, 데이터에는 <td>를 쓰는데, 이 선택은 단순히 굵은 외형이 아니라 의미의 문제입니다. 한 행에서 셀 개수를 잘못 세면 격자 전체가 비뚤어집니다.
Juno테이블의 기본 마크업이 행 우선 방식이기 때문에, 열은 각 행이 셀 순서를 맞춰 유지할 때만 존재하는 셈입니다. 셀 개수를 꼼꼼히 세어야 합니다. <th>는 스크린 리더에게 그 셀이 레이블이라는 것을 알려줌으로써 존재 가치를 얻는데, 이는 CSS로 아무리 굵게 만들어도 대신할 수 없는 부분입니다. 이 의미상의 구분이야말로 애초에 진짜 테이블을 써야 하는 이유입니다.

테이블 구조화하기

행의 순서는 곧 사람들이 읽는 순서이므로, <th> 셀로 이루어진 **제목 행**을 맨 위에 둡니다. 그다음에는 모든 행의 길이를 동일하게 유지합니다. 제목 행에 셀이 세 개라면, 그 아래의 모든 <tr><td> 셀 세 개를 가져야 합니다. 셀이 하나라도 빠진 행이 있으면 격자에 구멍이 생기고 열이 맞지 않게 됩니다. 여기서 새로 배울 내용은 없으며, 이미 알고 있는 행 다루는 방식을 신경 써서 지키는 것뿐입니다.

테이블이 커지면 행들을 **행 그룹**으로 묶어 구조를 명확히 드러냅니다. 제목 행은 <thead>로, 데이터 행은 <tbody>로, 합계 줄 같은 요약 행은 <tfoot>으로 감쌉니다. 이런 그룹은 그 자체로 레이아웃을 크게 바꾸지는 않지만, 스타일링, 인쇄, 보조 소프트웨어를 위해 테이블의 각 부분에 이름을 붙여주는 역할을 합니다.

<caption><table>의 첫 번째 자식으로 넣어 테이블에 제목을 붙일 수 있습니다. 이 요소는 격자 위쪽에 렌더링되며, 테이블 옆에 그냥 놓아둔 제목과 달리 마크업상으로 테이블과 확실하게 연결되어 있습니다.

셀이 여러 열이나 여러 행에 걸쳐야 할 때는 colspan이나 rowspan을 사용합니다. colspan="2"는 셀을 두 열만큼 넓게 만들고, rowspan="2"는 셀을 두 행만큼 높게 만듭니다. 이렇게 셀이 차지하는 위치는 그 공간이 이미 사용되고 있기 때문에 해당 행들에서 생략됩니다.

html
<thead>
  <tr>
    <th rowspan="2">Region</th>
    <th colspan="2">2024</th>
  </tr>
  <tr>
    <th>Q1</th>
    <th>Q2</th>
  </tr>
</thead>

"Region"은 헤더 행 두 개를 채우고, "2024"는 열 두 개를 채우며, Q1과 Q2는 두 번째 행에서 그 아래에 자리합니다.

행 그룹은 단순한 장식이 아닙니다. <thead><tfoot>은 긴 테이블을 인쇄할 때 매 페이지마다 반복되도록 정의되어 있어서, 페이지가 나뉘어도 헤더가 계속 보이게 해줍니다. 또한 <tbody>가 스크롤되는 동안에도 고정된 헤더나 푸터를 유지할 수 있게 해줍니다. 테이블 하나에 여러 개의 <tbody> 그룹을 넣을 수도 있는데, 이는 단일 격자를 깨뜨리지 않으면서 긴 데이터셋을 이름표가 붙은 여러 구획으로 나누는 깔끔한 방법입니다.

colspanrowspan은 **셀 병합(cell spanning)**을 제어합니다(하나의 셀이 여러 열이나 행을 차지하는 것). 그리고 이 부분에서 테이블이 조용히 망가지는 경우가 가장 많습니다. 모든 행은 여전히 격자 전체의 너비를 채워야 합니다. colspan="2"인 셀은 열 두 개를 채우므로, 그 셀과 겹치는 행들은 자기 셀을 따로 채우지 않고 해당 위치를 비워둬야 합니다. 셀 개수를 잘못 세더라도 브라우저는 뭔가를 렌더링해주긴 하지만, 의도한 격자와는 다른 모습이 됩니다. <caption>은 테이블의 접근 가능한 이름입니다. 보조 소프트웨어(페이지를 음성으로 읽어주는 스크린 리더 같은 도구)는 리더가 테이블에 도달했을 때 이를 읽어주는데, 이는 근처에 있는 제목 셀로는 할 수 없는 역할입니다.

html
<table>
  <caption>Quarterly revenue by region</caption>
  <thead>
    <tr>
      <th>Region</th>
      <th>Q1</th>
      <th>Q2</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>North</th>
      <td>£12,000</td>
      <td>£15,400</td>
    </tr>
    <tr>
      <th>South</th>
      <td>£9,800</td>
      <td>£11,200</td>
    </tr>
  </tbody>
  <tfoot>
    <tr>
      <th>Total</th>
      <td>£21,800</td>
      <td>£26,600</td>
    </tr>
  </tfoot>
</table>
Juno테이블 구조화하기 행은 위에서 아래로 읽히므로, <th> 셀로 이루어진 제목 행을 맨 앞에 둡니다. 그다음 모든 행의 길이를 똑같이 유지합니다. 제목이 세 개면 그 아래 각 행에도 <td> 셀이 세 개 있어야 합니다. 셀이 빠지면 격자에 구멍이 생기는데, 이는 초보자들이 초반에 흔히 걸려드는 부분입니다.
Juno테이블 구조화하기 테이블이 커지면 부분들을 묶어줍니다. 제목 행은 <thead>, 데이터는 <tbody>, 합계 줄은 <tfoot>, 제목은 첫 번째 자식으로 넣는 <caption>입니다. colspanrowspan은 셀을 여러 열이나 행에 걸쳐 늘려주지만, 그렇게 덮인 위치는 해당 행에서 빠진다는 점을 기억하세요. 개수를 정확히 맞추면 격자가 흐트러지지 않습니다.
Juno테이블 구조화하기 행 그룹은 실제로 중요한 역할을 합니다. <thead><tfoot>은 테이블을 인쇄할 때 반복되고, 스크롤되는 본문을 고정해주는 기준점이 될 수 있으며, <caption>은 스크린 리더가 읽어주는 이름입니다. 병합은 조용히 숨어 있는 함정인데, 모든 행이 여전히 격자 전체 너비를 채워야 하므로 덮인 위치에는 셀을 넣지 않아야 합니다. 브라우저는 개수를 잘못 세도 아무렇지 않게 렌더링해버리기 때문에 오히려 발견하기가 어렵습니다.

접근성 있는 테이블

접근성 있는 테이블이란 화면을 보는 대신 스크린 리더(페이지를 음성으로 읽어주는 소프트웨어)로 페이지를 듣는 사람들을 포함해 누구에게나 이해가 되는 테이블입니다. 여러분이 할 수 있는 가장 유용한 일은 이미 알고 있는 것입니다. 모든 제목 셀에 <td>가 아니라 <th>를 사용하는 것입니다. 그러면 스크린 리더가 어떤 셀이 레이블이고 어떤 셀이 데이터인지 청취자에게 알려줄 수 있으므로, "£15" 같은 값이 그것이 속한 열과 함께 안내됩니다. 모든 셀이 그냥 <td>인 테이블은 아무런 기준점 없이 숫자만 잔뜩 늘어놓은 것처럼 읽힙니다.

<th>는 셀이 헤더임을 나타내지만, 열 제목과 행 제목이 모두 있는 테이블에서는 그것이 어느 쪽인지까지는 말해주지 않습니다. scope 속성이 그것을 명확히 해줍니다. scope="col"은 그 헤더가 자신이 속한 열 전체를 나타낸다는 뜻이고, scope="row"는 그 헤더가 자신이 속한 행 전체를 나타낸다는 뜻입니다. 모든 <th>에 이를 추가하면 스크린 리더가 어떤 데이터 셀이든 알맞은 열 제목과 행 제목을 한꺼번에 짝지어 줄 수 있습니다.

html
<th scope="col">Month</th>
<th scope="row">Rainfall</th>

단순한 격자라면 브라우저가 이런 연관 관계를 스스로 짐작할 수 있는 경우가 많지만, scope로 명시해두면 짐작할 필요가 없어지고 추가하는 데 드는 비용도 크지 않습니다.

scope는 행 헤더와 열 헤더가 각각 한 단계뿐인 테이블을 다룹니다. 여러 열에 걸친 헤더나 두 단계로 된 열 제목이 있는 복잡한 테이블은 scope를 붙여도 여전히 모호합니다. 데이터 셀 위에 정확히 하나의 열 헤더와 하나의 행 헤더만 있는 것이 아니기 때문입니다. 이런 경우에는 **id와 headers**를 이용해 셀을 명시적으로 연결합니다. 각 헤더 셀에 id(그 요소를 위한 고유한 이름)를 부여한 다음, 각 데이터 셀의 headers 속성에 해당하는 id들을 나열합니다.

html
<th id="q1">Q1</th>
<th id="north">North</th>
<td headers="north q1">£12,000</td>

이제 데이터 셀은 자신을 설명하는 헤더가 무엇인지, 그리고 어떤 순서로 읽어야 하는지를 정확히 지정하게 되며, 그 관계를 알고리즘이 추측할 필요가 없습니다. 다만 표기가 번거롭기 때문에 정말로 필요한 테이블에만 사용하세요. 먼저 scope를 사용하고, 헤더 구조가 너무 불규칙해서 scope로 표현할 수 없을 때만 idheaders로 단계를 올리세요.

접근성은 테이블에만 국한된 이야기가 아닙니다. 나머지 전체적인 내용은 접근성 챕터에서 다룹니다.

Juno접근성 있는 테이블 가장 큰 이득은 아무런 추가 비용도 들지 않습니다. 제목에는 <th>, 데이터에는 <td>를 사용하세요. 스크린 리더는 이 구분을 이용해 각 값을 그것이 속한 제목과 함께 읽어주므로, £15가 자기 열과 함께 전달됩니다. 이를 빠뜨리면 테이블 전체가 뿔뿔이 흩어진 숫자 더미로 들리게 됩니다.
Juno접근성 있는 테이블 헤더 셀에 scope를 붙이세요. 열 제목에는 scope="col", 행 제목에는 scope="row"입니다. 그러면 스크린 리더가 각 데이터 셀을 양쪽 축의 올바른 제목과 연결할 수 있습니다. 단순한 격자라면 브라우저가 짐작할 수도 있지만, 명시해두는 데 드는 비용은 크지 않고 짐작할 필요도 없어집니다.
Juno접근성 있는 테이블scope는 각 방향으로 헤더가 하나씩만 있는 평범한 격자를 처리합니다. 헤더가 여러 열에 걸치거나 여러 단계로 쌓여서 한 축에 헤더가 둘 이상 있는 셀이 생기면, 헤더에는 id를, 데이터 셀에는 headers를 붙여 직접 연결하는 방식으로 전환하세요. 표기가 번거로우니 정말로 필요한 테이블에만 사용하고, 나머지는 scope에 맡기세요.

테이블은 데이터를 위한 것이며, 레이아웃을 위한 것이 아닙니다

CSS가 레이아웃을 다루지 못했던 시절에는 사람들이 테이블로 페이지 전체 레이아웃을 만들곤 했습니다. 예를 들어 <table>을 이용해 왼쪽에 메뉴를, 오른쪽에 본문을 배치하는 식이었습니다. 시각적으로는 작동하기 때문에 오래된 페이지나 일부 이메일 템플릿에서 여전히 이런 모습을 볼 수 있습니다. 이 방식을 따라 하지 마세요. 테이블은 표 형태의 데이터(tabular data), 즉 행과 열의 격자 안에 속하는 정보를 위한 것입니다. 페이지의 조각들을 배치하는 일은 CSS의 역할입니다. 데이터를 담기 위해서가 아니라 요소를 배치하기 위해 테이블을 사용하면, 스크린 리더를 사용하는 사람에게는 페이지가 더 읽기 어려워지고, 나중에 수정하기도 더 어려워집니다.

규칙은 간단합니다. <table>은 "이 콘텐츠는 서로 관련된 데이터의 격자다"라는 뜻입니다. 정보가 실제로 행과 열을 갖는 경우, 예를 들어 일정표, 비교표, 결과 목록 같은 경우에 사용하세요. 페이지를 배치하는 데 사용하지 말고, 데이터가 아닌 것들을 줄 맞추는 데도 사용하지 마세요. CSS grid와 flexbox는 레이아웃을 제대로 처리하며, 화면 크기에 맞춰 적응하고, HTML이 배치가 아니라 콘텐츠를 설명하도록 유지해줍니다.

열을 만들기 위해 테이블을 가져다 쓰는 것은 대표적인 잘못된 사용 사례입니다. 콘텐츠가 내비게이션 바나 페이지 전체에 흩어진 폼이라면 그것은 표 형태의 데이터가 아니며, 겉모습을 그럴듯하게 만들 수는 있더라도 테이블은 잘못된 도구입니다.

테이블을 레이아웃 용도로 사용하면, 보조 소프트웨어(페이지를 음성으로 읽어주는 스크린 리더 같은 도구)에 데이터가 아닌 것을 데이터 구조로 넘기게 됩니다. 스크린 리더는 테이블을 그 차원으로 안내하고 사용자가 셀 단위로 이동할 수 있게 해주는데, 이때 레이아웃을 위한 뼈대를 마치 격자 안의 모든 영역이 서로 관련된 값인 것처럼 취급합니다. 결과적으로 실제로는 헤더, 사이드바, 본문일 뿐인 것을 두고 "테이블, 4열, 10행"과 같은 안내가 나오게 됩니다.

읽는 순서 문제도 있습니다. 보조 소프트웨어와 검색 엔진은 페이지를 화면상의 위치가 아니라 소스 순서, 즉 HTML에 요소가 나타나는 순서대로 읽습니다. 레이아웃용 테이블을 쓰면 소스 순서가 시각적 격자와 일치하도록 강제되므로, 청취자에게는 가장 먼저 전달되어야 할 콘텐츠, 예를 들어 본문 기사가 시각적 열이 요구하는 위치 때문에 내비게이션 열 뒤로 밀려나 버릴 수 있습니다. CSS 레이아웃은 이런 결합을 끊어줍니다. 중요한 콘텐츠를 소스상 앞쪽에 두면서도 화면상으로는 원하는 곳 어디에나 배치할 수 있습니다. 테이블을 데이터 용도로만 남겨두면, 읽는 순서를 계속 여러분이 통제할 수 있습니다.

유일하게 애매한 영역은 HTML 이메일입니다. 오래된 메일 클라이언트들은 여전히 안정적인 CSS 레이아웃을 지원하지 못해서 테이블 기반 구조가 흔히 쓰입니다. 이는 그 매체가 가진 제약일 뿐이며, 웹으로 다시 가져올 만한 패턴은 아닙니다.

html
<!-- Wrong: a table used to place a sidebar next to an article -->
<table>
  <tr>
    <td><nav>...</nav></td>
    <td><article>...</article></td>
  </tr>
</table>

<!-- Right: real elements, with CSS handling the columns -->
<nav>...</nav>
<article>...</article>

이는 HTML이란 무엇인가에서 다룬 구조와 표현의 분리와 같은 이야기입니다. 구조는 HTML에, 배치는 CSS에 맡기세요. 의미에 따라 요소를 고르는 방법은 시맨틱 HTML을 참고하세요.

Juno테이블은 데이터를 위한 것이며, 레이아웃을 위한 것이 아닙니다 테이블은 격자 안에 속하는 데이터, 즉 서로 관련된 값들의 행과 열을 위한 것입니다. 오래된 사이트들이 그렇게 썼다 하더라도 페이지를 배치하는 용도는 아닙니다. 페이지의 부분들을 배치하는 일은 CSS의 역할이며, 이를 테이블에 억지로 끼워 넣으면 페이지를 음성으로 읽기도, 나중에 바꾸기도 더 어려워집니다.
Juno테이블은 데이터를 위한 것이며, 레이아웃을 위한 것이 아닙니다 콘텐츠가 정말로 데이터의 격자일 때만 <table>을 사용하세요. 일정표, 비교표, 결과 목록 같은 경우입니다. 페이지를 배치하거나 데이터가 아닌 것을 줄 맞추는 일에는 CSS grid나 flexbox를 쓰세요. 이들은 화면에 맞춰 적응하고 HTML을 콘텐츠에 관한 것으로 유지해줍니다. 레이아웃 용도로 억지로 쓰이는 테이블이 대표적인 잘못된 도구 사용 사례입니다.
Juno테이블은 데이터를 위한 것이며, 레이아웃을 위한 것이 아닙니다 레이아웃용 테이블은 스크린 리더를 헷갈리게 합니다. 데이터 격자로 인식하고서 아무 의미 없는 행과 열을 읽어주게 됩니다. 또한 소스 순서를 시각적 격자에 묶어버리는데, 보조 기술과 검색 엔진이 실제로 읽는 것은 바로 그 소스 순서입니다. 테이블은 데이터 용도로만 남겨두고 배치는 CSS에 맡기면, 읽는 순서를 계속 여러분이 통제할 수 있습니다. HTML 이메일만이 유일한 예외인데, 이는 그저 그 클라이언트들이 과거에 머물러 있기 때문입니다.