LaTeX의 환경에는 사실 특별한 장치가 하나도 없습니다. \begin{quote}는 \quote라는 명령을 부르고, \end{quote}는 \endquote라는 명령을 부를 뿐입니다. 환경을 직접 만드는 \newenvironment가 하는 일도 이 두 매크로를 한꺼번에 써 주는 것뿐입니다. 이 짝 구조만 붙잡으면 나머지는 줄줄이 딸려 나옵니다. 환경 이름이 왜 명령 이름과 충돌하는지, 인수가 왜 끝 코드에서는 쓸 수 없는지, \begin과 \end가 어긋났을 때의 오류 문구가 왜 그런 모양인지가 모두 같은 사실에서 설명됩니다. 이 페이지에서는 그 줄기를 따라 \newenvironment와 \renewenvironment, 인수와 선택 인수, 거저 얻는 그룹화, 그리고 현대적인 \NewDocumentEnvironment까지 살펴봅니다.
\newenvironment 쓰는 법: 시작 코드와 끝 코드
전처리부에 \newenvironment{name}{시작 코드}{끝 코드}라고 써 두면 본문에서 \begin{name}…\end{name}을 쓸 수 있습니다. 첫 번째 인수는 환경 이름이며 백슬래시는 붙이지 않습니다. 두 번째 인수는 LaTeX이 \begin{name}을 만났을 때 실행할 코드이고, 세 번째 인수는 \end{name}을 만났을 때 실행할 코드입니다. 사이에 낀 본문 자체에는 아무 손도 대지 않고 평소대로 조판됩니다. 결국 사용자 환경을 설계한다는 것은 한 가지 질문에 답하는 일입니다. 들어갈 때 무엇을 준비하고, 나올 때 무엇을 치울 것인가.
% preamble: define a warning environment
\newenvironment{warning}{%
\par\noindent\textbf{Warning:}\itshape
}{%
\par
}
% body: use it
\begin{warning}
This operation cannot be undone.
\end{warning}이 예제에서 끝 코드가 \par 하나뿐인 것은 게으름이 아닙니다. 시작 코드에서 켠 \itshape(이탤릭)을 끄는 처리가 어디에도 없는데, 환경 바깥 글자는 멀쩡히 정체로 돌아옵니다. 이유는 다음 두 절에서 밝히겠지만 결론만 먼저 말하면, 환경은 자동으로 그룹을 이루기 때문입니다. \newenvironment를 쓸 때의 요령은 되돌릴 필요가 없는 것을 되돌리려 하지 않는 것 이며, 끝 코드는 빈 {}여도 아무 문제가 없습니다.
환경의 정체는 \name과 \endname 두 매크로
\begin{name}은 \name을, \end{name}은 \endname을 호출합니다. 추측이 아닙니다. latex.ltx에 있는 \end의 정의가 그대로 \csname end#1\endcsname이기 때문입니다. 믿기 어렵다면 TeX의 \show로 들여다보는 것이 빠릅니다. 표준 quote 환경에 대해 \show\quote와 \show\endquote를 실행하면, 한쪽은 \list를 여는 매크로이고 다른 쪽은 그저 \endlist임을 알 수 있습니다. 환경이라는 별도의 문법은 어디에도 없고, 이름으로 짝지어진 두 매크로만 있을 뿐입니다.
% ask LaTeX what the quote environment is actually made of
\show\quote
% > \quote=\long macro:
% -> \list {}{\rightmargin \leftmargin }\item \relax .
\show\endquote
% > \endquote=\long macro:
% -> \endlist .
% so \begin{quote} ... \end{quote} is, in effect:
% \begingroup \quote ... \endquote \endgroup이 사실은 곧바로 오류라는 형태로 다가옵니다. 환경 이름은 철자가 같은 명령 이름을 차지합니다. 시험 삼아 \newenvironment{alpha}{...}{...}라고 쓰면 컴파일이 ! LaTeX Error: Command \alpha already defined.에서 멈춥니다. 그리스 문자 \alpha가 이미 있기 때문이며, 환경의 이름 공간과 명령의 이름 공간은 애초에 하나였던 셈입니다. \newenvironment{quote}가 Command \quote already defined.가 되는 것도 같은 이유이고, 이 검사는 실수로 덮어쓰는 일을 막기 위한 의도적인 장치입니다. 사용자 환경 이름으로는 mywarning, thmbox처럼 기존 명령과 부딪히기 어려운 말을 고르는 편이 안전합니다.
같은 논리를 뒤집으면 \newcommand 쪽의 유명한 제약이 됩니다. \newcommand{\endnotes}{...}는 그런 명령이 어디에도 없는데도 거절당합니다. end로 시작하는 이름을 마음대로 만들 수 있으면 \end{...}가 호출하는 \endname 쪽과 부딪힐 수 있으므로 접두사 전체가 예약되어 있는 것입니다. 이 검사의 자세한 이야기는 매크로 페이지에 맡기고, 여기서는 한 가지만 짚습니다. 그쪽의 금지 조항은 바로 이 페이지에서 만들고 있는 환경의 이름 공간을 지키기 위해 존재합니다.
환경은 자동으로 그룹이 됩니다: 무엇이 되돌아오고 무엇이 새는가
\begin은 시작 코드를 실행하기 전에 \begingroup을, \end는 끝 코드를 실행한 뒤에 짝이 되는 \endgroup을 냅니다. 그러므로 시작 코드, 본문, 끝 코드 세 가지가 모두 하나의 그룹 안에 놓입니다. 앞의 warning 환경이 \itshape을 되돌릴 필요가 없었던 이유가 이것입니다. 매크로로 같은 일을 하려면 내용을 { … }로 직접 감싸야 하지만, 환경에서는 \begin…\end가 바로 그 중괄호 역할을 합니다. 서식 변경을 일정 범위 안에 가두려면 매크로보다 환경이 솔직한 도구 인 이유가 여기에 다 있습니다.
다만 “그룹을 벗어나면 전부 되돌아온다”는 뜻은 아닙니다. TeX의 대입에는 국소적인 것과 대역적인 것이 있고, LaTeX의 카운터 조작은 의도적으로 대역 입니다. latex.ltx의 \addtocounter는 \global\advance로 쓰여 있어서, 환경 안에서 \stepcounter로 올린 번호는 \end를 지나도 그대로 남습니다. 절 번호나 그림 번호가 환경 안에서 올라가도 사라지지 않는 것은 이 설계 덕분입니다. 반대로 환경 안에서 \newcommand로 정의한 매크로는 \end와 함께 사라지고, 밖에서 쓰면 ! Undefined control sequence.가 납니다.
| 환경 안에서 실행하면 | \end 를 지난 뒤 | 이유 |
|---|---|---|
\itshape | 되돌아옵니다 | 글꼴 전환은 국소 대입입니다 |
\setlength | 되돌아옵니다 | \setlength 는 평범한 국소 대입입니다 |
\newcommand | 사라집니다 | 정의가 국소적입니다. 밖에서 쓰면 ! Undefined control sequence. |
\stepcounter | 남습니다 | 카운터 조작은 \global 로 쓰여 있습니다 |
\gdef | 남습니다 | 명시적으로 대역적인 정의입니다 |
\label | 남습니다 | .aux 파일에 쓰는 동작은 그룹이 취소하지 못합니다 |
인수를 받는 환경과 기본값이 있는 선택 인수
호출할 때마다 내용을 바꾸려면 이름 뒤 대괄호에 인수 개수를 적고, 시작 코드에서 #1, #2 처럼 참조합니다. \newenvironment{name}[⟨개수⟩]{시작 코드}{끝 코드} 형태이며 #1부터 #9까지 최대 아홉 개입니다. 대괄호를 한 쌍 더 붙여 \newenvironment{name}[⟨개수⟩][⟨기본값⟩]{...}{...} 로 쓰면 #1이 선택 인수가 됩니다. \begin{name} 이면 기본값이, \begin{name}[x] 면 x가 #1에 들어갑니다. 이 문법은 \newcommand와 완전히 같으며, ⟨개수⟩에 선택 인수까지 센 전체 개수 를 적는 규칙까지 공통입니다.
% one mandatory argument
\newenvironment{point}[1]{%
\par\noindent\textbf{#1}\quad
}{%
\par
}
\begin{point}{Conclusion}
Back up early.
\end{point}
% first argument optional, default "Note"
\newenvironment{callout}[1][Note]{%
\par\noindent\textbf{#1:}\itshape
}{%
\par
}
\begin{callout} % label is "Note"
Nothing to configure.
\end{callout}
\begin{callout}[Warning] % #1 becomes "Warning"
This cannot be undone.
\end{callout}끝 코드에서 #1을 쓰면 오류가 나는 이유와 우회법
인수 #1, #2 등은 시작 코드에서만 쓸 수 있습니다. 끝 코드에 적으면 사용할 때가 아니라 정의하는 바로 그 줄에서 ! Illegal parameter number in definition of \enddemo. 라고 지적당합니다. 오류가 지목하는 이름 \enddemo에 주목하십시오. 지금까지의 이야기를 확인해 주는 대목으로, \newenvironment는 인수를 받는 매크로 \demo와 인수를 하나도 받지 않는 매크로 \enddemo를 만듭니다. 매개변수가 없는 매크로 본문에 #1이 나타나면 TeX에게는 그저 문법 오류일 뿐입니다. 실행 중에 인수가 사라지는 것이 아니라, 처음부터 받을 자리가 없었던 것입니다.
끝날 때도 인수 값이 필요하다면 정석은 시작 코드에 있는 동안 값을 저장해 두는 것 입니다. 문자열이라면 \newsavebox로 확보한 상자에 \sbox로 담는 방법이 든든하고, \def나 \newcommand로 매크로에 기억시켜도 됩니다. 환경 전체가 하나의 그룹이므로 시작 코드에서 저장한 내용은 끝 코드까지 온전히 살아남습니다. 다음의 citequote는 인용문 끝에 출처를 오른쪽 정렬로 찍는 예제입니다. 출처를 #1(기본값 Shakespeare)로 받아 상자 \quoteauthor에 넣고, 끝 코드에서 \usebox로 꺼냅니다.
\newsavebox{\quoteauthor}
\newenvironment{citequote}[1][Shakespeare]{%
\sbox\quoteauthor{#1}% save the argument while we still have it
\begin{quotation}%
}{%
\hspace{1em plus 1fill}---\usebox{\quoteauthor}% retrieve it here
\end{quotation}%
}
\begin{citequote}
To be, or not to be.
\end{citequote}
\begin{citequote}[Knuth]
Premature optimization is the root of all evil.
\end{citequote}\renewenvironment 와 별표가 붙은 \newenvironment*
이미 있는 환경을 다시 만들 때는 \renewenvironment를 씁니다. [⟨개수⟩][⟨기본값⟩]을 포함한 인수 문법은 \newenvironment와 한 글자도 다르지 않고, 다른 것은 전제뿐입니다. \newenvironment가 이름이 비어 있을 때만 성공한다면, \renewenvironment는 이름이 이미 차 있을 때만 성공하며, 없는 이름에 쓰면 ! LaTeX Error: Environment nosuch undefined. 로 멈춥니다. 문서 전체의 quote를 이탤릭으로 바꾸는 것처럼 일괄 변경에 알맞습니다. 다만 클래스나 패키지가 제공하는 환경을 다시 정의하면 그것에 기대는 다른 코드까지 휘말릴 수 있다는 점은 잊지 마십시오.
% italicise every quote in the document
\renewenvironment{quote}{%
\list{}{\rightmargin\leftmargin}\item\relax\itshape
}{%
\endlist
}\newenvironment와 \renewenvironment에는 이름 뒤에 *를 붙인 별표형도 있습니다. 이 별표가 바꾸는 것은 인수에 빈 줄을 넣을 수 있는지 여부 입니다. 별표가 없는 인수는 문단(\par)을 넘나들 수 있지만, 별표가 붙은 인수는 “짧은” 인수가 되어 중간에 빈 줄이 끼면 ! Paragraph ended before \shortenv was complete. 로 멈춥니다. 제약처럼 보이지만 실은 배려입니다. 닫는 중괄호를 빠뜨려 인수가 폭주할 때, 문서 끝까지 가지 않고 다음 빈 줄에서 멈춰 주기 때문입니다. 참고로 \providecommand에 해당하는 “없을 때만 정의” 환경판은 표준에 없습니다. 그것이 필요하면 다음 절의 도구를 쓰십시오.
\NewDocumentEnvironment 라면 끝 코드에서도 인수를 쓸 수 있습니다
\NewDocumentEnvironment{name}{⟨인수 지정⟩}{시작 코드}{끝 코드}를 쓰면 저장 상자를 거치는 수고가 필요 없습니다. 이 인터페이스에서는 시작 코드와 끝 코드 양쪽 이 같은 인수를 참조할 수 있기 때문입니다. 인수 개수를 숫자로 적는 대신 m(필수), o(선택), O{기본값}(기본값이 있는 선택), s(별표 유무) 같은 글자를 늘어놓은 인수 지정을 씁니다. 원래는 xparse 패키지의 기능이었지만 2020년 10월 1일 릴리스에서 커널로 편입되어 이제는 \usepackage 없이 쓸 수 있습니다(지정자 목록은 xparse 페이지에 있습니다).
% O{...} is an optional argument with a default; #1 works in both halves
\NewDocumentEnvironment{citequote}{O{Shakespeare}}{%
\begin{quotation}%
}{%
\hspace{1em plus 1fill}---#1%
\end{quotation}%
}
\begin{citequote}[Knuth]
Premature optimization is the root of all evil.
\end{citequote}같은 릴리스에서 \RenewDocumentEnvironment(다시 만들기), \ProvideDocumentEnvironment(없을 때만 정의), \DeclareDocumentEnvironment(있든 없든 정의)도 함께 갖춰졌습니다. 앞 절에서 표준에 없다고 적었던 \providecommand의 환경판이 여기서 비로소 손에 들어옵니다. 새로 쓰는 코드라면 선택 인수를 여러 개 받을 수 있다는 점, 별표 변종을 정식으로 다룬다는 점, 끝 코드에서 인수가 보인다는 점, 이 세 가지 이유로 이쪽을 기본 선택으로 삼아도 좋습니다. \newenvironment는 기존 문서를 읽고 유지보수하기 위해 알아 두는 자리로 물러납니다.
대부분의 사용자 환경을 덮는 세 가지 패턴
실무에서 쓰는 사용자 환경은 거의 다음 세 가지 가운데 하나에 들어갑니다. 뼈대는 모두 같아서, 시작 코드에서 준비하고 끝 코드에서 뒷정리를 할 뿐입니다.
- 서식으로 감싸기 — 시작 코드에서 글꼴, 크기, 정렬을 설정하고 본문이 그 모습을 따르게 합니다. 그룹 덕분에 끝 코드는 비어 있어도 됩니다.
- 위아래에 여백 넣기 — 시작 코드 첫머리와 끝 코드 마지막에
\par\medskip같은 세로 간격을 두어 본문을 위아래 여백으로 감쌉니다. - 기존 환경 위에 얹기 — 시작 코드에서 다른 환경을
\begin{...}으로 열고, 끝 코드에서 대응하는\end{...}을 닫습니다.quote,center,list를 바탕으로 조금만 손보는 방식입니다.
% 1. wrap the body in formatting
\newenvironment{aside}{\par\small\itshape}{\par}
% 2. add vertical space above and below
\newenvironment{spaced}{\par\medskip\noindent}{\par\medskip}
% 3. build on an existing environment
\newenvironment{smallquote}{%
\small\begin{quotation}%
}{%
\end{quotation}%
}세 번째 패턴이 가장 자주 쓰입니다. 바탕이 되는 환경의 들여쓰기와 여백을 그대로 물려받으므로 덧붙일 분량이 놀랄 만큼 적습니다. 다만 시작 코드에서 연 것은 반드시 끝 코드에서 닫아 짝을 무너뜨리지 마십시오. 그리고 위 예제에서 줄 끝마다 붙은 %는 장식이 아닙니다. %를 쓰지 않으면 줄 끝의 줄바꿈이 공백 하나로 본문에 섞여 들어가, 환경 앞뒤에 설명되지 않는 틈이 생깁니다. 여러 줄에 걸친 정의를 쓸 때 줄 끝 %를 습관으로 삼으면 사고 한 종류가 통째로 사라집니다.
\begin 과 \end 가 어긋났을 때 만나는 오류
\end{name} 은 \endname 을 부르는 데 그치지 않고, name 이 지금 열려 있는 환경과 일치하는지도 확인합니다. 여기서 어긋나면 ! LaTeX Error: \begin{sidenote} on input line 5 ended by \end{remarkbox}. 처럼 연 쪽의 환경 이름과 그 줄 번호 를 알려 줍니다. 오류가 보고된 줄보다 이 줄 번호가 대개 더 쓸모 있습니다. 실제로 자주 마주치는 얼굴은 다음 네 가지입니다.
! LaTeX Error: Environment nosuchenv undefined.— 그런 이름의 환경이 정의되어 있지 않습니다. 철자 오류이거나, 그것을 정의하는 패키지의\usepackage를 빠뜨린 것입니다.! LaTeX Error: \begin{sidenote} on input line 5 ended by \end{remarkbox}.— 연 이름과 닫은 이름이 다릅니다.! LaTeX Error: \begin{sidenote} on input line 4 ended by \end{document}.—\end{sidenote}를 빠뜨려 환경이\end{document}까지 열린 채로 갔습니다.! LaTeX Error: \begin{document} ended by \end{nosuchenv}.— 열지도 않은 환경을 닫았습니다. 앞의\begin이 미정의라 이미 오류를 낸 경우에도 뒤이어 이 형태가 나옵니다.
넷 다 “짝이 맞지 않는다”는 하나의 증상을 달리 말한 것입니다. 자작 환경이 시작 코드에서 다른 환경을 열고 있다면 끝 코드의 \end{...} 부터 의심하십시오. 그리고 이런 오류는 ! Missing $ inserted. 처럼 무관해 보이는 비명을 함께 끌고 오는 일이 잦습니다. 2차 피해가 원인보다 더 눈에 띄기 때문에, 로그는 가장 처음 나온 오류부터 읽는다 가 원칙입니다.