\setcounter와 \setlength는 같은 모양처럼 보이지만 정반대로 움직입니다. 둘을 한 그룹 안에서 실행하면 그룹을 벗어난 뒤 카운터는 새 값을 그대로 지니고, 길이만 조용히 원래대로 돌아갑니다. 커널 정의를 보면 둘 중 하나에만 \global이라는 단어가 붙어 있기 때문입니다. 이 페이지는 LaTeX의 두 가지 수치 레지스터를 다룹니다. 정수를 담아 장・그림・수식 번호를 움직이는 카운터(\newcounter)와, 치수를 담는 길이(\newlength, 정체는 dimen이 아니라 skip 레지스터)입니다. 함께 볼 것은 \stepcounter 바로 뒤에 놓인 \label이 왜 남의 번호를 가리키는지, \value가 실제로 무엇으로 전개되는지, 그리고 \dimexpr・calc・\fpeval로 어떻게 계산하는지입니다(1in = 72.27pt, 1pt = 65536sp 같은 환산도 포함).
\newcounter로 카운터를 만들고 움직이기
새 카운터는 \newcounter{foo} 로 만듭니다. 이름은 백슬래시가 없는 문자열이고, 만든 직후 값은 0입니다. 그 정체는 TeX의 \count 레지스터로, \meaning으로 들여다보면 \count196 같은 답이 돌아옵니다. 값을 바꾸는 명령은 셋입니다. \setcounter{foo}{3} 는 대입, \addtocounter{foo}{2} 는 덧셈(음수를 주면 뺄셈), \stepcounter{foo} 는 정확히 1만큼 증가입니다. 선택적 두 번째 인수 \newcounter{foo}[⟨parent⟩] 를 붙이면 foo가 parent에 종속되어 parent가 한 번 증가할 때마다 foo가 0으로 돌아갑니다. 절이 바뀔 때 하위 절 번호가 다시 시작하는 구조가 바로 이것입니다.
\newcounter{trial} % created with value 0
\setcounter{trial}{5} % trial = 5
\addtocounter{trial}{-2} % trial = 3
\stepcounter{trial} % trial = 4, and every child counter resets
\newcounter{item}[section] % "item" restarts at 0 whenever section steps
% errors you will actually see
\setcounter{nosuch}{3} % ! LaTeX Error: No counter 'nosuch' defined.
\newcounter{trial} % ! LaTeX Error: Command \c@trial already defined.
\setcounter{trial}{} % ! Missing number, treated as zero.오류 문구는 외워 둘 값어치가 있습니다. 존재하지 않는 카운터를 건드리면 ! LaTeX Error: No counter 'nosuch' defined.가 나오고, 같은 이름으로 \newcounter를 두 번 하면 ! LaTeX Error: Command \c@trial already defined.가 나옵니다. 여기서 지목된 \c@trial이야말로 trial의 내용을 실제로 담고 있는 내부 레지스터입니다. 이름에 @가 들어 있으므로 프리앰블에서 직접 건드리려면 \makeatletter가 필요합니다. 재설정은 내부 매크로 \@stpelt가 맡는데, 그 내용은 자식을 −1로 만든 뒤 \stepcounter를 부르는 것입니다. 0을 대입하는 대신 다시 한 번 증가시키는 방식이라서, 재설정이 손자 카운터까지 줄줄이 전달됩니다.
\stepcounter와 \refstepcounter의 차이 — \label이 남의 번호를 가리킬 때
\refstepcounter{foo} 는 \stepcounter와 똑같이 증가와 재설정을 하고, 거기에 더해 foo를 “지금 참조할 수 있는 번호”로 설정 합니다. 이 한 수가 없으면 \label은 붙잡을 것이 없습니다. 실제로 재어 보면 증상은 짐작보다 고약합니다. 절을 셋 쓴 문서에서 \stepcounter{demo}\label{bad}를 하면 demo의 값은 1인데, .aux에 적히는 것은 \newlabel{bad}{{3}{1}{}{}{}} 입니다. 즉 \ref{bad}는 3, 바로 앞 절의 번호를 인쇄합니다. 라벨은 그 카운터의 이전 값으로 물러나는 것이 아니라, 가장 최근에 \refstepcounter가 건드린 카운터를 붙잡습니다. 보통은 전혀 다른 카운터입니다. 게다가 오류도 경고도 없습니다. 문서 맨 앞에서 같은 일을 하면 \newlabel{step}{{}{1}{}{}{}}이 되고 \ref는 아무것도 인쇄하지 않습니다.
\newcounter{demo}
\begin{document}
\section{First}\section{Second}\section{Third}
Here: \stepcounter{demo}\label{bad} (demo is now \thedemo)
Reference: \ref{bad}
\end{document}
% .aux -> \newlabel{bad}{{3}{1}{}{}{}}
% output -> "Here: (demo is now 1)" / "Reference: 3"
% log -> 0 errors, 0 warnings
% the fix -> \refstepcounter{demo}\label{good} gives \ref = 1구조는 latex.ltx를 보면 한눈에 들어옵니다. \refstepcounter는 \stepcounter를 부른 뒤 \thefoo의 값을 \@currentlabel에 써 넣고, \label은 그 \@currentlabel을 .aux에 옮겨 적을 뿐입니다. 즉 \label은 “바로 앞의 카운터”가 아니라 전역 변수의 현재 값을 저장합니다. LaTeX 자신도 section・equation・figure를 증가시킬 때 \refstepcounter를 쓰므로, 제목이나 수식에 붙인 \label이 올바른 번호를 돌려줍니다. 실무 규칙은 곧바로 따라옵니다. 숫자를 내부적으로 진행시키기만 하면 되면 \stepcounter, \label로 참조할 수 있는 “번호 붙은 대상”을 만들면 \refstepcounter 입니다. 직접 만든 정리 환경이나 문제 번호가 제대로 상호 참조되지 않으면 먼저 이곳을 의심하세요.
\value: 카운터를 “계산할 수 있는 수”로 꺼내기
\value{foo}는 그 카운터를 담고 있는 \count 레지스터 자체로 전개됩니다. 커널의 정의는 말 그대로 \csname c@#1\endcsname 한 줄입니다. 그래서 \setcounter와 \addtocounter의 값 자리, \ifnum 비교, \numexpr 식 안 등 LaTeX가 정수를 기대하는 모든 곳 에서 쓸 수 있습니다. 뒤집어 말하면 \value는 표시용 명령이 아닙니다. 본문에 번호를 찍는 일은 다음 절의 \arabic이나 \thefoo의 몫이고, \value는 계산을 위한 날 숫자만 돌려줍니다. 두 역할을 나누어 기억하면 “번호가 안 나온다”, “숫자가 두 번 나온다” 같은 혼란이 사라집니다.
\setcounter{bar}{\value{foo}} % copy foo into bar
\addtocounter{bar}{\value{foo}} % bar = bar + foo
\ifnum\value{page}>10 \dots\fi % compare
\setcounter{bar}{\numexpr\value{foo}*2+1\relax} % arithmetic, no package
% \value is NOT for printing:
\value{foo} % wrong -- prints nothing useful
\arabic{foo} % right
\thefoo % right, and honours any format you defined번호의 모양 바꾸기 — \arabic에서 \thefoo까지
카운터의 값을 문자열로 바꾸는 명령은 여섯 가지이며, 모두 인수로 카운터 이름을 받습니다. 그러나 실제 조판에서 번호를 찍는 것은 각 카운터에 자동으로 마련되는 전용 매크로 \thefoo 입니다. section에는 \thesection, figure에는 \thefigure가 있고, 이를 \renewcommand로 고쳐 쓰면 번호 형식 전체를 한 번에 바꿀 수 있습니다. \alph와 \Alph는 알파벳 26자에 대응합니다. 경계는 생각보다 미묘해서 0을 넘기면 아무것도 출력하지 않고 오류도 나지 않지만, 음수와 27 이상은 ! LaTeX Error: Counter too large.가 납니다. \fnsymbol의 유효 범위도 1–9이며 10을 넘기면 똑같이 ! LaTeX Error: Counter too large.가 납니다. 여기서 오래된 설명에 남은 오류를 하나 바로잡겠습니다. TeX Live 2024에 실린 LaTeX2e에서는 \@fnsymbol의 정의가 모든 경우에 \TextOrMath를 쓰기 때문에 \fnsymbol은 본문에서 그대로 동작합니다. $\fnsymbol{footnote}$처럼 감쌀 필요가 없습니다(각주에서는 \thefootnote를 거치는 것이 여전히 보통입니다).
| 명령 | 출력 | 범위 / 주의 |
|---|---|---|
\arabic | 1, 2, 3 … | 기본값이며 음수도 인쇄됩니다 |
\roman | i, ii, iii … | 1 미만이면 아무것도 출력하지 않습니다 |
\Roman | I, II, III … | 1 미만이면 아무것도 출력하지 않습니다 |
\alph | a, b, c … | 1–26. 0은 무출력, 음수와 27 이상은 Counter too large |
\Alph | A, B, C … | 1–26. 0은 무출력, 음수와 27 이상은 Counter too large |
\fnsymbol | ∗ † ‡ § ¶ ‖ ∗∗ †† ‡‡ | 1–9, 10이면 Counter too large. 본문에서 그대로 쓰이며 수식 감싸기가 필요 없습니다 |
\thefoo | 그 카운터의 “공식” 형식 | 실제로 인쇄되는 것은 이것이며 \renewcommand로 바꿉니다 |
\renewcommand{\thesection}{\Roman{section}} % 1, 2, 3 -> I, II, III
\renewcommand{\thefigure}{\thesection.\arabic{figure}} % figure 2.3
\renewcommand{\thefootnote}{\fnsymbol{footnote}} % footnotes as * dagger ...
% appendix figures as A.1, A.2, ...
\appendix
\renewcommand{\thefigure}{\Alph{section}.\arabic{figure}}두 번째 예처럼 \the…의 정의에 다른 카운터의 표시를 엮으면 “2.3” 같은 복합 번호를 만들 수 있습니다. 여기서 드러나는 것은 설계입니다. 번호의 모양(\thefigure의 정의)과 언제 0으로 돌아갈지를 정하는 연동(\newcounter의 부모 인수)이 완전히 따로 결정된다는 점입니다. 그래서 \renewcommand{\thefigure}{\thesection.\arabic{figure}}만 써서는 절이 바뀌어도 그림 번호가 1로 돌아가지 않습니다. 재설정을 원한다면 \counterwithin{figure}{section} 처럼 부모 자식 관계도 함께 설정해야 합니다.
표준 카운터와 설정값으로서의 secnumdepth・tocdepth
LaTeX는 한 무리의 카운터를 미리 마련해 두고 문서를 읽어 가며 자동으로 움직입니다. 이들 역시 \setcounter로 자유롭게 건드릴 수 있어서, 중간에 장 번호를 다시 매기거나 부록의 그림 번호를 A.1로 만드는 일이 가능합니다. 빈 article 클래스에서 카운터를 하나 만들면 내부 레지스터는 \count196 부근에 배정되었습니다. 곧 수십 개의 표준 카운터가 이미 돌아가고 있다는 뜻입니다. 참고로 고전 TeX에서는 레지스터가 종류마다 256개까지였지만 e-TeX이 이 한계를 올려서, 지금은 카운터 400개를 연달아 확보해도 아무 불평이 없습니다(마지막 하나는 \count662에 안착했습니다).
- 제목 계열:
part,chapter,section,subsection,subsubsection,paragraph,subparagraph(어느 것이 있는지는 클래스에 따라 다릅니다) - 플로트와 수식:
figure,table,equation - 각주:
footnote,mpfootnote(minipage안의 각주) - 목록:
enumi,enumii,enumiii,enumiv(enumerate의 네 단계 중첩) - 쪽:
page - 설정값:
secnumdepth(어느 깊이의 제목까지 번호를 매길지)와tocdepth(어느 깊이까지 목차에 실을지)
마지막 둘은 성격이 다릅니다. 번호와 목차의 “깊이”를 지배하는 정수 다이얼 이지 무언가를 세는 물건이 아닙니다. 각 제목 명령에는 계층 번호가 있고(section이 1, subsection이 2 …), 그 번호가 secnumdepth 이하일 때만 번호가 매겨집니다. 기본값은 대체로 2이므로 \setcounter{secnumdepth}{1} 이라고 쓰면 하위 절 아래의 번호가 사라지고, \setcounter{tocdepth}{1} 이라고 쓰면 목차가 절까지로 좁혀집니다. 이 둘은 카운터가 단순한 계수기가 아니라 조판을 조종하는 설정값 이기도 함을 잘 보여 줍니다.
\newlength가 만드는 것은 dimen이 아니라 skip 레지스터
\newlength{\mylen}의 속내용은 \newskip입니다. 커널의 정의는 \@ifdefinable#1{\newskip#1} 한 줄이고, \meaning으로 확인하면 \skip50 같은 답이 돌아옵니다. 이것이 중요한 이유는 skip 레지스터가 신축 성분(글루)을 담을 수 있기 때문입니다. 그래서 \setlength{\parskip}{1em plus 2pt minus 1pt} 는 완전히 정당한 대입입니다. 실측하면 \the\parskip은 10.00002pt plus 2.0pt minus 1.0pt를 돌려주어 세 성분이 모두 보존됨을 보여 줍니다. 카운터와 달리 인수는 백슬래시가 붙은 명령 이름(\mylen)이고 초기값은 0pt입니다. 같은 이름을 두 번 선언하면 ! LaTeX Error: Command \mylen already defined.가 납니다. \a・\b・\c・\d・\t는 이미 악센트 명령으로 존재하므로 \newlength{\a}가 통하지 않는 것도 그 때문입니다.
\newlength{\gap}
\setlength{\gap}{20pt plus 10pt minus 5pt} % a rubber length in a register
% watch the stretch and shrink actually being used
\showboxbreadth=5 \showboxdepth=5
\setbox0=\hbox to 70pt{\rule{20pt}{2pt}\hspace{\gap}\rule{20pt}{2pt}}\showbox0
\setbox1=\hbox to 55pt{\rule{20pt}{2pt}\hspace{\gap}\rule{20pt}{2pt}}\showbox1
% log: \hbox(2.0+0.0)x70.0, glue set 1.0 <- all 10pt of "plus" used
% \hbox(2.0+0.0)x55.0, glue set - 1.0 <- all 5pt of "minus" used\showbox를 쓰면 신축이 실제로 쓰이는 모습을 로그에서 곧바로 읽을 수 있습니다. 자연 너비 60pt인 상자를 70pt로 늘리면 glue set 1.0 이 나옵니다. plus 10pt를 꼭 맞게 다 썼다는 뜻입니다. 55pt로 줄이면 glue set - 1.0 이 되어 minus 5pt를 모두 썼음을 보여 줍니다. 이 glue set 값은 레이아웃을 조사할 때 든든한 아군으로, 값이 2나 3이면 그 상자가 편안한 범위를 훨씬 넘겨 늘어나고 있다는 뜻입니다. plus・minus가 지면 전체에서 어떻게 작동하는지(\fill과 \flushbottom 이야기)는 “길이와 공백” 페이지가 자세히 다룹니다.
\setcounter는 왜 전역이고 \setlength는 왜 지역인가
이유는 latex.ltx의 정의에 적힌 단어 하나입니다. \setcounter와 \addtocounter에는 \global이 붙어 있고, \setlength와 \addtolength에는 붙어 있지 않습니다. 그 차이 하나가 전혀 다른 동작을 만듭니다. 카운터와 길이를 같은 그룹 안에서 바꾸면, 그룹을 벗어난 뒤 카운터는 새 값을 그대로 지니고 길이는 옛 값으로 돌아가 있습니다. 실측에서 1cm에서 9cm로 바꾼 길이는 그룹이 끝나는 순간 28.45274pt(곧 1cm)로 되돌아갔습니다. 이 비대칭은 이치에 맞습니다. 장 번호는 환경이 끝났다고 잊혀서는 곤란한 “문서의 상태”이지만, 여백의 일시적 조정은 그 범위 안에서만 듣기를 바라기 때문입니다.
\newcounter{cnt}\newlength{\len}
\setcounter{cnt}{1}\setlength{\len}{1cm}
{\setcounter{cnt}{99}\setlength{\len}{9cm}%
inside the group: cnt=\thecnt\ len=\the\len} % 99 / 256.0748pt
after the group: cnt=\thecnt\ len=\the\len % 99 / 28.45274pt
% latex.ltx:
% \def\setcounter#1#2{... {\global\csname c@#1\endcsname#2\relax}}
% \def\setlength#1#2{#1 #2\relax} <- no \global
% to make a length assignment survive a group:
\global\setlength{\len}{9cm}이 차이는 \newcommand나 \newenvironment로 자기 도구를 만들기 시작하면 곧바로 중요해집니다. 환경 안에서 \setlength{\parindent}{0pt} 이라고 쓰면 \end에서 자동으로 원래대로 돌아가므로 뒷정리가 필요 없습니다. 반대로 환경 안에서 센 횟수를 바깥으로 가지고 나가려면 \setcounter(또는 명시적인 \global)가 필요합니다. “환경을 벗어나니 설정이 사라졌다”와 “환경 바깥까지 여백이 바뀌어 버렸다”는 두 가지 고전적인 버그는 모두 이 단어 하나의 유무로 설명됩니다. 환경이 애초에 그룹을 이룬다는 성질 자체는 “환경 만들기” 페이지가 다룹니다.
길이의 계산 — \dimexpr・calc・\fpeval과 단위 환산
덧셈・곱셈・나눗셈 가운데 추가 패키지 없이 쓸 수 있는 것은 e-TeX의 원시 명령 \numexpr와 \dimexpr 입니다. 기본 article에서 \the\dimexpr\textwidth/3\relax는 115.0pt를, \the\dimexpr 2cm+4pt\relax는 60.9055pt를 돌려줍니다. 한 가지 주의점은 \numexpr의 나눗셈이 버림이 아니라 반올림이라는 것입니다. \the\numexpr 7/2\relax는 4, -7/2는 −4입니다. 더 LaTeX다운 문법을 원한다면 calc 패키지를 읽어 들이면 됩니다. \setlength{\x}{(\textwidth - 2em)/2} 같은 식을 \setlength 안에 그대로 쓸 수 있습니다(실측 162.49998pt). 소수배만은 쓰는 법이 달라서, \widthof{Total:} * \real{0.5} 처럼 \real{}를 끼워 넣습니다.
| 쓰는 법 | 필요한 것 | TeX Live 2024에서 실측한 결과 |
|---|---|---|
\dimexpr | 없음(e-TeX 원시 명령) | \the\dimexpr\textwidth/3\relax → 115.0pt |
\numexpr | 없음(e-TeX 원시 명령) | 7/2 → 4, -7/2 → -4(버림이 아니라 반올림) |
calc | \usepackage{calc} | (\textwidth - 2em)/2 → 162.49998pt |
\real | \usepackage{calc} | \widthof{Total:} * \real{0.5} → 12.91669pt |
\fpeval | LaTeX 커널(xfp 불필요) | \fpeval{345/7}pt → 49.28572pt, \fpeval{sqrt(2)} → 1.414213562373095 |
계산할 때 단위의 실체를 알아 두면 사고가 줄어듭니다. TeX의 기준은 pt(TeX 포인트)이고 1in = 72.27pt 입니다. \the\dimexpr 1in\relax는 72.26999pt를 돌려줍니다. 정확히 72.27이 아닌 것은 TeX가 모든 치수를 sp(스케일드 포인트)의 정수배 로 보관하기 때문이며, 1pt = 65536sp 이고 \the\dimexpr 65536sp\relax는 정확히 1.0pt입니다. PostScript 계열의 bp는 1/72 in 이므로 1bp는 1.00374pt이고 72bp는 72.26999pt, 곧 1in으로 딱 돌아옵니다. 하나 더, em은 대입하는 순간 평가되어 얼어붙습니다. 프리앰블에서 \setlength{\x}{1em} 이라고 쓰면 10.00002pt가 들어가고, 나중에 \Large 그룹에 들어가도 \x는 여전히 10.00002pt입니다. 같은 자리에서 새로 1em을 재면 14.09984pt입니다. 단위 목록 자체와 \hspace・\vspace는 “길이와 공백” 페이지가 주인입니다.
조판한 결과 재기 — \settowidth・\settoheight・\settodepth
가장 강력한 길이 조작은 실제로 조판해 본 결과를 재어 길이에 담는 것입니다. \settowidth{\mylen}{text}는 주어진 텍스트를 내부에서 조판하고 그 너비 를 \mylen에 대입합니다. 마찬가지로 \settoheight는 베이스라인 위 의 높이를, \settodepth는 아래 의 깊이를 잽니다. 실측값을 보면 차이가 뚜렷합니다. Total:은 너비 25.83339pt, 높이 6.94444pt, 깊이 0.0pt입니다. xyz는 높이 4.30554pt(x 하이트)에 깊이 1.94444pt, gjpqy는 높이 6.67859pt에 깊이 1.94444pt입니다. 디센더가 들어오는 순간 깊이가 생겨나는 모습이 숫자로 그대로 보입니다.
\usepackage{calc}
\newlength{\labelw}
\settowidth{\labelw}{Total:} % 25.83339pt -- the real typeset width
\noindent\makebox[\labelw][l]{Total:}42\par
\rule{\labelw}{0.4pt} % a rule exactly as wide as the label
% the same idea as a value inside an expression (needs calc):
\setlength{\labelw}{\widthof{Total:} + 1em}
\the\labelw % print the value to check itcalc를 읽어 들이면 같은 발상을 식 안에서 쓸 수 있어, \widthof{text}・\heightof・\depthof를 값으로 바로 적을 수 있습니다. 차이는 역할입니다. \settowidth는 결과를 길이에 대입하는 명령이고, \widthof는 식 안에 놓을 수 있는 값입니다. 그리고 calc를 읽어 들이는 것을 잊었을 때의 증상을 기억해 두세요. \setlength{\mylen}{\textwidth/3} 은 오류를 하나도 내지 않습니다. \mylen에는 \textwidth의 값(기본 article에서 345.0pt)이 그대로 들어가고, 남은 /3은 본문에 글자로 조판됩니다. PDF에 “/3”이 나와야 비로소 알아차리는 종류의 사고이므로, 나눗셈을 쓰려면 calc를 읽어 들이거나 패키지가 필요 없는 \dimexpr를 쓰세요.
한데 모아 쓰기 — 참조 가능한 문제 번호 직접 만들기
마지막으로 지금까지의 부품을 한자리에 모읍니다. 카운터 question을 만들고, \thequestion으로 표시 형식을 “Q1.”으로 정하고, \settowidth로 라벨 너비를 재어 번호를 가지런히 맞추고, \refstepcounter로 번호를 진행시켜 \label・\ref가 작동하게 합니다. 이 네 가지가 갖춰지면 실전에 쓸 만한 번호 붙은 구조물이 됩니다. 형식이 \thequestion의 정의 한 곳에만 모여 있으므로, 나중에 “Q”를 다른 말로 바꾸는 것도 한 줄이면 됩니다.
\documentclass{article}
\newcounter{question}
\newlength{\qlabel}
\renewcommand{\thequestion}{Q\arabic{question}}
\newcommand{\question}{%
\refstepcounter{question}% <- \refstepcounter, so \label works
\settowidth{\qlabel}{\textbf{\thequestion.}}%
\par\noindent\makebox[\qlabel][l]{\textbf{\thequestion.}}\hspace{0.5em}}
\begin{document}
\question The first question.\label{q:first}
\question The second question.
See question~\ref{q:first}. % -> "See question Q1."
\end{document}이 작은 예에 카운터와 길이를 프로그래밍 도구로 쓰는 요령이 응축되어 있습니다. 값을 직접 적지 말고 이름을 붙여 한곳에서 관리하라는 것입니다. 번호 형식은 \the…에, 치수는 \newlength로 붙인 이름에 모아 두면 나중에 사양이 바뀌어도 고칠 곳은 한 군데입니다. 치수는 가능한 한 기존 길이로 조립하세요. 0.8\textwidth나 2\baselineskip처럼 써 두면 용지 크기나 여백이 바뀌어도 비율이 유지됩니다. 반대로 8cm라고 못 박는 순간, 그 값은 다음 레이아웃 변경 때 깨지도록 예약된 셈입니다.