데이터로 표 만들기

숫자 열을 r(오른쪽 정렬)로 조판하는 것은 LaTeX 표에서 가장 흔한 실수입니다. 182.5, 95.0, 1450.25, 7을 오른쪽 정렬로 늘어놓으면 소수점은 정확히 숫자 한 자리(5pt)만큼 어긋나고, 홀로 있는 7은 일의 자리가 아니라 소수 첫째 자리 위치에 놓입니다. 실제로 재서 확인할 수 있는 이야기입니다. 고치는 방법은 열 지정에서 한 글자를 바꾸는 것뿐입니다. siunitxS 로 바꾸면 소수점 위치가 0.001pt 이내로 맞춰집니다. 이 페이지에서는 그 숫자 정렬과, .csv를 컴파일 시점에 읽어 표로 만드는 csvsimplepgfplotstabledatatool, 그리고 tabularray의 현대적인 방식을 다룹니다. 데이터는 데이터 파일에 그대로 두고 LaTeX가 읽게 하는 것이 원칙입니다.

LaTeX 표에서 소수점 맞추기 — siunitx의 S 열

\usepackage{siunitx}를 불러오고 열 지정의 rS 로 바꾸기만 하면 됩니다. S 열은 숫자를 해석해 소수점(decimal marker) 위치에 맞춰 정렬 하므로, 자릿수가 다른 숫자가 늘어서도 열을 읽을 수 있게 됩니다. PDF에서 글자의 좌표를 읽어 비교하면 차이가 뚜렷합니다. r 열에서는 182.595.0의 소수점이 x = 62.0에, 1450.25의 소수점은 x = 57.0에 놓여 약 5pt, 정확히 숫자 한 자리만큼 어긋납니다. 게다가 7은 일의 자리가 아니라 소수 첫째 자리 위치에 놓입니다. 같은 데이터를 S[table-format=4.2]로 조판하면 네 숫자의 일의 자리가 모두 x = 122.11에서 끝나고 소수점도 모두 같은 자리에서 시작합니다. 편차는 0.001pt 미만입니다.

latex
\usepackage{siunitx}
\usepackage{booktabs}

% table-format = <integer digits>.<decimal digits> of the widest entry
\begin{tabular}{l S[table-format=4.2]}
  \toprule
  Sample & {Mass / \unit{\gram}} \\
  \midrule
  A &  182.5  \\
  B &   95.0  \\
  C & 1450.25 \\
  D &    7    \\
  \bottomrule
\end{tabular}

요점은 두 가지입니다. 첫째, table-format=정수부.소수부 를 그 열의 가장 큰 값에 맞춰 지정할 것. 지정하지 않고 S만 써도 동작하지만 열 폭 확보가 부실해집니다(같은 예에서 열 폭이 88.44pt → 98.44pt로 늘었습니다). 반대로 table-format=2.1처럼 작게 지정해도 siunitx 3은 오류를 내지 않고 정렬도 유지됩니다. 다만 숫자가 확보된 공간을 넘어서 이웃 열과 부딪히기 쉬워집니다. 둘째, 숫자로 오인될 수 있는 것은 중괄호 {…}로 보호할 것. siunitx 설명서는 “그 자료가 숫자의 일부로 오인될 수 있다면 중괄호로 보호해야 한다”고 적고 있습니다. 뒤집어 말하면 Sample 같은 평범한 문자열은 지금의 v3에서는 중괄호 없이도 올바르게 가운데에 놓입니다.

