플롯

논문에 이미지로 붙여 넣은 그래프는 대개 한눈에 알아볼 수 있습니다. 곡선이 지저분해서가 아니라 축의 숫자만 글꼴이 다르기 때문입니다. LaTeX에서 이를 근본적으로 고치는 도구가 pgfplots입니다. 그래프를 이미지로 받는 대신, 숫자에서 출발해 본문과 같은 조판 엔진으로 만들어 냅니다. 이 페이지에서는 pgfplotsaxis 환경과 \addplot, .dat 파일 읽기, 계산을 gnuplot에 맡기는 경로, 그리고 R이나 Python에서 그린 그림을 글꼴째로 가져오는 방법을 다룹니다. 중간중간 TeX Live 2024가 실제로 출력한 오류 메시지를 그대로 인용합니다. 검색으로 여기까지 온 사람 대부분은 바로 그 오류를 보고 있을 테니까요.

붙여 넣은 이미지가 문서와 겉도는 이유

원인은 해상도가 아니라 서체와 선 굵기입니다. 스프레드시트의 기본값은 산세리프체이고 논문 본문은 세리프체입니다. 독자가 글꼴 이름을 알지 못해도 그 어긋남만은 느낍니다. 게다가 이미지를 판면에 맞춰 축소하면 선 굵기도 함께 줄어들어, 그림 안의 선만 본문의 선보다 가늘어집니다. pgfplots는 좌표만 받아서 눈금 숫자와 범례를 문서 자신의 글꼴로, 문서 자신의 수식 조판을 거쳐 놓기 때문에 이 두 가지 어긋남이 원리적으로 생기지 않습니다. 축 라벨에 $\sin x$라고 쓰면 본문 수식과 완전히 같은 자형이 나옵니다.

실무에서 효과가 큰 두 번째는 데이터와의 분리입니다. 좌표를 본문에 직접 쓰지 말고 .dat.csv를 읽게 해 두면, 측정을 다시 했을 때 파일을 바꾸고 다시 컴파일하기만 하면 그림이 따라옵니다. 논문, 슬라이드, 부록이 같은 숫자 파일을 보므로 옮겨 적다 생기는 사고가 애초에 발생하지 않습니다. 표 쪽에도 같은 발상의 자매 패키지 pgfplotstable이 있습니다. pgfplots 자체는 TikZ/PGF 위에 만들어져 있어 전체 그림이 tikzpicture 안에 놓이고, TikZ의 노드나 장식을 그대로 섞을 수 있습니다. TikZ 자체(\draw, 좌표계, 컴파일이 느린 이유)는 TikZ 페이지가 담당합니다.

\pgfplotsset{compat=1.18}을 쓰지 않으면 어떻게 되는가

경고가 나오고, 그림은 옛 기본값 그대로 남습니다. TeX Live 2024에 들어 있는 pgfplots 1.18.1에서 compat 없이 컴파일하면 로그에 이렇게 나옵니다. "Package pgfplots Warning: running in backwards compatibility mode (unsuitable tick labels; missing features). Consider writing \pgfplotsset{compat=1.18} into your preamble." 경고문이 써야 할 번호까지 알려 주므로 그대로 따르면 됩니다.

latex
\usepackage{pgfplots}
\pgfplotsset{compat=1.18}   % pin the release whose defaults you want

이런 장치가 있는 이유는 pgfplots가 판을 거듭하며 자신의 기본값 자체를 바꿔 왔기 때문입니다. 눈금 라벨의 서식이나 축이 잡는 여백이 달라지면 같은 소스에서 나오는 그림도 달라집니다. compat은 "어느 판의 관례로 조판할지"를 선언하는 열쇠이며, 5년 전 원고를 다시 컴파일해도 그림이 움직이지 않도록 보장합니다. TeX Live 2024에 들어 있는 것은 pgfplots 1.18.1(2021년 5월 15일) 이므로 이 환경에서 지정할 수 있는 최신값은 compat=1.18입니다. 새 문서에는 반드시 이 한 줄을 넣고, 기존 문서에서는 함부로 올리지 않는 것이 정석입니다. 올리는 순간 그림이 미세하게 어긋날 수 있습니다.

axis\addplot — 동작하는 최소 그래프

tikzpicture 안에 axis 환경을 하나 두면, 그 안에서 \addplot을 호출할 때마다 곡선이 하나씩 겹칩니다. 역할 분담이 명확합니다. 축 주변(라벨, 눈금, 격자, 범례 위치)은 axis의 옵션이고, 곡선 하나하나의 겉모습(색, 마커, 선 모양)은 \addplot의 옵션입니다. 이 두 층 구조만 잡히면 나머지는 옵션 이름을 찾는 일뿐입니다.

