VS Code(LaTeX Workshop)

확장을 하나도 설치하지 않은 Visual Studio Code에서 .tex를 열어도 코드에는 이미 색이 입혀집니다. VS Code가 LaTeX 문법 정의를 처음부터 함께 배포하기 때문이며, 그 문법은 LaTeX Workshop 확장에서 떼어 낸 것입니다. 다만 색이 VS Code가 LaTeX에 대해 아는 전부입니다. 빌드도, PDF 표시도, 소스와 PDF를 오가는 이동도 모두 LaTeX Workshop의 몫이고, 조판 자체는 컴퓨터에 설치한 TeX 배포판이 자식 프로세스로 실행되어 수행합니다. 이 페이지에서는 빌드를 기술하는 “도구와 레시피”의 두 층 구조, 기본 레시피가 수많은 문서에서 다른 엔진을 부르는 이유, 그리고 내장 PDF 뷰어와 SyncTeX를 다룹니다.

VS Code가 스스로 아는 LaTeX

아무 확장도 없는 VS Code가 등록하는 것은 세 개의 언어 ID와 각각의 문법뿐입니다. .sty.clstex, .texlatex, .bibbibtex. 그것으로 끝이며 빌드 명령도, PDF 뷰어도, 자동 완성도, \ref로 이동하는 기능도 없습니다. 이 문법 파일들은 jlelong/vscode-latex-basics 저장소에서 왔고, 그 README에는 해당 파일들이 “원래 LaTeX Workshop의 일부였다”고 적혀 있습니다. VS Code가 이를 기본 포함하기 시작한 것은 2022년 1월 릴리스부터입니다. 곧 .tex를 열자마자 보이는 색은 확장을 설치하기도 전에 이미 그 확장의 작품인 셈입니다.

나머지를 모두 떠맡는 것이 LaTeX Workshop입니다(제작자 James Yu, 마켓플레이스 ID는 James-Yu.latex-workshop). 다만 이 확장도 TeX을 품고 있지는 않습니다. latexmk, pdflatex, biber 같은 실행 파일을 자식 프로세스로 띄우고 그 출력을 읽어 올 뿐이므로, 확장의 건강 상태는 뒤에 있는 배포판——TeX Live, MiKTeX, MacTeX——의 상태를 넘어설 수 없습니다. 여기서 실무적인 판별 규칙이 하나 나옵니다. 설정을 한 줄도 건드리기 전에 같은 프로젝트를 터미널에서 한 번 빌드해 보십시오. 거기서 latexmk가 실패한다면 settings.json을 아무리 고쳐도 소용이 없고, 반대로 터미널에서는 되는데 확장이 “명령을 찾을 수 없다”고 한다면 의심할 것은 확장이 아니라 VS Code가 물려받은 환경 변수입니다.

설치 자체는 평범합니다. 확장 보기(Ctrl/Cmd+Shift+X)에서 “LaTeX Workshop”을 찾아 넣으면 됩니다. 대신 딸려 오는 것은 작업 표면의 거의 전부입니다. 빌드 명령, PDF 미리 보기, 자동 완성, \ref\cite에서 대상으로의 이동, 문서 개요, 그리고 \input\include를 따라가 조립한 프로젝트 파일 트리——자동 빌드가 감시하는 것도 이 목록입니다. TeX을 설치하느라 PATH를 고쳤다면 VS Code를, 되도록 로그아웃했다가 다시 로그인해 재시작해서 새 환경을 다시 읽게 하십시오. 그런 다음 배포판이 응답하기는 하는지 터미널에서 확인합니다.

terminal
# does the TeX distribution answer at all?
latexmk --version

# does the project build outside the editor?
latexmk -pdf main.tex

# with a .latexmkrc that already picks the engine, no flags are needed
latexmk main.tex

# is the extension looking at the same PATH you are?
which latexmk

