카테고리 코드와 \makeatletter

달러 기호 자체에는 수식을 여는 힘이 없습니다. 그 역할을 !에 넘기면 !x_1!이 오류 하나 없이 수식으로 조판되는데, 역할을 정하는 것은 문자가 아니라 카테고리 코드(catcode), 즉 TeX가 읽어 들인 문자마다 붙이는 0부터 15까지의 번호입니다. 이 번호 하나로 LaTeX의 내부 명령에 @가 넘쳐 나는 이유도, \makeatletter가 실제로 하는 일도, %가 아무 말 없이 줄의 나머지를 삼키는 이유도 모두 설명됩니다. 이 페이지에서는 열여섯 개 코드의 배정표, 이를 고쳐 쓰는 원시 명령 \catcode, 단 한 줄뿐인 \makeatletter의 실체, 그리고 \string\meaning\detokenize로 토큰의 정체를 들여다보는 방법까지 다룹니다.

열여섯 개의 카테고리 코드와 기본 배정

카테고리 코드는 0부터 15까지 열여섯 가지이며, TeX가 원고를 토큰 열로 잘라 내는 단계, 즉 조판보다 훨씬 앞선 어휘 분석에서 쓰입니다. 읽어 들인 문자가 명령의 시작인지, 그룹을 여는지, 그저 잉크인지는 이 번호 하나로 결정됩니다. 일상적으로 의식하게 되는 것은 명령을 시작하는 0, 그룹의 12, 수식의 3, 표의 셀 구분자 4, 문단을 만드는 줄끝 5, 매크로 인수 표시 6, 위첨자와 아래첨자의 78, 본문을 이루는 101112, 그리고 주석의 14입니다.

catcode역할기본 문자(LaTeX 문서 안)
0이스케이프(명령의 시작)백슬래시 \
1그룹 시작왼쪽 중괄호 {
2그룹 끝오른쪽 중괄호 }
3수식 모드 전환달러 기호 $
4정렬 탭(표의 셀 구분자)앰퍼샌드 &
5줄끝(빈 줄이 새 문단을 만듦)캐리지 리턴(문자 13)
6매개변수(매크로 인수)해시 #
7위첨자캐럿 ^
8아래첨자밑줄 _
9무시됨(없는 것처럼 건너뜀)LaTeX에서는 기본적으로 어떤 문자도 배정되지 않음
10공백스페이스와 탭(문자 9)
11문자(명령 이름을 이룰 수 있음)az, AZ
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이라서”입니다. 한 줄로 그것을 증명할 수 있다는 점이 이 장치의 매력입니다.

document.tex
\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가 됩니다.

document.tex
% 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로 바꿔 주므로, 패키지 안에서는 이 이름들이 하나의 명령으로 해석됩니다.

latex.ltx
% 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에 지금도 남아 있는 “매크로 공간과 시간을 절약한다”는 주석은 메모리가 귀하던 시절의 흔적이 그대로 현역으로 돌아가고 있다는 증거이기도 합니다.

error
! 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을 포장 안에서 다시 정의합니다.

document.tex
\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 안에 쓴 재정의는 다음 릴리스에서 조용히 듣지 않게 됩니다.

\verblistings가 catcode를 건드려야 하는 이유

소스를 있는 그대로 인쇄하려면 방법은 하나뿐입니다. 특수 문자가 특수하기를 그만두게 하는 것입니다. \verb의 정의가 바로 그 일을 하며, verbatim 환경도 블록 전체에 같은 일을 합니다. \dospecials가 나열하는 열한 개 문자, 곧 공백・\{}$&#^_%~\@makeother를 적용해 전부 catcode 12(기타)로 낮춥니다. 이어서 \@noligs는 반대 방향으로 여섯 문자, 곧 백틱・<>,・아포스트로피・-catcode 13(활성) 으로 만들어, 백틱 두 개가 둥근 여는 따옴표로 합쳐지지 않게 합니다. 즉 \verb는 catcode를 낮추는 동시에 올리는 이중 조작이며, 전체가 \bgroup에 싸여 있어 끝나면 자동으로 원래대로 돌아갑니다.

latex.ltx
\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를 조작하는 것은 바로 이런 문제를 떠맡기 위해서입니다.

document.tex
% 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 Athe letter A(catcode 11이라 “letter”), \meaning 7the character 7(catcode 12), \meaning\bgroupbegin-group character {를 냅니다. 한마디로 그 문자가 지금 무엇인지 알 수 있습니다. \string은 명령을 백슬래시를 포함한 문자들로 풀고, \detokenize는 인수 전체를 인쇄 가능한 문자열로 바꿉니다. 출력에서 ###이 되는 것은 토큰 열로서 정확함을 유지하기 위한 이중화입니다. 디버깅에는 \show도 유용합니다. 정의를 로그에 쓰고 일시 정지합니다.

명령하는 일TeX Live 2024에서 실측한 출력
\the\catcode문자의 catcode를 숫자로 읽어 옴\the\catcode + 백틱 + $3
\meaning토큰의 정체를 말로 설명\meaning Athe letter A, \meaning 7the character 7
\string명령을 문자열로 풀어냄(\ 포함)\string\frac\frac
\detokenize인수 전체를 인쇄 가능한 문자열로 변환\detokenize{\frac{1}{2} #1}\frac {1}{2} ##1
\show정의를 로그에 쓰고 일시 정지\show\LaTeXmacro:->\protect \LaTeX

마지막으로 실무 감각 하나. “왜인지 이 문자만 이상하다”는 문제에 부딪히면 먼저 \the\catcode로 그 문자의 번호를 확인하세요. babel, csquotes, listings, hyperref 같은 패키지는 각자의 목적을 위해 특정 문자를 활성으로 만들거나 catcode 12로 낮춥니다. 증상이 “특정 한 문자만 깨진다”는 형태라면 원인은 십중팔구 카테고리 코드입니다. 그것만 알면 해결은 세 갈래로 좁혀집니다. 읽어 들이는 순서를 바꾸거나, 그 문자를 피하거나, 패키지가 제공하는 정식 탈출구(\%, \verb의 다른 구분 기호, \lstinline)를 쓰는 것입니다.