“오인될 수 있다”의 실제 사례를 재 봅니다. 머리글을 중괄호 없이 2024 sales라고 쓰면 siunitx는 앞의 2024를 숫자로 집어 다른 행의 숫자와 정렬한 뒤, 남은 sales를 뒤따르는 자료로 놓습니다. 그 결과 2024sales 사이가 7.75bp 벌어져, 전체를 {2024 sales}로 감쌌을 때의 3.32bp(보통의 낱말 간격)와 비교하면 눈에 띄게 늘어집니다. 오류는 나지 않습니다. 망가지는 것은 겉모습뿐 이므로, 머리글에 숫자가 들어 있는 표에서는 중괄호를 잊지 마십시오. \multicolumn이나 \multirow의 인수 안에서는 S 열을 쓸 수 없으므로, 그때는 매크로판인 \tablenum[table-format=4.2]{1450.25} 를 씁니다. 설명서에 따르면 “사실상 S 열의 매크로판”입니다.

\num, \qty, \unit의 서식, 불확도(\num{1.234(5)} 같은 괄호 표기), 지수 처리, v2와 v3의 차이 같은 siunitx 전반은 “단위(siunitx)” 페이지가 담당합니다. 본문 속 물리량은 \qty, 표 속 숫자는 S 열로 분담해 두면 문서 전체에서 숫자 서식이 일관됩니다. 표 쪽에서 기억해 둘 것은 조판 관례 하나입니다. 단위는 각 셀이 아니라 열 머리글에 쓰십시오. booktabs 설명서도 같은 내용을 지침으로 들고 있습니다.

왜 CSV를 LaTeX에 읽히는가

숫자 한 자리가 바뀔 때마다 원고를 고치지 않아도 되기 때문입니다. 실험 결과나 집계표는 대개 스프레드시트나 측정 장비에서 CSV(쉼표로 구분된 텍스트) 로 나옵니다. 이것을 tabular의 셀에 손으로 옮겨 적는 일은 지루할 뿐 아니라, 행을 더할 때마다 값을 고칠 때마다 옮겨 적기 실수의 기회가 늘어납니다. 발상을 뒤집어 데이터는 데이터 파일에 두고 LaTeX에게 “읽어서 조판하라”고 지시하면, 데이터를 갱신하고 다시 컴파일하는 것만으로 표가 따라옵니다. 같은 CSV를 본문, 슬라이드, 부록에서 몇 번을 돌려 써도 전사 오류가 생기지 않습니다. 논리 구조와 겉모습을 분리한다는 LaTeX의 방식이 그대로 이어지는 셈입니다.

이 페이지의 예제는 모두 아래의 작은 CSV 파일을 사용합니다. 첫 줄이 머리글 행 으로 열 이름은 product, price, weight이고, 나머지가 데이터입니다. .tex와 같은 디렉터리에 data.csv로 저장하면 이어지는 코드는 그대로 컴파일됩니다.

data.csv
product,price,weight
Apple,380,182.5
Orange,120,95.0
Melon,1280,1450.25

csvsimple로 CSV 읽어 들이기

\usepackage{csvsimple}을 불러오고 \csvautotabular{data.csv} 한 줄만 쓰면 CSV 전체가 그대로 tabular가 됩니다. 첫 줄은 괘선이 붙은 머리글로 조판되며, 내용을 우선 훑어보고 싶을 때는 이것으로 충분합니다. 다만 서식의 자유는 거의 없습니다. 정렬, 괘선, 출력할 열을 직접 정하고 싶다면 \csvreader 를 쓰십시오. 이 패키지의 본체가 그쪽입니다. 알아 둘 만한 점 하나. TeX Live 2024에 들어 있는 v2.6.0에서는 \usepackage{csvsimple}만 쓰면 예전 구현인 csvsimple-legacy 가 로드됩니다(csvsimple.sty\SetKeys{ legacy }라고 적혀 있습니다). LaTeX3 구현을 쓰려면 \usepackage[l3]{csvsimple} 또는 \usepackage{csvsimple-l3}처럼 명시해야 합니다.

latex
\usepackage{csvsimple}
\usepackage{booktabs}
\usepackage{siunitx}