latex
\begin{tikzpicture}
  \begin{axis}[
    xlabel = {$x$},
    ylabel = {$f(x)$},
    title  = {A parabola},
    grid   = major,
  ]
    \addplot[blue, domain=-3:3, samples=100] {x^2};
    \addlegendentry{$x^2$}
  \end{axis}
\end{tikzpicture}

이 코드는 −3 ≤ x ≤ 3 구간의 포물선 y = x²를 100점으로 샘플링해 매끄러운 파란 곡선으로 그립니다. grid=major는 주 눈금을 따라 옅은 격자를 놓고, \addlegendentry는 범례 한 줄을 더합니다. 세부 사항 두 가지만 짚겠습니다. 라벨과 제목은 반드시 중괄호로 감싸세요. xlabel={$x$}처럼 쓰면 안에 쉼표나 ]가 있어도 옵션 해석이 깨지지 않습니다. 범례는 곡선마다 \addlegendentry{…}를 두는 방식과 axis 쪽에 legend entries={A,B,...}로 한꺼번에 쓰는 방식 두 가지가 있고, 위치는 legend pos=north west 등으로 지정합니다.

옵션역할
xlabel= / ylabel=x축과 y축 라벨. xlabel={$x$}처럼 중괄호로 감싸면 수식도 안전
title=플롯 위에 놓는 제목
xmin= xmax= ymin= ymax=보이는 범위를 고정. 데이터가 늘어도 축이 움직이지 않음
grid=grid=major는 주 눈금에, grid=both는 보조 눈금까지 격자를 그림
legend pos=범례 위치. north west 등이며 outer north east는 틀 바깥으로 뺌
xtick=눈금 위치를 명시(xtick={0,1,2}). xtick=data는 데이터점에 맞춤
width= / height=그림의 완성 치수. width=\linewidth는 판면 폭에 맞춤
ybar / xbar세로/가로 막대그래프로 만듦. \addplot을 겹치면 자동으로 묶임

domainsamples의 기본값, 그리고 sin이 맞지 않는 이유

기본값은 구간 −5:5를 25점으로, 그리고 삼각함수는 도 단위입니다. 앞의 것은 곡선이 각져 보이는 이유를 곧바로 설명해 주고, 뒤의 것은 pgfplots에서 "그래프 모양이 이상하다"의 가장 흔한 원인입니다. 재미있는 점은 이 두 기본값이 pgfplots의 것이 아니라는 사실입니다. pgfplots/pgfplots/samples/.initial은 비어 있고, 값은 아래 계층인 PGF에서 내려옵니다. TeX Live 2024의 tikz.code.tex\def\tikz@plot@samples{25}\def\tikz@plot@domain{-5:5}가 나란히 있고, 그것이 실체입니다.

도 단위 이야기는 숫자로 보는 것이 가장 빠릅니다. TeX Live 2024에서 \pgfmathparse{sin(1)}을 평가하면 0.01746, \pgfmathparse{sin(deg(1))}0.84143이 됩니다. 앞은 1도의 사인, 뒤는 1라디안의 사인이고, deg()는 라디안을 도로 바꾸는 변환기입니다. deg(1) = 57.29578입니다. 따라서 \addplot {sin(x)}라고 쓰면 x = 1 자리에 sin 1°가 오고, 주기가 360인 낯선 파형이 나옵니다. 올바른 형태는 sin(deg(x))입니다.

latex
% wrong: pgfmath reads the argument as degrees
\addplot[domain=0:2*pi, samples=200] {sin(x)};
% right: convert radians to degrees first
\addplot[red, domain=0:2*pi, samples=200] {sin(deg(x))};

\addplot에 데이터를 주는 세 가지 방법 — 함수, 좌표, .dat 파일

식을 {...}에 쓰기, coordinates {...}에 점을 나열하기, table {파일명}으로 파일 읽기 세 가지입니다. 앞의 둘은 위에서 보았습니다. 실무에서 가장 많이 쓰는 것은 셋째로, 공백으로 구분된 텍스트 파일이면 첫 줄을 열 이름으로 보고 기본적으로 1열을 x, 2열을 y로 그립니다.