터미널에서 빌드가 통과하고 나면 남는 문제는 VS Code 쪽 세 가지로 좁혀집니다. 어떤 레시피가 도는가, 어떤 파일이 루트인가, PDF를 어디에 띄우는가. 이 페이지의 나머지는 그 셋에 대한 이야기입니다.

도구와 레시피: latex-workshop.latex.recipes 읽는 법

빌드는 두 층으로 기술됩니다. 도구(latex-workshop.latex.tools)는 “한 번 실행할 명령”의 정의로, namecommand(실행 파일)・args(인수 배열)로 이루어집니다. 레시피(latex-workshop.latex.recipes)는 “도구 이름을 순서대로 늘어놓은 목록”입니다. latexmk는 도구 하나짜리 레시피이고, pdflatex -> bibtex -> pdflatex * 2는 도구 네 개짜리 레시피입니다. 두 층으로 나뉜 이유는 같은 실행 파일을 상황에 따라 다른 인수로 쓰고 싶기 때문입니다. 기본 제공 도구에는 latexmk, lualatexmk, xelatexmk, latexmk_rconly, pdflatex, bibtex, tectonic 등이 있고, 레시피는 이들을 다시 조합할 뿐 명령 정의를 중복해 적지 않습니다.

terminal
{
  "name": "latexmk",
  "command": "latexmk",
  "args": [
    "-synctex=1",
    "-interaction=nonstopmode",
    "-file-line-error",
    "-pdf",
    "-outdir=%OUTDIR%",
    "%DOC%"
  ],
  "env": {}
}

인수를 하나씩 읽어 보면 설계 의도가 드러납니다. -synctex=1은 아래에서 다룰 SyncTeX 대응표를 만들라는 지시이고, -interaction=nonstopmode는 오류가 나도 입력을 기다리지 않고 끝까지 달리라는 지시이며, -file-line-error는 오류를 main.tex:42: Undefined control sequence 형태로 찍으라는 지시입니다. 확장이 “문제” 패널에서 해당 줄로 곧장 뛸 수 있는 것은 이 마지막 플래그 덕분입니다. -pdf는 latexmk에게 “pdfLaTeX으로 PDF를 바로 만들라”고 하는 것으로, 이 페이지 뒤쪽에서 말썽을 일으키는 플래그입니다. %…% 형태의 토큰은 확장이 실행 직전에 치환하는 자리표시자입니다.

자리표시자치환되는 값
%DOC%루트 파일의 경로(확장자 제외)
%DOC_EXT%루트 파일의 경로(확장자 포함)
%DOCFILE%루트 파일의 이름만(확장자 제외)
%DIR%루트 파일이 있는 디렉터리. outDir의 기본값
%OUTDIR%latex-workshop.latex.outDir로 정한 출력 위치
%TMPDIR%중간 파일용 임시 디렉터리. 소스를 어지럽히지 않습니다
%WORKSPACE_FOLDER%현재 열려 있는 작업 영역의 경로

어떤 레시피가 도는지는 latex-workshop.latex.recipe.default가 정합니다. 기본값은 "first"——목록의 맨 위가 쓰인다는 뜻이며, "lastUsed"로 두면 지난번에 고른 레시피를 기억합니다. 빌드 실행 단축키는 Ctrl+Alt+B(Mac은 Cmd+Alt+B)입니다. 특정 레시피를 한 번만 돌리고 싶으면 명령 팔레트의 “LaTeX Workshop: Build with recipe”를 쓰고, 파일 단위로 고정하려면 첫 줄에 %!LW recipe=latexmk (lualatex)를 씁니다. 다만 패널에서 손으로 레시피를 고른 경우에는 이 지시가 무시됩니다.

기본 레시피가 다른 엔진을 부르는 이유

