달러 기호 자체에는 수식을 여는 힘이 없습니다. 그 역할을 !에 넘기면 !x_1!이 오류 하나 없이 수식으로 조판되는데, 역할을 정하는 것은 문자가 아니라 카테고리 코드(catcode), 즉 TeX가 읽어 들인 문자마다 붙이는 0부터 15까지의 번호입니다. 이 번호 하나로 LaTeX의 내부 명령에 @가 넘쳐 나는 이유도, \makeatletter가 실제로 하는 일도, %가 아무 말 없이 줄의 나머지를 삼키는 이유도 모두 설명됩니다. 이 페이지에서는 열여섯 개 코드의 배정표, 이를 고쳐 쓰는 원시 명령 \catcode, 단 한 줄뿐인 \makeatletter의 실체, 그리고 \string・\meaning・\detokenize로 토큰의 정체를 들여다보는 방법까지 다룹니다.
열여섯 개의 카테고리 코드와 기본 배정
카테고리 코드는 0부터 15까지 열여섯 가지이며, TeX가 원고를 토큰 열로 잘라 내는 단계, 즉 조판보다 훨씬 앞선 어휘 분석에서 쓰입니다. 읽어 들인 문자가 명령의 시작인지, 그룹을 여는지, 그저 잉크인지는 이 번호 하나로 결정됩니다. 일상적으로 의식하게 되는 것은 명령을 시작하는 0, 그룹의 1・2, 수식의 3, 표의 셀 구분자 4, 문단을 만드는 줄끝 5, 매크로 인수 표시 6, 위첨자와 아래첨자의 7・8, 본문을 이루는 10・11・12, 그리고 주석의 14입니다.
| catcode | 역할 | 기본 문자(LaTeX 문서 안) |
|---|---|---|
0 | 이스케이프(명령의 시작) | 백슬래시 \ |
1 | 그룹 시작 | 왼쪽 중괄호 { |
2 | 그룹 끝 | 오른쪽 중괄호 } |
3 | 수식 모드 전환 | 달러 기호 $ |
4 | 정렬 탭(표의 셀 구분자) | 앰퍼샌드 & |
5 | 줄끝(빈 줄이 새 문단을 만듦) | 캐리지 리턴(문자 13) |
6 | 매개변수(매크로 인수) | 해시 # |
7 | 위첨자 | 캐럿 ^ |
8 | 아래첨자 | 밑줄 _ |
9 | 무시됨(없는 것처럼 건너뜀) | LaTeX에서는 기본적으로 어떤 문자도 배정되지 않음 |
10 | 공백 | 스페이스와 탭(문자 9) |
11 | 문자(명령 이름을 이룰 수 있음) | a–z, A–Z |
12 | 기타(그저 잉크) | 숫자, 문장부호, @ 등 나머지 전부 |
13 | 활성 문자(문자 자체가 명령) | 틸드 ~와 제어 문자 1–31 |
14 | 주석(줄의 나머지를 버림) | 퍼센트 % |
15 | 유효하지 않음(오류 발생) | 널 문자(0)와 삭제 문자(127) |
9번 행은 주의해서 볼 필요가 있습니다. 많은 설명이 “catcode 9는 널 문자”라고 적지만, 그것은 맨 TeX(IniTeX) 이야기입니다. TeX Live 2024의 LaTeX에서 0~255 전체 문자를 훑어보면 catcode 9를 가진 문자는 하나도 없습니다. latex.ltx가 널 문자를 15(무효)로 옮기고 9는 비워 둔 것으로, 열여섯 카테고리 중 기본 거주자가 없는 유일한 자리입니다. 직접 문자를 배정해야 비로소 작동합니다. 실제로 listings 패키지는 자기 소스를 읽는 동안 탭・폼피드・캐리지 리턴을 catcode 9로 낮추어 건너뜁니다. 반대쪽에서 제어 문자 1–31이 활성(13) 인 것은 UTF-8 입력을 처리하기 위한 현대 LaTeX의 장치입니다.
\catcode로 문자의 역할 바꾸기
문자와 카테고리의 대응은 고정되어 있지 않고, 원시 명령 \catcode 로 바꿔 쓸 수 있습니다. 형태는 “\catcode 뒤에 백틱, 대상 문자, =, 새 번호”이고, \the\catcode에 같은 백틱과 문자를 이어 붙이면 현재 값을 읽어 올 수 있습니다. 아래 코드는 수식 모드 전환 역할(catcode 3)을 !에 넘긴 것으로, 오류는 0건이며 !x_1!도 !\frac{a}{b}!도 제대로 수식으로 조판됩니다. $가 수식을 여는 것은 “달러 기호라서”가 아니라 “catcode 3이라서”입니다. 한 줄로 그것을 증명할 수 있다는 점이 이 장치의 매력입니다.
\documentclass{article}
\begin{document}
\catcode`\!=3 % hand the math-shift job to "!"
!x_1! and !\frac{a}{b}! % typeset as mathematics, zero errors
\catcode`\!=12 % give it back to "other"
Back to normal!
% read a catcode back
\the\catcode`\$ % 3
\the\catcode`\% % 14
\the\catcode`A % 11
\end{document}다만 이것은 너무 잘 듣는 도구입니다. 카테고리는 토큰화 시점에 얼어붙으므로, 고쳐 쓴 순간부터 이후에 읽히는 모든 것의 의미가 바뀝니다. 되돌리는 것을 잊으면 전혀 무관한 곳이 깨집니다. 게다가 변경은 그룹({ }이나 환경)을 벗어나면 원래대로 돌아가므로 “프리앰블에서는 됐는데 본문에서는 안 된다”는 혼란도 생깁니다. 날것의 \catcode는 아래의 \makeatletter 같은 정형화된 포장에 담기지 않을 때의 최후 수단으로 여기세요. 물론 패키지들은 이 일을 일상적으로 합니다. babel의 독일어 옵션을 읽어 들이면 "가 catcode 13(활성) 이 되어 "a가 ä로, 큰따옴표와 백틱을 이으면 „로 바뀝니다. 독일어 문서에서 따옴표 동작이 갑자기 달라 보이는 이유가 이것입니다.
%가 조용히 줄을 삼키는 이유, &가 오류를 내는 이유
답은 catcode 값 자체에 있습니다. %는 catcode 14 로 “여기서 줄 끝까지 전부 버려라”라는 지시이며, 버린 사실을 전혀 보고하지 않습니다. 그래서 Only 50% of the sample survived.라고 쓰고 다음 줄에 The rest did not.을 이으면 출력은 Only 50The rest did not.이 되고 오류도 경고도 0건입니다. 본문 한 줄이 사라졌는데 로그는 완전히 조용합니다. LaTeX 전체에서 가장 발견하기 어려운 사고 중 하나이므로, 본문에 퍼센트 기호를 쓸 때는 반드시 \%로 이스케이프하세요. 반면 &는 catcode 4(표의 셀 구분자)라서 표 바깥에 쓰면 곧바로 ! Misplaced alignment tab character &.가 나옵니다. $는 catcode 3이며, 닫는 것을 잊으면 ! Missing $ inserted가 됩니다.
% catcode 14: everything after % on this line is discarded, silently
Only 50% of the sample survived.
The rest did not.
% output: "Only 50The rest did not." -- no error, no warning
% catcode 4 outside a table:
Smith & Sons % ! Misplaced alignment tab character &.
% the fix in running text:
Only 50\% of the sample survived.catcode 11이 명령 이름의 끝을 정합니다
TeX의 제어어(control word), 곧 \section 같은 이름 있는 명령은 catcode 0 문자(보통 \) 뒤에 이어지는 catcode 11 문자들의 연속 으로 인식됩니다. 원칙적으로 명령 이름을 만들 수 있는 것은 catcode 11 문자뿐이며, \section의 이름이 section에서 끝나는 것도 바로 다음에 오는 공백이나 {가 catcode 11이 아니기 때문입니다. 숫자와 문장부호는 catcode 12(기타) 이므로 \a2는 “\a라는 명령 + 문자 2”로 읽힙니다. 이를 뒤집으면 핵심 수가 나옵니다. 어떤 문자의 catcode를 11로 바꾸면 그 문자를 명령 이름에 넣을 수 있습니다. 다음 절의 @ 이야기가 바로 이 한 수에서 시작합니다. (제어어 뒤의 공백이 사라지는 것, \LaTeXlogo가 미정의가 되는 것 같은 사용자 층위의 결과는 “문법 규칙” 페이지가 맡습니다.)
\makeatletter란 무엇인가 — 실체는 단 한 줄
\makeatletter는 @의 catcode를 11로 만드는 것뿐인 명령이고, 짝이 되는 \makeatother는 다시 12로 되돌리는 것뿐입니다. latex.ltx의 정의는 각각 말 그대로 한 줄이며, 이름이 그대로 뜻입니다. @를 letter로, @를 other로. 왜 필요할까요? LaTeX 커널과 패키지에는 이름에 @가 들어가는 내부 명령 이 가득하기 때문입니다. \@startsection(절 제목 조립), \@ifnextchar(다음 토큰을 보고 분기), \@maketitle(제목 영역) 등 수백 개에 이릅니다. \usepackage와 \documentclass가 .sty나 .cls를 읽는 동안 @를 catcode 11로 바꿔 주므로, 패키지 안에서는 이 이름들이 하나의 명령으로 해석됩니다.
% the whole of \makeatletter and \makeatother, in the LaTeX kernel
\DeclareRobustCommand\makeatletter{\catcode`\@11\relax}
\DeclareRobustCommand\makeatother{\catcode`\@12\relax}
% two abbreviations you will meet in internal code (note the values)
\newdimen\p@ \p@=1pt % 1pt -- "this saves macro space and time"
\newdimen\z@ \z@=0pt % 0pt, and doubles as the integer 0보통 문서 안에서 @는 catcode 12(기타) 입니다. 그래서 본문이나 프리앰블에 \p@라고 그대로 쓰면 TeX는 “\p라는 명령 + 문자 @”로 읽고 ! Undefined control sequence.로 멈춥니다. 오류 표시는 l.3 Value: \p에서 끊기고 다음 줄이 @로 시작합니다. 명령 이름이 어디서 잘렸는지가 눈으로 보이므로, 이 증상을 한 번 익혀 두면 진단이 빨라집니다. 참고로 \p@는 1pt이고 0pt를 뜻하는 것은 \z@입니다. 둘을 혼동하는 실수가 흔합니다. latex.ltx에 지금도 남아 있는 “매크로 공간과 시간을 절약한다”는 주석은 메모리가 귀하던 시절의 흔적이 그대로 현역으로 돌아가고 있다는 증거이기도 합니다.
! Undefined control sequence.
l.3 Value: \p
@
The control sequence at the end of the top line
of your error message was never \def'ed.\makeatletter가 필요한 경우와 써서는 안 되는 경우
실무적인 판단은 한 문장으로 끝납니다. .tex 문서(거의 언제나 프리앰블)에서 이름에 @가 들어간 내부 명령을 쓸 때만 필요합니다. 그 밖에는 쓰지 않습니다. 특히 .sty/.cls 안에는 쓰지 마세요. 읽어 들이는 동안 이미 @가 catcode 11이므로 불필요하고, 불필요한 \makeatother가 이후 처리를 망가뜨릴 수 있습니다. 전형적인 용도는 클래스가 정의한 내부 매크로를 프리앰블에서 조금 손보는 것입니다. 다음 예는 제목 영역을 조판하는 내부 매크로 \@maketitle을 포장 안에서 다시 정의합니다.
\documentclass{article}
\makeatletter % @ becomes a letter here
\renewcommand{\@maketitle}{% % redefine the internal title block
\begin{center}
{\LARGE\bfseries \@title}\par
\vspace{1ex}{\large \@author}\par
\end{center}%
}
\makeatother % ... and goes back to "other" here
\title{Category codes}
\author{A. Author}
\begin{document}
\maketitle
\end{document}함정은 두 가지입니다. 첫째는 \makeatother를 잊는 것입니다. 닫지 않으면 @가 문자 상태로 뒤까지 이어져 본문의 이메일 주소나 @를 특별히 다루는 패키지가 깨집니다. 하나로 열면 다른 하나로 닫는다고 기계적으로 외우세요. 둘째는 애초에 내부 명령을 건드리지 않고 끝낼 길이 없는지 확인하지 않는 것입니다. 공개 명령을 \renewcommand로 덮어쓸 수 있거나 원하는 기능을 제공하는 정식 패키지가 있다면 그쪽이 언제나 더 안전합니다. 내부 명령은 패키지 갱신과 함께 예고 없이 바뀌고, \makeatletter 안에 쓴 재정의는 다음 릴리스에서 조용히 듣지 않게 됩니다.
\verb와 listings가 catcode를 건드려야 하는 이유
소스를 있는 그대로 인쇄하려면 방법은 하나뿐입니다. 특수 문자가 특수하기를 그만두게 하는 것입니다. \verb의 정의가 바로 그 일을 하며, verbatim 환경도 블록 전체에 같은 일을 합니다. \dospecials가 나열하는 열한 개 문자, 곧 공백・\・{・}・$・&・#・^・_・%・~에 \@makeother를 적용해 전부 catcode 12(기타)로 낮춥니다. 이어서 \@noligs는 반대 방향으로 여섯 문자, 곧 백틱・<・>・,・아포스트로피・-를 catcode 13(활성) 으로 만들어, 백틱 두 개가 둥근 여는 따옴표로 합쳐지지 않게 합니다. 즉 \verb는 catcode를 낮추는 동시에 올리는 이중 조작이며, 전체가 \bgroup에 싸여 있어 끝나면 자동으로 원래대로 돌아갑니다.
\def\@makeother#1{\catcode`#1=12\relax}
\def\dospecials{\do\ \do\\\do\{\do\}\do\$\do\&%
\do\#\do\^\do\_\do\%\do\~}
\def\verb{\relax\ifmmode\hbox\else\leavevmode\null\fi
\bgroup % everything below is local
\verb@eol@error \let\do\@makeother \dospecials % all 11 -> catcode 12
\verbatim@font\@noligs % ` < > , ' - -> catcode 13
\language\l@nohyphenation
\@ifstar\@sverb\@verb}여기서 \verb의 유명한 제약이 따라 나옵니다. \verb는 다른 명령의 인수 안에서는 쓸 수 없습니다. 인수는 매크로가 호출되기 전에 토큰화되므로, \verb가 catcode를 바꿀 무렵에는 이미 늦었습니다. \footnote{code: \verb|\foo_bar|}는 ! Undefined control sequence.(\foo가 진짜 명령으로 읽힘)에 이어 ! Missing $ inserted.(_가 아래첨자로 읽힘)를 내고, 매크로 인수 안에서는 ! Extra }, or forgotten $.까지 붙습니다. 우회로는 listings의 \lstinline입니다. \section{A \lstinline|x_1| heading}은 오류 0건으로 컴파일되고 목차에도 그대로 실립니다. listings가 자체적으로 마흔 곳 넘게 catcode를 조작하는 것은 바로 이런 문제를 떠맡기 위해서입니다.
% BREAKS: the argument is tokenized before \verb can act
\footnote{code: \verb|\foo_bar|}
% ! Undefined control sequence. <argument> ... \verb |\foo
% ! Missing $ inserted.
% WORKS: \lstinline survives inside a moving argument
\usepackage{listings}
\section{A \lstinline|x_1| heading} % zero errors, reaches the ToC토큰의 정체 보기 — \string・\meaning・\detokenize
카테고리 코드가 여전히 추상적으로 느껴진다면 가장 빠른 해결책은 직접 들여다보는 것입니다. \meaning은 catcode를 그대로 말로 돌려줍니다. \meaning A는 the letter A(catcode 11이라 “letter”), \meaning 7은 the character 7(catcode 12), \meaning\bgroup은 begin-group character {를 냅니다. 한마디로 그 문자가 지금 무엇인지 알 수 있습니다. \string은 명령을 백슬래시를 포함한 문자들로 풀고, \detokenize는 인수 전체를 인쇄 가능한 문자열로 바꿉니다. 출력에서 #이 ##이 되는 것은 토큰 열로서 정확함을 유지하기 위한 이중화입니다. 디버깅에는 \show도 유용합니다. 정의를 로그에 쓰고 일시 정지합니다.
| 명령 | 하는 일 | TeX Live 2024에서 실측한 출력 |
|---|---|---|
\the\catcode | 문자의 catcode를 숫자로 읽어 옴 | \the\catcode + 백틱 + $ → 3 |
\meaning | 토큰의 정체를 말로 설명 | \meaning A → the letter A, \meaning 7 → the character 7 |
\string | 명령을 문자열로 풀어냄(\ 포함) | \string\frac → \frac |
\detokenize | 인수 전체를 인쇄 가능한 문자열로 변환 | \detokenize{\frac{1}{2} #1} → \frac {1}{2} ##1 |
\show | 정의를 로그에 쓰고 일시 정지 | \show\LaTeX → macro:->\protect \LaTeX |
마지막으로 실무 감각 하나. “왜인지 이 문자만 이상하다”는 문제에 부딪히면 먼저 \the\catcode로 그 문자의 번호를 확인하세요. babel, csquotes, listings, hyperref 같은 패키지는 각자의 목적을 위해 특정 문자를 활성으로 만들거나 catcode 12로 낮춥니다. 증상이 “특정 한 문자만 깨진다”는 형태라면 원인은 십중팔구 카테고리 코드입니다. 그것만 알면 해결은 세 갈래로 좁혀집니다. 읽어 들이는 순서를 바꾸거나, 그 문자를 피하거나, 패키지가 제공하는 정식 탈출구(\%, \verb의 다른 구분 기호, \lstinline)를 쓰는 것입니다.