data.dat
x   y
0   0.0
1   0.8
2   0.9
3   0.1
4  -0.8
5  -1.0
latex
\begin{tikzpicture}
  \begin{axis}[xlabel={$x$}, ylabel={$y$}, grid=major]
    \addplot[mark=square, teal] table {data.dat};
    % naming the columns explicitly is the safer habit:
    % \addplot table[x=x, y=y] {data.dat};
  \end{axis}
\end{tikzpicture}

열 이름은 대소문자를 구분합니다. 헤더가 x인데 table[x=X, y=y]라고 쓰면 TeX Live 2024에서는 이렇게 멈춥니다. "! Package pgfplots Error: Sorry, could not retrieve column 'X' from table '...'. Please check spelling (or introduce name aliases)." 실패 중에서는 친절한 축입니다. 메시지가 철자를 확인하거나 별칭을 도입하라고 대처법까지 알려 주니까요. 쉼표로 구분된 CSV라면 table[col sep=comma, x=x, y=y] {data.csv}로 쓰고, #%로 시작하는 줄은 주석으로 건너뜁니다. 데이터를 재구성하거나 계산 열을 만드는 일은 자매 패키지 pgfplotstable의 몫입니다.

막대그래프, 로그 축, \addplot3로 그리는 3D

막대그래프는 axisybar를 더하기만 하면 되고, 로그 축은 axis를 다른 환경 이름으로 바꾸기만 하면 되며, 3D는 \addplot3를 쓰기만 하면 됩니다. 새 문법을 다시 익힐 필요가 없다는 점이 pgfplots 설계의 장점이며, \addplot의 작성법은 그대로 통합니다. 막대그래프에서는 \addplot을 여러 개 겹치면 자동으로 가로로 어긋나 묶음 막대가 되고, 연도 같은 문자열을 x축에 쓰려면 symbolic x coordsxtick=data를 함께 씁니다.

latex
\begin{tikzpicture}
  \begin{axis}[
    ybar,
    xlabel = {Year}, ylabel = {Count},
    symbolic x coords = {2023, 2024, 2025},
    xtick = data,
  ]
    \addplot coordinates {(2023,40) (2024,55) (2025,72)};
  \end{axis}
\end{tikzpicture}

로그 축은 환경 이름을 바꿉니다. 양쪽 로그는 loglogaxis, x축만 로그는 semilogxaxis, y축만 로그는 semilogyaxis입니다. 3D에서는 \addplot3를 쓰면 axis가 저절로 3차원이 되며, 곡면은 surf, 와이어프레임은 mesh를 지정하고 함수는 xy 두 변수로 씁니다. 시점은 view={방위각}{고도각}으로 돌립니다.

latex
\begin{tikzpicture}
  \begin{loglogaxis}[xlabel={$x$}, ylabel={$y$}]
    \addplot[domain=1:1000, samples=50] {1/x};
  \end{loglogaxis}
\end{tikzpicture}

\begin{tikzpicture}
  \begin{axis}[xlabel={$x$}, ylabel={$y$}, zlabel={$z$}]
    \addplot3[surf, samples=30, domain=-3:3]
      {exp(-x^2 - y^2)};
  \end{axis}
\end{tikzpicture}

gnuplot은 TeX Live에 들어 있지 않습니다

중요한 전제이므로 먼저 밝힙니다. \addplot gnuplot {...}를 쓰려면 gnuplot을 따로 직접 설치해야 합니다. TeX Live 2024의 바이너리 디렉터리를 들여다보면 asy(Asymptote)와 mpost(MetaPost)는 있지만 gnuplot은 없습니다. gnuplot은 TeX과 무관하게 개발되는 독립 플롯 프로그램이며, Homebrew나 배포판의 패키지 관리자로 설치해야 합니다.

그렇다면 무엇을 위해 부를까요. pgfplots 내장 파서는 TeX 위에서 돌아가는 탓에 복잡한 식이나 아주 많은 표본에는 어울리지 않습니다. \addplot gnuplot {...}이라고 쓰면 그 수치 계산을 gnuplot에 외주로 넘기고, pgfplots는 돌아온 좌표만 그립니다. 구조는 놀랄 만큼 소박합니다. TeX가 gnuplot용 스크립트를 파일로 써 놓고, gnuplot이 그것을 실행해 수치 표를 파일에 쓰며, TeX가 다시 읽어 들입니다. TeX Live 2024에서 \addplot[blue] gnuplot[domain=0:10] {sin(x)};를 처리했을 때 생성된 .gnuplot 파일에는 이런 지시가 늘어서 있었습니다(작업 이름은 job으로 바꾸었습니다). set table "job.pgf-plot.table"; set format "%.7e"; set samples 25; set dummy x; plot [x=0:10] sin(x);