답은 위의 도구 정의 안에 있습니다. -pdf가 latexmk에게 “pdfLaTeX으로 PDF를 바로 만들라”고 지시하는 인수이기 때문입니다. 이 한 단어가 “터미널에서는 되는데 VS Code에서는 안 된다”는 보고의 상당 부분을 설명합니다. 프리앰블에서 fontspec을 불러오는 문서——OpenType 글꼴을 쓰는 것, unicode-math를 쓰는 것, 최근 템플릿 대부분——라면 빌드가 ! Fatal Package fontspec Error: The fontspec package requires either XeTeX or LuaTeX.에서 멈춥니다. 한국어・일본어・중국어를 그대로 입력한 경우에는 ! LaTeX Error: Unicode character こ (U+3053) not set up for use with LaTeX.가 나옵니다. 두 메시지 어디에도 VS Code는 등장하지 않습니다. VS Code의 문제가 아니기 때문입니다.

해결책은 “다른 레시피를 고른다” 하나뿐이고, 지속성이 약한 순서로 네 가지가 있습니다. 이번 한 번만이라면 명령 팔레트의 “Build with recipe”. 이 파일만이라면 첫 줄에 %!LW recipe=…. 앞으로는 지난번에 고른 것으로라면 latex-workshop.latex.recipe.default"lastUsed"로. 프로젝트 전체에 고정하려면 settings.jsonlatex-workshop.latex.recipes 순서를 바꿔 원하는 레시피를 맨 앞에 둡니다(기본값이 "first"이기 때문입니다). 기본 레시피 목록에는 이미 latexmk (lualatex), latexmk (xelatex), latexmk (latexmkrc)가 들어 있으므로 대개는 쓰는 것이 아니라 고르는 것으로 끝납니다.

엔진은 settings.json이 아니라 .latexmkrc

에디터 설정에 적은 엔진 선택은 그 설정을 쓴 컴퓨터를 벗어나지 못합니다. .latexmkrc에 적으면 그것은 프로젝트와 함께 움직입니다. 공저자의 TeXstudio에서도, CI 컨테이너에서도, 맨몸의 latexmk main.tex에서도 같은 결과가 나옵니다. 기본 레시피 latexmk (latexmkrc)는 바로 이를 위한 것으로, 인수를 하나도 덧붙이지 않고 latexmk %DOC%만 실행합니다. 다음은 일본어 논문에서 오랫동안 정석이었던 upLaTeX + dvipdfmx 조합의 예입니다.

latex
$latex = 'uplatex -synctex=1 -interaction=nonstopmode -file-line-error %O %S';
$bibtex = 'upbibtex %O %B';
$biber = 'biber --bblencoding=utf8 -u -U --output_safechars %O %S';
$makeindex = 'upmendex %O -o %D %S';
$dvipdf = 'dvipdfmx %O -o %D %S';
$pdf_mode = 3;
$max_repeat = 5;

핵심은 $pdf_mode 한 줄입니다. 3은 “DVI를 만든 뒤 $dvipdf로 PDF를 만든다”는 경로이고, 1은 pdfLaTeX 직행, 4는 LuaLaTeX입니다. 색인은 일본어 정렬이 되는 upmendex가, 참고문헌은 upbibtex가 맡습니다. %S, %O, %D, %B는 latexmk 자신의 자리표시자로 각각 소스, 추가 옵션, 출력 대상, 확장자 없는 기본 이름을 뜻하며, 확장의 %DOC%와는 다른 계통이니 섞지 마십시오. 눈에 안 띄지만 중요한 대목은 $latex-synctex=1이 들어 있다는 점입니다. 이것을 빠뜨리면 아래에서 설명할 클릭 이동이 아무 메시지 없이 작동하지 않게 됩니다.

반대로 .latexmkrc 없이 settings.json만으로 끝내고 싶다면 도구와 레시피를 직접 쓰고 그 레시피를 맨 앞에 둡니다. 다음은 LuaLaTeX로 조판하는 자족적인 예입니다(LuaLaTeX은 luatexjaltjsclasses 계열로 일본어를 조판하므로 dvipdfmx를 거칠 필요가 없습니다).