% NOTE the braces around the column spec: an unbraced S[...] breaks the key list
\csvreader[
  tabular        = {l r S[table-format=4.2]},
  table head     = \toprule Product & {Price} & {Weight} \\ \midrule,
  table foot     = \bottomrule,
  late after line = \\]
{data.csv}
{product=\product, price=\price, weight=\weight}
{\product & \price & \weight}

\csvreader[옵션]{파일}{할당}{본체}의 네 부분은 차례로 읽어 들일 파일, 열 이름과 매크로의 결속, 그리고 행마다 출력할 내용을 담당합니다. price=\price라고 쓰면 본체 안에서 \price가 그 행의 값으로 전개됩니다. 바깥 틀은 옵션 쪽에서 정합니다. tabular=가 열 지정, table head=가 머리글 행, table foot=이 마무리, late after line = \\각 행 끝에 행 종결자 \\를 붙이는 지정입니다(마지막 행 뒤에 불필요한 줄바꿈을 남기지 않기 위한 정석). 머리글 이름에 공백이나 기호가 들어 있을 때는 할당을 비워 두고 \csvcoli, \csvcolii, \csvcoliii(1·2·3열)로 번호 참조할 수 있습니다.

이 페이지에서 유일하게 진짜 “함정”이 여기 있습니다. 위 예제에서 tabular의 값을 중괄호로 감싼 것은 장식이 아닙니다. tabular = l r S[table-format=4.2]처럼 맨몸으로 쓰면 키·값 목록의 구분자인 쉼표가 S[…] 안의 쉼표와 충돌해 ! Paragraph ended before \NC@rewrite@S was complete. 로 멈춥니다. 이어서 ! Missing $ inserted.! Package csvsimple Error: File ',' not existent…까지 나오므로 원인이 열 지정에 있다고 알아채기 어렵습니다. tabular = {l r S[table-format=4.2]}처럼 중괄호로 감싸면 통과합니다. 이 습관은 S에 국한되지 않습니다. 대괄호가 붙은 열 지정을 키·값 목록 안에 쓸 때는 반드시 중괄호로 지킬 것.

기본적으로 첫 줄은 머리글 로 취급되어 데이터에서 제외됩니다. 머리글 행이 없는 CSV를 읽을 때는 별표가 붙은 \csvreader*를 쓰면 첫 줄도 데이터로 읽힙니다. 그 밖에 조건으로 행을 고르는 filter, 할당 묶음을 재사용하는 \csvstyle / \csvnames가 있어 표에 국한되지 않고 “행 단위 처리” 전반에 쓸 수 있습니다. 주소록에서 우편 라벨을 만드는 것 같은 용도입니다.

pgfplotstable로 숫자 서식까지 만들기

숫자의 보이는 방식 자체를 설계하고 싶다면 가장 강력한 선택지는 pgfplotstable입니다. pgfplots의 일부이며 \usepackage{pgfplotstable}로 불러오고, 중심 명령은 하나뿐입니다. \pgfplotstabletypeset[옵션]{data.csv} 입니다. CSV를 읽어 지정한 자릿수와 표기 방식으로 정형한 뒤 내부에서 tabular를 조립해 출력합니다. CSV를 읽히려면 구분자를 col sep=comma로 선언해야 합니다(기본값은 공백 구분입니다). 제어는 모두 키·값 으로 이루어집니다.

옵션하는 일
col sep=commaCSV(쉼표 구분)로 읽기. 기본값은 공백 구분
header=has colnames첫 줄을 열 이름으로 취급. header=false면 머리글 행이 없다고 간주
columnscolumns={a,b,…}으로 출력할 열과 그 순서를 고름
columns/NAME/.stylecolumns/price/.style={…}처럼 특정 열에만 서식을 적용
column name인쇄되는 머리글을 교체(CSV의 열 이름과 다르게 지정 가능)
fixed고정 소수점. fixed zerofill로 끝자리 0을 채우고 precision=n으로 소수 자릿수를 정함
sci지수(과학) 표기로 조판. sci zerofill은 가수부의 0을 채움
string type텍스트 열. 숫자 서식을 전혀 적용하지 않음
dec sep align소수점 위치로 열을 정렬(array 필요)
latex
\usepackage{pgfplotstable}
\usepackage{booktabs}
\pgfplotsset{compat=1.18}

