여러 파일로 나눈 LaTeX 프로젝트를 .tex 열댓 개로 쪼개도 컴파일러는 눈 하나 깜짝하지 않습니다. pdflatex main.tex는 그 전부를 읽어 하나의 PDF를 냅니다. 갈피를 잃는 쪽은 편집기입니다. 3장을 열어 둔 채 빌드 키를 누르면 ! LaTeX Error: Missing \begin{document}.가 돌아옵니다. 편집기가 고분고분 “지금 보고 있는 파일”을 조판했기 때문입니다. 이를 고치는 한 줄이 각 장 맨 위에 두는 % !TEX root = ../main.tex인데, 재미있는 점은 이 줄을 LaTeX이 단 한 번도 읽지 않는다는 것입니다. 그저 주석이고, 수신인은 컴파일러가 아니라 편집기입니다. 이 페이지는 분할 프로젝트의 바로 그 두 번째 층을 다룹니다. 어떤 편집기가 이 매직 코멘트를 읽고 읽지 않는 쪽은 무엇을 대신 쓰는지, SyncTeX은 어떻게 올바른 장 파일로 되돌아오는지, 빌드 산출물은 실제로 어디에 떨어지는지, 그리고 눈앞의 파일이 본 파일이 아닐 때 무엇이 망가지는지입니다. 분할 자체를 맡는 \input, \include, \includeonly는 별개의 이야기이며 페이지 끝에서 이어집니다.
% !TEX root — 장 파일을 열어 둔 채 본 파일 빌드하기
본 파일이 아닌 모든 파일의 맨 위에 % !TEX root = ../main.tex를 두면, 어느 파일이 앞에 있든 빌드 키가 올바르게 동작합니다. TeXShop 자체 문서에 있는 두 가지 세부는 알아 둘 값어치가 있습니다. 둘 다 사람들이 자주 걸리는 지점이기 때문입니다. 첫째, 이 줄은 파일의 첫 스무 줄 안에 있어야 합니다. 긴 라이선스 머리말 아래에 묻으면 그냥 눈에 띄지 않습니다. 둘째, 경로는 그 줄이 들어 있는 파일 기준의 상대 경로로 풀립니다. 프로젝트 루트 기준이 아닙니다. chapters/에 있는 장 파일에 필요한 것은 main.tex가 아니라 ../main.tex입니다. 절대 경로도 되지만, 프로젝트를 옮길 수 없게 되는 대가가 따릅니다. 본 파일 자신에게는 이 줄이 필요 없습니다. 이미 루트이니까요.
thesis/
main.tex <- the root; needs no magic comment
chapters/
03-results.tex <- carries the line below% chapters/03-results.tex -- the first lines of the file
% !TEX root = ../main.tex % relative to THIS file, not to thesis/
% !TEX TS-program = xelatex % pin the engine for the whole project
% !TEX encoding = UTF-8 Unicode
\chapter{Results}이 줄에는 선대가 있고, 후계가 이긴 이유가 시사적입니다. TeXShop에는 예전에 “Set Project Root…”라는 메뉴 명령이 있었고, 그 답을 장 파일 옆의 부속 파일에 기록했습니다. two.tex에는 two.texshop이 딸리는 식이었습니다. 그 보이지 않는 파일을 버리면 TeXShop은 곧장 다시 장 파일을 조판했습니다. TeXShop 문서는 현재 이 명령을 두고 % !TEX root 방식이 더 견고하기에 메뉴에서 뺐다고 적고 있는데, 이유는 그 한 단어에 다 들어 있습니다. 파일 안에 있는 줄은 파일과 함께 이동합니다. 복사해도, 상위 폴더 이름을 바꿔도, Git으로 clone해도, 당신의 편집기를 한 번도 열어 본 적 없는 공저자에게 넘겨도 살아남습니다. 파일 곁에 세워 둔 설정은 언젠가 반드시 파일과 헤어집니다.
LaTeX 자신은 왜 % !TEX root를 읽지 않을까
%가 주석을 열고, 주석은 TeX의 어휘 분석 단계에서 다른 무엇보다 먼저 버려지기 때문입니다. pdflatex, xelatex, lualatex은 그 줄에서 아무것도 보지 못합니다. 이것은 한 프로그램(편집기)이 다른 프로그램(편집기의 빌드 명령)에게 보내는 전언이고, 다만 소스 파일을 경유할 뿐입니다. 실무적 귀결이 둘 있습니다. 첫째, 이 줄이 틀려도 아무도 경고해 주지 않습니다. 존재하지 않는 파일을 가리키게 하면 편집기는 조용히 제 나름의 추측, 대개는 열려 있는 파일로 되돌아가고, 당신은 다시 ! LaTeX Error: Missing \begin{document}.를 봅니다. 둘째, 터미널이나 CI에서 도는 빌드(latexmk main.tex, Makefile, GitHub Actions의 한 단계)는 명령줄에서 본 파일을 직접 지목하므로 매직 코멘트를 완전히 무시합니다. 이 줄은 대화식 편집을 위한 편의이지 프로젝트 정의의 일부가 아닙니다.
TeX이 정말로 읽는 주석이 딱 하나 있으니, 둘을 혼동하지 않으려면 알아 둘 만합니다. 주 입력 파일의 맨 첫 줄이 %&로 시작하면 엔진 자신이 그것을 해석해 포맷을 고릅니다. %&pdflatex, %&latex 같은 식이며, tex 맨 페이지는 이 동작이 -parse-first-line 옵션과 parse_first_line 설정 변수로 제어된다고 적고 있습니다. 이쪽은 TeX 자체의 포맷 적재 장치에서 유래한, 엔진 쪽 구조입니다. 반면 % !TEX ...라고 적히는 것은 전부 편집기 쪽에 있습니다. 생김새가 닮은 것은 둘 다 “LaTeX이 걸려 넘어지지 않을 자리에 지시를 숨기고 싶었다”는 우연의 일치입니다.
% main.tex -- first line only: the ENGINE parses this one
%&pdflatex
% chapters/03-results.tex -- the engine never sees this one
% !TEX root = ../main.tex어떤 편집기가 % !TEX root를 읽고, 나머지는 무엇을 쓰는가
TeXShop, TeXworks, TeXstudio, 그리고 LaTeX Workshop 확장을 넣은 VS Code는 모두 이 줄을 읽습니다. Emacs(AUCTeX)는 자기만의 파일 로컬 변수를 쓰고, Overleaf는 소스가 아니라 프로젝트 설정에서 답을 가져옵니다. 그중 살펴볼 값어치가 있는 것은 LaTeX Workshop인데, 판단 절차를 전부 문서로 밝혀 두었기 때문입니다. 먼저 열려 있는 편집기의 매직 코멘트를 보고, 다음으로 그 파일 자체가 \documentclass나 \begin{document}를 담고 있는지 보고, 그다음에는 작업 공간 최상위의 .tex 파일들을 훑어 그 파일을 끌어들이는 쪽을 찾고, 이어서 subfiles의 형태인 \documentclass[main.tex]{subfiles}를 알아보고, 마지막으로는 지난 컴파일이 남긴 .fls 파일 목록에 기댑니다. 매직 코멘트가 이기는 이유는 가장 먼저 조회되기 때문입니다. 반대로 듣지 않게 하고 싶을 때의 설정 이름은 latex-workshop.latex.build.enableMagicComments입니다.
| 편집기 | 읽는 것 | 비고 |
|---|---|---|
TeXShop | % !TEX root | 이 지시자의 출처. 형제로 % !TEX TS-program, encoding, spellcheck |
TeXworks | % !TEX root | 같은 매직 코멘트 방식을 채택 |
TeXstudio | % !TeX root | 먼저 루트를 자동 감지하고, 이 줄이 있으면 그것을 우선 |
LaTeX Workshop | % !TEX root | VS Code용. 다섯 단계 탐색의 첫 단계이며 latex-workshop.latex.build.enableMagicComments로 끔 |
AUCTeX | TeX-master | Emacs용. 파일 로컬 변수이며 관례상 파일 끝에 둠 |
Overleaf | 프로젝트 설정 | 프로젝트 메뉴에서 메인 문서를 지정. 소스에는 아무것도 남지 않음 |
흥미로운 예외는 Emacs입니다. AUCTeX은 같은 질문을 던지지만 답을 파일 로컬 변수로 저장하고, 관례상 파일 끝의 블록에 둡니다. 각 편집기가 자기 관례만 읽으므로 둘 다 실어 두어도 충돌하지 않습니다. AUCTeX 블록은 다른 편집기에게 그저 주석이고, % !TEX root는 Emacs에게 그저 주석입니다. 공유 저장소에서는 둘을 함께 지닌 장 파일을 흔히 보는데, 그것이 옳고 비용은 두 줄입니다. Overleaf는 이 논쟁 바깥에 있습니다. 메인 문서는 프로젝트의 속성이고 프로젝트 메뉴에서 정하므로 소스와 어긋날 여지가 없습니다. 뒤집어 말하면, 프로젝트를 내려받아 로컬에서 열 때 그 정보는 파일을 따라오지 않습니다.
% chapters/03-results.tex -- both conventions in one file
% !TEX root = ../main.tex
\chapter{Results}
%%% Local Variables:
%%% mode: latex
%%% TeX-master: "../main"
%%% End:파일을 넘나드는 SyncTeX — PDF를 클릭하면 맞는 장이 열리는 이유
SyncTeX이 페이지 위의 모든 상자에 대해 어느 입력 파일의 몇 번째 줄에서 왔는지를 기록해 두기 때문입니다. PDF에서 3장의 문단을 더블클릭하면 main.tex이 아니라 chapters/03-results.tex이 열립니다. 켜는 방법은 -synctex=1이고, 그러면 프로젝트 루트에 main.synctex.gz 딱 하나가 생기며 이름은 루트 파일에서 따옵니다. 장별 synctex 파일이라는 것은 없습니다. 단 하나의 색인이 프로젝트 전체를 덮으며, 바로 그 때문에 그 안의 어떤 파일이든 가리킬 수 있습니다.
$ pdflatex -synctex=1 main.tex
$ synctex edit -o "2:100:200:main.pdf"
SyncTeX result begin
Output:main.pdf
Input:./chapters/ch2.tex
Line:2
SyncTeX result end명령줄 도구를 한 번 써 보면 구조가 손에 잡힙니다. synctex edit은 PDF의 페이지와 좌표를 받아 파일 이름과 줄 번호를 돌려주고, synctex view는 반대로 소스의 한 줄에서 지면상의 위치를 돌려줍니다. 그 뒤에 있는 Synchronize TeXnology는 자체 맨 페이지의 표현대로 본질적으로 Jérôme Laurens 덕분이며, 지금은 TeX Live의 일부로 유지됩니다. 그리고 TeXShop 문서는 앞 절과의 연결을 분명히 밝힙니다. 역방향 검색 클릭이 main.tex이 아니라 올바른 장 창을 열어 활성화할 수 있는 것은 % !TEX root 줄이 있기 때문이라고 말입니다. 두 기능을 보통 함께 설정하는 이유가 바로 이것입니다.
함정은 장을 단독으로 컴파일할 때 옵니다. 엔진 실행의 부산물은 입력 파일 옆이 아니라 빌드를 돌린 작업 디렉터리에 떨어집니다. 프로젝트 루트에서 pdflatex -synctex=1 chapters/03-results.tex를 치면 03-results.synctex.gz가 루트에, main.synctex.gz 바로 옆에 생깁니다. 이제 같은 소스 줄을 기술하는 색인이 둘이 되고, 그중 하나는 1쪽에서 시작하는 한 장짜리 PDF를 가리킵니다. 뷰어가 어느 쪽을 읽느냐에 따라 클릭이 도착하는 자리가 달라지고, 페이지 번호가 어긋나기 시작합니다. 책 전체 빌드로 돌아갈 때는 장 단위 빌드가 남긴 PDF와 synctex 파일을 지우십시오.
빌드 산출물은 어디에 떨어지는가 — .gitignore와 청소
\include는 장마다 .aux를 하나씩 쓰고, 그것도 장 파일 옆에 씁니다. chapters/01-intro.tex을 가진 프로젝트를 빌드하면 그 옆에 chapters/01-intro.aux가 놓여 있습니다. 나머지는 전부 본 파일과 같은 루트에 머뭅니다. main.aux, main.log, main.toc, main.out, main.synctex.gz, 그리고 latexmk를 쓴다면 main.fls와 main.fdb_latexmk입니다. 즉 산출물은 한곳에 모이지 않고 소스 트리 전체에 얇게 흩뿌려집니다.
thesis/
main.tex main.pdf
main.aux main.log main.toc main.out
main.synctex.gz main.fls main.fdb_latexmk
chapters/
01-intro.tex 01-intro.aux <- one .aux per \include, here
02-method.tex 02-method.auxGit에게는 보이는 것만큼 성가시지 않습니다. .gitignore의 패턴은 슬래시를 포함하지 않으면 어느 깊이에서든 일치하므로, 밋밋한 *.aux 한 줄이 이미 chapters/01-intro.aux까지 덮습니다. 덮지 못하는 것은 루트에 고정된 /*.aux 쪽이고, 프로젝트 루트에서 rm *.aux를 치는 청소 습관도 닿지 않습니다. 더 뜻밖인 것은 latexmk도 닿지 않는다는 점입니다. TeX Live 2024에서 시험해 보면 latexmk -c는 물론 latexmk -C조차 루트의 중간 파일만 지우고 chapters/*.aux는 남겨 둡니다. 그러니 오래된 .aux가 의심될 때, 곧 “건드린 적 없는 장에서 오류가 난다”는 부류의 문제일 때는 find . -name "*.aux" -delete처럼 명시적으로 치우십시오.
분할이 정말로 도구를 망가뜨리는 자리가 하나 있습니다. -output-directory입니다. \include를 쓰는 프로젝트에서 pdflatex -output-directory=build main.tex로 출력을 따로 빼려고 하면 실행이 죽습니다. TeX이 build/chapters/01-intro.aux를 열려 하는데 그 하위 디렉터리가 없기 때문이며, ! I can't write on file에 이어 치명적 오류가 나고 PDF는 한 장도 나오지 않습니다. TeX은 디렉터리를 만들지 않습니다. 해법은 둘입니다. 같은 구조의 하위 디렉터리를 미리 직접 파 두거나, 일을 latexmk -outdir=build에 넘기는 것입니다. latexmk는 하위 디렉터리를 스스로 만듭니다. 트리 바깥으로 빌드하는 여러 파일 프로젝트가 대개 엔진이 아니라 latexmk로 구동되는 이유가 이것입니다.
# fails: TeX will not create build/chapters/ by itself
pdflatex -output-directory=build main.tex
# ! I can't write on file ... chapters/01-intro.aux
# works: mirror the subdirectories first
mkdir -p build/chapters && pdflatex -output-directory=build main.tex
# works: latexmk creates them for you
latexmk -pdf -outdir=build main.tex바로 그 .fls 파일이 분할된 프로젝트에서 저장 시 재빌드가 성립하는 이유이기도 합니다. -recorder를 붙여 실행하면(latexmk가 알아서 붙입니다) 엔진이 열었던 모든 파일을 기록하므로, main.fls에는 장마다 INPUT chapters/01-intro.tex 줄이 늘어섭니다. latexmk는 거기서 얻은 의존 목록을 main.fdb_latexmk에 담아 전부 감시합니다. 그래서 3장을 저장하면 책 전체가 다시 빌드됩니다. “3장이 이 책의 일부다”라고 어디에도 적어 준 적이 없는데도 말입니다. 분할 구조를 두 번 선언할 필요는 없습니다. \include 줄의 나열이 이미 의존 선언입니다.
열어 둔 파일이 본 파일이 아닐 때 무엇이 망가지는가
증상은 셋이고, 서로 조금도 닮지 않았습니다. 첫째, 평범한 장 파일을 단독으로 컴파일하면 곧바로 멈춥니다. 첫 \chapter에서 ! Undefined control sequence., 이어서 ! LaTeX Error: Missing \begin{document}., 그리고 ! Emergency stop.이 나오고 PDF는 없습니다. \documentclass가 없는 파일이니 당연합니다. 둘째, subfiles 장을 단독으로 컴파일하면 더 고약합니다. 성공하기 때문입니다. 1쪽에서 시작하는 그럴듯한 한 장짜리 PDF가 나오고, 다른 장으로 향하는 상호 참조는 ??로 인쇄됩니다. 셋째, 잘못된 작업 디렉터리에서 시작한 실행은 대신 그림에서 넘어집니다. 프로젝트 안의 모든 상대 경로는 파일이 있는 자리가 아니라 빌드를 돌린 자리에서 풀리기 때문입니다.
- 빌드 키가 엉뚱한 파일을 조판한다 → 본 파일이 아닌 모든 파일에
% !TEX root를 첫 스무 줄 안에, 그 파일 기준 상대 경로로 적는다. ! LaTeX Error: Missing \begin{document}.→ 장 파일을 직접 컴파일하고 있다. 그 파일에는 프리앰블이 없고, 있어야 할 이유도 없다.- 그림이 사라지거나 파일을 못 찾아 멈춘다 → 빌드가 프로젝트 루트가 아닌 곳에서 돌고 있다. 상대 경로는 작업 디렉터리 기준이다.
- PDF를 클릭하면 장이 아니라 본 파일이 열린다 → 그 빌드에
-synctex=1이 없었거나, 뷰어가 오래된.synctex.gz를 읽고 있다. - 실패한 실험 뒤에 루트에
.log,.pdf가 흩어진다 → 엔진은 부산물을 입력 파일 옆이 아니라 작업 디렉터리에 쓴다.
결국 이 층을 조용하게 만드는 것은 두 가지 습관입니다. 하나, 파일 이름과 폴더 이름에 공백을 넣지 않는 것. 이 페이지에 나온 도구는 모두 언젠가 경로를 셸이나 %로 시작하는 매직 코멘트에 넘깁니다. 공백은 따옴표에서 비롯한 버그가 사는 자리입니다. 둘, 빌드는 반드시 프로젝트 루트에서 시작하는 것. 손으로 치든, Makefile에서든, 편집기에 맡기든 마찬가지입니다. 작업 디렉터리는 \includegraphics, \include, -output-directory가 함께 참조하는 유일한 기준점이라, 여기가 어긋나면 셋이 한꺼번에 어긋납니다. 이 둘만 지키면 여러 파일이라는 층은 보이지 않게 됩니다. 그리고 이 층은 보이지 않을 때에만 제 몫을 하고 있는 것입니다.