terminal
{
  "latex-workshop.latex.tools": [
    {
      "name": "lualatexmk",
      "command": "latexmk",
      "args": [
        "-synctex=1",
        "-interaction=nonstopmode",
        "-file-line-error",
        "-lualatex",
        "-outdir=%OUTDIR%",
        "%DOC%"
      ],
      "env": {}
    }
  ],
  "latex-workshop.latex.recipes": [
    { "name": "lualatexmk", "tools": ["lualatexmk"] }
  ]
}

가장 먼저 정할 세 가지 설정: 출력 위치, 자동 빌드, PDF 표시

설정은 settings.json에 씁니다. Ctrl/Cmd+,로 설정 화면을 열고 오른쪽 위의 “설정(JSON) 열기”를 쓰면, 전역 사용자 파일이나 프로젝트 안의 .vscode/settings.json 중 하나를 편집할 수 있습니다. 공저자나 빌드 서버와 공유하고 싶은 것은 반드시 후자에 둡니다. 수십 가지 설정 가운데 처음에 뜻을 정해 두면 뒤가 편해지는 것은 다음 셋입니다.

  • latex-workshop.latex.outDir — 중간 파일과 PDF의 출력 위치. 기본값은 %DIR%.tex와 같은 자리입니다. %DIR%/out으로 두면 .aux, .log, .fls가 소스 폴더에 흩어지지 않고 .gitignore도 한 줄로 끝납니다.
  • latex-workshop.latex.autoBuild.run — 자동 빌드의 계기. 기본값은 onFileChange로, 의존 파일을 디스크에서 감시하므로 편집기 밖의 변경에도 반응합니다. 그 밖에 onSave(저장할 때만)와 never(수동만)가 있습니다. 무엇이 빌드를 일으켰는지 놓치기 쉬운 사람에게는 onSave가 읽기 쉬운 선택입니다.
  • latex-workshop.view.pdf.viewer — PDF가 나타나는 곳: tab(기본값, VS Code 안의 탭), browser(기본 브라우저), external(외부 프로그램, 실험적 취급). SyncTeX을 매끄럽게 쓰려면 tab입니다.
terminal
{
  "latex-workshop.latex.outDir": "%DIR%/out",
  "latex-workshop.latex.autoBuild.run": "onSave",
  "latex-workshop.view.pdf.viewer": "tab",
  "latex-workshop.latex.recipe.default": "lastUsed"
}

출력 위치를 나눌 때 함정이 하나 있습니다. outDir를 바꾸면 확장이 .aux.fls를 찾는 자리도 함께 바뀝니다. 이것이 실제 빌드가 파일을 쓰는 자리와 어긋나면, PDF는 만들어졌는데 확장이 찾지 못하고 상호 참조가 끝내 풀리지 않는 상태가 됩니다. 둘을 같은 값으로 맞추십시오. 특히 .latexmkrc에서 $out_dir를 지정한 경우에 그렇습니다. 중간 파일 정리는 latex-workshop.latex.autoClean.run이 맡지만, 어차피 out/에 모아 두었다면 폴더째 지우면 되므로 대개는 필요하지 않습니다.

장 파일을 편집하면서 main.tex를 빌드하기: % !TEX root

자식 파일 첫 줄에 % !TEX root = ../main.tex라고 적습니다. 이것만으로 그 장만 열어 두어도 빌드는 본문 파일에서 시작합니다. 이 한 줄이 듣는 이유는 LaTeX Workshop이 루트 파일을 다섯 단계로 찾고 그 첫 단계가 이 매직 코멘트이기 때문입니다. (1) % !TEX root, (2) 열려 있는 파일 자체에 \documentclass\begin{document}가 있는가, (3) 작업 영역 최상위의 .tex들을 훑어 클래스 선언이 있는 파일을 찾기, (4) subfiles 패키지 구성, (5) .fls 파일 분석. 그냥 두어도 맞히는 일이 많지만, 장 파일이 수십 개인 학위논문에서는 “맞히는 일”이라는 사실 자체가 사고의 씨앗입니다.

latex
% !TEX root = ../main.tex
% !TEX program = lualatex