\pgfplotstabletypeset[
  col sep = comma,
  header  = has colnames,
  columns = {product, price, weight},
  columns/product/.style = {string type, column name = Product},
  columns/price/.style   = {column name = Price, fixed, precision = 0},
  columns/weight/.style  = {column name = {Weight / g}, fixed, fixed zerofill,
                            precision = 1, dec sep align},
  every head row/.style  = {before row = \toprule, after row = \midrule},
  every last row/.style  = {after row = \bottomrule},
]{data.csv}

product 열은 string type(텍스트), price는 정수(precision=0), weight는 소수 한 자리에 0을 채우고 dec sep align으로 소수점을 맞춥니다. 머리글은 column name으로 교체하고, 괘선은 every head rowevery last row를 통해 booktabs 명령을 끼워 넣었습니다. 여기서 precision만 바꿔도 같은 데이터의 자릿수가 달라집니다. CSV를 건드리지 않고 숫자의 겉모습을 제어하는 것이야말로 pgfplotstable의 진가입니다. 동시에 주의도 필요합니다. 이 설정에서는 1450.251,450.3으로 반올림되어 출력됩니다. 자릿수를 줄이는 것은 서식 지정이고 반올림은 조용히 일어나므로, 유효 숫자는 의도적으로 정하십시오. (천 단위 구분이 필요 없다면 1000 sep={}을 덧붙입니다.)

pgfplotstable은 읽어 들인 열에서 계산 열 을 이끌어 낼 수도 있습니다. create on use로 “쓸 때 계산되는 열”을 정의하거나, columns/…/.style 안에서 postproc cell content로 값을 후처리하는 식입니다. 요컨대 스프레드시트에 가까운 작업을 LaTeX 안에서 끝낼 수 있습니다. 그만큼 구문이 무거워지므로 기준은 정교한 수치 표라면 pgfplotstable, 단순한 CSV → 표라면 csvsimple 입니다. 같은 CSV로 그림도 그린다면 pgfplots\addplot table과 도구를 공유할 수 있다는 점도 이점입니다.

datatool — CSV를 데이터베이스로 다루기

세 번째 패키지 datatool은 CSV를 데이터베이스 로 읽어 들이며, 행 단위 처리(메일 머지 같은 작업)에 강합니다. \usepackage{datatool} 위에서 \DTLloaddb{이름}{data.csv}라고 쓰면 이름이 붙은 데이터베이스로 들어옵니다. 기본적으로 첫 줄이 머리글이고, 그 열 이름이 각 값의 가 됩니다. 머리글 행이 없는 CSV는 \DTLloaddb[noheader]{…}{…}로 하면 열 이름이 Column1, Column2… 로 자동 부여됩니다. 읽어 들인 뒤에는 \DTLforeach{이름}{할당}{본체} 로 행을 차례로 훑습니다. 할당은 “매크로 = 열 이름” 방향으로 \DTLforeach{db}{\Product=product,\Price=price}{…}처럼 쓰고, 본체에서는 \Product, \Price가 그 행의 값으로 전개됩니다.

latex
\usepackage{datatool}
\usepackage{booktabs}

\DTLloaddb{goods}{data.csv}

% the row break goes at the START of the loop body, not the end
\begin{tabular}{l r}
  \toprule
  Product & Price
  \DTLforeach{goods}{\Product=product, \Price=price}{%
    \\ \Product & \Price}
  \\ \bottomrule
\end{tabular}

