! Undefined control sequence는 거의 모든 사람이 처음 만나는 LaTeX 오류입니다. 다만 엄밀히 말하면 LaTeX의 오류가 아닙니다. LaTeX을 전혀 읽어들이지 않은 순수 TeX도 같은 세 단어를 그대로 찍습니다. control sequence(제어열)는 “역슬래시 뒤에 붙은 이름”을 가리키는 TeX 자신의 용어이고, 이 메시지는 그 이름이 사전에 없다는 말만 하고 있을 뿐입니다. 그런데도 이것이 검색량이 가장 많은 오류인 이유는 그다음에 벌어지는 일 때문입니다. TeX은 멈추지 않습니다. 모르는 명령을 조용히 버리고 계속 읽어 나가며, 명령의 인수가 본문 글자로 바뀐 PDF를 아무렇지 않게 건네줍니다. 이 페이지에서는 거의 모든 사례를 차지하는 세 가지 원인——철자 오류, 불러오지 않은 패키지, 정의보다 먼저 쓴 매크로——과, 로그의 줄 번호가 TeX이 알아챈 자리일 뿐 틀린 자리가 아닐 수도 있다는 함정을 다룹니다.
메시지 읽는 법: 줄이 꺾인 자리가 범인입니다
두 줄로 꺾인 것 중 윗줄 끝에 있는 명령이 정의되지 않은 명령입니다. TeX은 오류를 보고할 때 문제의 줄을 “이미 읽은 부분”과 “아직 읽지 않은 부분”으로 갈라 위아래로 늘어놓습니다. 읽기를 멈춘 순간이 곧 문제가 생긴 순간이므로, 그 경계가 그대로 범인을 가리킵니다. 아래는 \textbf를 \textbnf로 잘못 친 실제 로그입니다.
! Undefined control sequence.
l.4 This is \textbnf
{bold} text.l.4는 4행이라는 뜻입니다. 문제가 수식 안에서 일어나면 l.4 위에 <recently read> \fra 같은 줄이 하나 더 붙어 명령 이름만 따로 보여 주기도 합니다. 어느 쪽이든 첫 수순은 그 명령 이름을 자기 원고에서 검색하는 것입니다. 철자가 맞다면 다음 절로 넘어갑니다. 참고로 패키지 이름을 잘못 친 경우에는 이 오류가 아니라 ! LaTeX Error: File 뒤에 찾지 못한 .sty 이름이 붙는 다른 오류가 납니다.
원인은 셋뿐입니다: 철자, 패키지, 정의 순서
실무에서 정의되지 않은 명령의 원인은 사실상 셋뿐입니다. 철자 오류, 패키지를 불러오지 않은 것, 정의보다 먼저 쓴 매크로. 순서도 중요합니다. 위에서부터 차례로 확인하는 편이 가장 빠릅니다. 철자 확인은 몇 초, 패키지 확인은 프리앰블을 한 번 훑으면 끝이고, 매크로만이 손이 좀 갑니다.
- 철자 오류 —
\frac를\fra로,\textbf를\textbnf로,\begin을\begni로. 명령 이름은 대소문자를 구별하므로\LaTeX을\Latex으로 써도 정의되지 않은 명령이 됩니다. - 불러오지 않은 패키지 — 철자는 맞지만 그 명령을 정의하는 패키지를 프리앰블에서
\usepackage로 부르지 않았습니다. 다음 절의 대응표를 보세요. - 정의보다 먼저 쓴 매크로 —
\newcommand를 빠뜨렸거나, 정의한 줄보다 위에서 썼거나, 정의가 중괄호나 환경 안에 있어 그 밖에서는 사라진 경우입니다.
그 명령은 어느 패키지의 것인가
아래 표는 이 이유로 정의되지 않기 쉬운 명령과 그것을 정의하는 패키지의 대응입니다(TeX Live 2024에서 \ifdefined로 직접 확인했습니다). 가장 흔한 혼동은 amsmath와 amssymb입니다. \lVert는 amsmath에 있고 amssymb에는 없으며, \mathbb과 \therefore는 amssymb에 있고 amsmath에는 없습니다. 수식 명령이 “패키지를 불러왔는데도” 정의되지 않는다면 여기부터 의심하세요. 표에 없는 명령은 texdoc PACKAGE로 그 패키지의 설명서를 열어 확인할 수 있습니다.
| 명령 | 패키지 | 비고 |
|---|---|---|
\includegraphics | graphicx | 그림 삽입. 불러오지 않으면 옵션이 본문에 인쇄됩니다 |
\toprule | booktabs | \midrule・\bottomrule도 같습니다 |
\lVert | amsmath | amssymb에는 없습니다. \rVert도 같습니다 |
\mathbb | amssymb | amsmath만으로는 안 됩니다(실체는 amsfonts) |
\therefore | amssymb | \because도 같습니다. amsmath에는 없습니다 |
\bm | bm | 수식 굵은 글자. \boldsymbol은 amsmath |
\coloneqq | mathtools | amsmath만 불러와서는 부족합니다 |
\multirow | multirow | 표 셀의 세로 병합 |
\FloatBarrier | placeins | 플로트가 이 지점을 넘어가지 못하게 합니다 |
\href | hyperref | \url만 필요하면 url 패키지로도 충분합니다 |
\textcolor | xcolor | \definecolor・\colorbox도 같습니다 |
\SI | siunitx | 새 표기는 \qty. 같은 패키지입니다 |
\newcommand가 듣지 않을 때: 순서와 유효 범위
TeX은 위에서 아래로 딱 한 번 읽으므로, 정의는 사용보다 앞에 있어야 합니다. 사람은 원고를 “전체”로 훑어보지만 TeX은 줄을 차례로 읽고 \newcommand에 다다른 그 순간에야 그 이름을 사전에 등록합니다. 그래서 200행에서 정의한 매크로를 40행에서 쓰면 정의되지 않은 명령이 됩니다. 또 하나의 함정은 유효 범위입니다. 중괄호나 환경 안에서 정의한 매크로는 닫는 괄호에서 사라집니다. 두 사고 모두 정의를 프리앰블에 모아 두면 일어나지 않습니다. \newcommand 자체의 사용법(인수, 기본값, \renewcommand와의 차이)은 “매크로 정의” 페이지가 다룹니다.
\documentclass{article}
\begin{document}
% too early: \R is not in the dictionary yet
$\R$
\newcommand{\R}{\mathbb{R}}
% scoped: \tmp dies at the closing brace
{\newcommand{\tmp}{scoped}\tmp}
\tmp
\end{document}보고된 줄이 잘못된 자리가 아닐 때
l.NN 위에 ->가 든 줄이 보이면, 잘못은 그 줄에 이름이 적힌 매크로 안에 있습니다. l.NN은 TeX이 이상을 알아챈 자리, 곧 매크로를 쓴 줄일 뿐입니다. 정의 자체는 수백 줄 떨어져 있을 수도, 패키지 안에 있을 수도 있습니다. 아래 예에서는 \mysq가 \mynorm을 부르고 \mynorm이 \lVert를 쓰는데 amsmath를 불러오지 않았습니다. 로그의 \mynorm #1->\lVert가 유일한 단서입니다.
! Undefined control sequence.
\mynorm #1->\lVert
#1 \rVert
l.5 The value $\mysq{x}
$ is here.이 로그에는 \mysq라는 이름이 아예 나오지 않습니다. LaTeX이 \errorcontextlines를 -1로 설정해 두었기 때문입니다(latex.ltx 535행). 그래서 매크로 호출 사슬 중 가장 안쪽 한 단계만 표시됩니다. 패키지 내부에서 넘어졌을 때는 이것으로 부족합니다. 프리앰블에 \errorcontextlines=999를 넣고 다시 조판하면 \mysq #1->\mynorm {#1} 줄이 더해져 호출 경로 전체가 보입니다. 원인을 찾은 뒤에는 지워 두는 편이 좋습니다. 평소에는 조용한 로그가 읽기 쉽습니다.
PDF는 그래도 만들어집니다. 그것이 진짜 위험입니다
정의되지 않은 명령은 조판을 멈추지 않습니다. TeX은 그 명령 하나만 버리고 나머지를 보통 글자로 조판합니다. 즉 graphicx를 빠뜨린 채 \includegraphics[width=3cm]{example-image}라고 쓰면, 그림이 사라지는 데 그치지 않고 옵션과 파일 이름이 본문으로 인쇄된 PDF가 나옵니다. 에디터와 빌드 도구는 기본적으로 -interaction=nonstopmode로 돌아가므로 오류를 지나쳐 파일을 만들어 냅니다. 경고 더미를 그냥 넘기고 “나온 PDF”를 제출하는 사고가 이렇게 일어납니다. 로그에 정의되지 않은 명령이 하나라도 남아 있다면 그 PDF를 믿어서는 안 됩니다.
% graphicx was never loaded
\includegraphics[width=3cm]{example-image}
% the run still succeeds, and this is what lands on the page:
% [width=3cm]example-image