\section{Method}
% Building from inside this chapter still starts at main.tex.
  • % !TEX root 말고도 % !TEX program, % !TEX options, % !BIB program도 읽힙니다. 한꺼번에 끄려면 latex-workshop.latex.build.enableMagicCommentsfalse로 둡니다.
  • 작업 영역은 main.tex가 있는 프로젝트 루트에서 엽니다. 장 폴더만 따로 열면 (3)번 탐색이 애초에 본문 파일에 닿지 않습니다.
  • % !TEX root의 경로는 그것을 적어 둔 파일 기준의 상대 경로입니다. 장을 다른 폴더로 옮기면 이 줄도 고쳐야 합니다.
  • 레시피 선택과 % !TEX program 지정이 어긋나면 길을 잃기 쉽습니다. 팀으로 쓴다면 결정을 .latexmkrc로 몰고 레시피는 latexmk (latexmkrc)로 통일하는 편이 안전합니다.

내장 PDF 뷰어와 Ctrl+클릭으로 뛰는 SyncTeX

tab을 골랐을 때 열리는 PDF 뷰어의 정체는 Mozilla의 PDF.js를 품은 웹 페이지이며, 확장이 로컬에 띄운 작은 서버가 이를 제공합니다. 그래서 browser로 바꿔도 똑같은 뷰어가 나오고, PDF 렌더링이 운영체제나 설치된 PDF 프로그램에 좌우되지 않습니다. external만 다릅니다. 파일을 다른 프로그램에 넘길 뿐이라 실험적 취급이며, 외부 뷰어에서의 정방향 검색은 latex-workshop.view.pdf.external.synctex.command 같은 키로 따로 구성해야 합니다.

여기서 붙들어 둘 점은 SyncTeX이 에디터의 기능이 아니라는 사실입니다. 소스 줄과 PDF 위치의 대응표를 써 내는 것은 TeX 엔진이고, 그 스위치가 -synctex=1입니다. 확장이 .synctex.gz를 읽을 수 있는 것은 레시피가 그 플래그를 넘겨 주었기 때문일 뿐입니다. 직접 도구를 정의하면서 이 플래그를 빠뜨리면 빌드는 성공하고 PDF도 나오는데 클릭 이동만 조용히 멈춥니다. 오류는 어디에도 나오지 않습니다. “어제까지는 됐는데” 싶으면 먼저 레시피의 인수를 의심하십시오.

동작은 둘만 외우면 됩니다. 정방향 검색(소스 → PDF)은 커서 위치에서 PDF의 대응 지점으로 뛰는 것으로 Ctrl+Alt+J(Mac은 Cmd+Alt+J)입니다. 명령 팔레트에서는 “LaTeX Workshop: SyncTeX from cursor”입니다. 빌드 직후 자동으로 뛰게 하려면 latex-workshop.synctex.afterBuild.enabledtrue로 둡니다. 역방향 검색(PDF → 소스)은 내장 뷰어에서 Ctrl+클릭(Mac은 Cmd+클릭)입니다. 이 동작은 latex-workshop.view.pdf.internal.synctex.keybinding에서 ctrl-click(기본값)이나 double-click 중에 고를 수 있습니다. 덧붙이면 빌드는 Ctrl+Alt+B, PDF 열기는 Ctrl+Alt+V입니다.

DVI를 거치는 일본어 빌드에서도 SyncTeX은 살아 있습니다. 위의 .latexmkrc처럼 $latex-synctex=1을 넘겨 두면 upLaTeX이 써 둔 대응 정보가 dvipdfmx를 지나 PDF까지 도달합니다. “pdfLaTeX 직행이어야만 뛸 수 있다”는 것은 사실이 아닙니다. SyncTeX 자체의 구조——.synctex.gz 안에 무엇이 들어 있는지, 음수를 주면 압축되지 않은 읽을 수 있는 텍스트 파일이 나온다는 것——는 SyncTeX 페이지에 맡깁니다.