여기서는 CSV를 goods로 읽어 들이고, tabular 안의 \DTLforeach가 행마다 “품명 & 가격”을 출력합니다. 행 종결자 \\ 가 본체의 끝이 아니라 앞에 있다는 점에 유의하십시오. 끝에 두면 마지막 반복이 빈 행을 열어 버려서, 뒤따르는 \bottomrule(이나 \hline)이 그 행의 셀 안으로 떨어지고 ! Misplaced \noalign. 로 멈춥니다. 앞에 두고 루프 뒤에 \\ 를 하나 더 붙이는 것이 안전한 형태입니다. datatool의 진가는 표 조판보다 데이터 조작 쪽에 있습니다. 매크로로 값의 합계와 평균을 구하고, 정렬하고, 조건으로 행을 제외할 수 있으며, 참고 문헌이나 병합 문서 생성에도 쓰입니다. 반대로 CSV를 표로 만들기만 하면 된다면 csvsimple이 더 간결합니다.

tabularray의 Q 열로 숫자 정렬하기

\UseTblrLibrary{siunitx}를 불러오면 tabularrayQ[si={table-format=4.2}] 형태로 S 열과 같은 소수점 정렬을 tblr 환경에 들여올 수 있습니다. \UseTblrLibrary{booktabs}를 함께 쓰면 \toprule\midrule\bottomrule도 그대로 쓸 수 있고, 서식은 colspec이나 row{1}={font=\bfseries}처럼 전부 키·값으로 지정합니다. >{…}\multirow 같은 구문을 다시 익힐 필요가 없다는 것이 큰 장점입니다. 다만 tabularray 자체(tblr의 설계 사상, width=, 셀 병합, rowsepcolsep)는 “고급 표 환경” 페이지가 담당하므로, 여기서는 숫자 열 에만 집중합니다.

latex
\usepackage{tabularray}
\UseTblrLibrary{booktabs}
\UseTblrLibrary{siunitx}

\begin{tblr}{colspec = {l r Q[si={table-format=4.2}]}}
  \toprule
  Product & Price & {{{Weight}}} \\
  \midrule
  Apple  &  380 &  182.5  \\
  Orange &  120 &   95.0  \\
  Melon  & 1280 & 1450.25 \\
  \bottomrule
\end{tblr}

어느 것을 쓸까

목적에서 출발해 고르는 것이 빠릅니다. 데이터가 수십 행 정도이고 손으로 쓴 표의 숫자만 맞추면 된다면 S 열로 충분합니다. CSV가 계속 갱신된다면 읽어 들이는 쪽으로 옮기십시오. 대략의 지침은 다음과 같습니다.

  • 소수점만 맞추면 될 때siunitxS 열. 손으로 쓴 tabular에 그대로 끼워 넣을 수 있고, \multicolumn 안에서는 \tablenum을 씁니다.
  • 단순한 CSV → 표csvsimple. \csvautotabular로 즉석에서, \csvreader로 정렬・괘선・열 선택까지 제어합니다.
  • 자릿수와 표기 방식을 다듬거나 계산 열이 필요할 때pgfplotstable. 가장 강력하지만 구문이 무겁고 반올림이 조용히 일어납니다.
  • 데이터 조작 자체가 목적일 때(합계・정렬・조건 처리・병합 문서) — datatool.
  • 키·값으로 쓰고 싶거나 기존 tblr 표에 숫자 열을 더하고 싶을 때tabularraysiunitx 라이브러리의 Q[si={…}].

어느 방법을 쓰든 최종적으로 나오는 것은 같은 tabular의 어휘, 즉 열 지정, &, \\, 괘선입니다. 읽어 들이는 일은 잘 되는데도 표가 읽기 어렵다면 원인은 대개 괘선 쪽에 있습니다. \hlinebooktabs의 세 줄로 바꾸고 단위를 열 머리글로 옮기기만 해도 같은 데이터가 훨씬 읽기 좋아집니다. 페이지 폭에 들어가지 않는 표는 “고급 표 환경”, 페이지를 넘어가는 긴 표는 “페이지를 넘기는 표”, 캡션과 배치는 “표 배치와 스타일링”을 보십시오.