이 한 줄에서 두 가지를 읽을 수 있습니다. 첫째, samples의 기본값 25가 그대로 gnuplot 쪽에도 넘어간다는 것. 둘째, 식이 gnuplot의 문법 그대로 전달된다는 것입니다. 그래서 거듭제곱은 pgfplots^가 아니라 gnuplot식 **를 쓰고, 삼각함수는 라디안이 기본입니다. 같은 사인 곡선도 내장 파서에서는 {sin(deg(x))}, gnuplot 경유에서는 {sin(x)}로 나뉩니다. 그리고 TeX가 외부 프로그램을 실행해야 하므로 --shell-escape가 필수입니다(-write18이라고도 합니다).

latex
% compile with:  pdflatex --shell-escape document
\begin{tikzpicture}
  \begin{axis}[xlabel={$x$}, ylabel={$y$}]
    % gnuplot syntax: radians, and ** for powers
    \addplot[blue] gnuplot[domain=0:10] {sin(x)};
  \end{axis}
\end{tikzpicture}

gnuplot이 없는 환경에서 실행하면 TeX Live 2024는 이렇게 보고합니다. "! Package pgfplots Error: Sorry, the gnuplot-result file 'job.pgf-plot.table' could not be found. Maybe you need to enable the shell-escape feature? For pdflatex, this is '>> pdflatex -shell-escape'." gnuplot이 정말 없다면 그 바로 위에 셸 자체의 "sh: gnuplot: command not found"도 나옵니다. 주목할 것은 메시지가 이어서 알려 주는 내용입니다. 생성된 파일에 대해 손으로 gnuplot <file>.gnuplot을 실행해도 된다는 것입니다. pgfplots가 정말로 필요로 하는 것은 .table 파일뿐이며, 실제로 손으로 쓴 .table을 놓고 다시 컴파일하니 gnuplot 없이도 그림이 나왔습니다. gnuplot을 설치할 수 없는 CI에서는 생성된 .table을 저장소에 함께 넣는 것이 현실적인 우회책이 됩니다.

R과 Python의 그림을 글꼴째로 가져오기

분석은 이미 R이나 Python에서 끝났지만 PNG를 붙이고 싶지는 않을 때, 답은 각 도구가 TikZ/PGF 코드를 내보내게 하고 \input으로 가져오는 것입니다. 그림이 문서의 일부로 조판되므로 서체도 수식도 본문과 맞습니다. 다만 이들은 모두 TeX Live에 포함되지 않는 외부 도구이므로, 각 언어의 패키지 관리 도구에서 따로 설치해야 합니다.

R에서는 CRAN의 tikzDevice가 R의 표준 그래픽 출력(base 플롯도 ggplot2도)을 TikZ 코드로 쓰는 그래픽 장치를 제공합니다. tikz()로 장치를 열고 평소의 작도 코드를 돌린 뒤 dev.off()로 닫으면 .tex를 얻습니다. 이 장치의 묘수는 글자를 배치할 때 LaTeX에 문자열 폭과 글꼴 메트릭을 물어본다는 점이고, 바로 그래서 출력이 본문 서체와 정확히 맞고 축 라벨에 LaTeX 수식을 넣을 수 있습니다. standAlone=TRUE로 하면 단독으로 컴파일 가능한 완결된 문서로 나옵니다.

R
library(tikzDevice)
tikz("plot.tex", width = 4, height = 3)
plot(cars$speed, cars$dist,
     xlab = "Speed", ylab = "Distance")
dev.off()

Python 쪽에는 길이 둘 있고, 서로 혼동되기 쉽습니다. 첫째는 matplotlib에 기본으로 들어 있는 pgf 백엔드로, 추가 패키지가 필요 없습니다. matplotlib.use("pgf")savefig("figure.pgf")를 하면 .pgf 파일이 나옵니다. 여기가 핵심인데, 이것이 뱉어 내는 것은 pgfplots 코드가 아니라 순수 PGF입니다. matplotlib 3.11.0으로 실제 생성한 파일을 살펴보면 \addplotaxis 환경도 하나 없고, 본문은 \pgfqpoint, \pgfpathlineto, \pgfsetstrokecolor 같은 저수준 그리기 명령의 나열이었습니다. 다시 말해 좌표축은 pgfplots가 조판한 축이 아니라 선으로 그린 축입니다. 문서 쪽에 필요한 것도 \usepackage{pgfplots}가 아니라 \usepackage{pgf} 뿐입니다.

python
import matplotlib
matplotlib.use("pgf")
import matplotlib.pyplot as plt

matplotlib.rcParams.update({
    "pgf.texsystem": "pdflatex",   # default here is xelatex
    "font.family": "serif",
    "text.usetex": True,
    "pgf.rcfonts": False,
})

fig, ax = plt.subplots(figsize=(4, 3))
ax.plot([0, 1, 2, 3], [0, 1, 4, 9])
ax.set_xlabel(r"$x$")
ax.set_ylabel(r"$x^2$")
fig.savefig("figure.pgf")

여기에 함정이 하나 있습니다. 생성된 .pgf는 첫머리 주석에서 프리앰블에 \usepackage{pgf}를 넣으라고 안내하는데, 그대로 하고 \input{figure.pgf}를 하면 TeX Live 2024에서는 멈춥니다. "! Undefined control sequence. <recently read> \mathdefault" 원인은 matplotlib이 눈금 라벨을 \mathdefault{...}로 감싸 내보내면서 그 정의를 .pgf 안에 넣어 주지 않기 때문입니다. matplotlib의 backend_pgf.py는 자신이 PDF를 만들 때만 내부 프리앰블에 정의를 주입합니다. 해결은 한 줄, 프리앰블에 \providecommand{\mathdefault}[1]{#1}을 쓰면 통과합니다. 또한 pgf.texsystem의 기본값은 xelatex이므로, 본문을 pdfLaTeX로 조판한다면 위 예처럼 명시해 두는 편이 안전합니다.

latex
\documentclass{article}
\usepackage{pgf}
\usepackage{lmodern}
% matplotlib wraps tick labels in \mathdefault but never defines it
\providecommand{\mathdefault}[1]{#1}
\begin{document}
\input{figure.pgf}
\end{document}

Python의 다른 길은 tikzplotlib(옛 이름 matplotlib2tikz)이며, matplotlib 그림을 pgfplots 코드로 변환합니다. tikzplotlib.save("figure.tex")로 내보낸 뒤, 문서 쪽에서 \usepackage{pgfplots}\pgfplotsset{compat=...}를 불러오고 \input{figure.tex}합니다. 좌표축이 순수 PGF가 아니라 실제 axis 환경이므로 생성 후에도 LaTeX 쪽에서 눈금과 범례를 손볼 수 있다는 점이 pgf 백엔드와의 결정적 차이입니다. 다만 tikzplotlib는 현재 유지보수가 멈췄고, 포크인 matplot2tikz가 거의 같은 API로 후속 개발되고 있습니다. 둘 다 PyPI에서 설치하며 TeX Live에는 포함되지 않습니다.

점이 너무 많아 컴파일이 끝나지 않을 때

먼저 점을 솎아내는 것. 이것이 가장 효과가 큽니다. pgfplots의 계산은 전부 TeX 매크로 확장 위에서 이루어지므로 수만 점의 산점도는 솔직히 느리고, TeX의 메모리 한계에 닿기도 합니다. 게다가 종이에 인쇄했을 때 이웃한 점의 간격이 인쇄 해상도보다 작다면 그 점들은 어차피 보이지 않습니다. 아래는 효과가 큰 순서에 가깝게 나열한 것입니다.

  • 점을 솎아내기. each nth point=k로 k번째 점마다 그리고, filter discard if not 같은 필터로 범위 밖 데이터를 버립니다. 산점도라면 겉보기는 거의 달라지지 않습니다.
  • 외부화(externalization). \usepgfplotslibrary{external}\tikzexternalize를 쓰면 각 그림이 한 번만 별도 PDF로 컴파일되고 이후에는 포함만 됩니다. 본문 재컴파일이 극적으로 가벼워지는 대신 --shell-escape가 필요합니다.
  • 메모리가 더 큰 엔진. lualatex는 TeX 메모리 제한이 느슨해 큰 그림에서 막힐 확률이 낮습니다.
  • 애초에 앞 단계에서 그려 두기. 끝내 무겁다면 R, Python, gnuplot에서 그린 결과를 가져오는 편(앞 절)이 현실적입니다.

외부화와 gnuplot은 둘 다 --shell-escape를 요구하므로, CI나 컨테이너에서 빌드한다면 그쪽에서도 명시적으로 켜 두어야 합니다. 그러지 않으면 로컬에서 통과한 빌드가 저쪽에서 무너집니다. 매우 흔한 사고이니 Docker / CI 설정은 관련 페이지에서 확인해 두는 것이 좋습니다.