SyncTeX(정방향/역방향 검색)

18쪽짜리 글을 조판해 보면 PDF는 76,974바이트인데, 그 옆에 떨어지는 .synctex.gz는 159,347바이트——설명 대상보다 두 배 넘게 큽니다. 이 비대한 지도가 바로 SyncTeX이며, 하는 일은 딱 하나입니다. LaTeX 소스의 어느 줄이 어느 쪽의 어느 사각형이 되었는지를 기억해 두는 것입니다. 이 페이지에서는 그 파일을 풀어 속을 들여다보고, synctex viewsynctex edit을 손으로 두드려 양방향을 모두 돌려 보며, 왜 클릭한 단어가 아니라 줄로 이동하는지까지 살펴봅니다. 정방향 검색이 아무 반응도 없을 때의 점검 순서도 마지막에 실었습니다.

-synctex=1은 결국 무엇을 만드는가

-synctex=1을 붙이면 엔진은 PDF 외에 같은 위치·같은 기본 이름의 main.synctex.gz를 하나 더 씁니다. 붙이지 않으면 아무것도 쓰이지 않습니다. SyncTeX 설정에서 가장 흔한 누락이 바로 이것입니다. 값은 참·거짓이 아니라 비트이며 man synctex에 그대로 적혀 있습니다. 0이거나 지정하지 않으면 파일 없음, 양수면 gzip 압축, 음수면 압축하지 않은 순수 텍스트, 2 비트가 켜지면 압축은 유지하되 이름에서 .gz가 빠지고, 4는 pdfTeX의 form 지원, 8은 더 강한 압축입니다. 전부 켜려면 -synctex=15입니다. LuaTeX만은 붙임표 두 개짜리 --synctex=1을 씁니다. 이 장치는 TeX Live에도 MiKTeX에도 똑같이 들어 있어, pdfLaTeX・XeLaTeX・LuaLaTeX 어느 쪽에서든 같은 형태의 지도가 나옵니다.

terminal
pdflatex -synctex=1  main.tex     # writes main.synctex.gz
xelatex  -synctex=1  main.tex
lualatex --synctex=1 main.tex     # LuaTeX wants two dashes

pdflatex -synctex=-1 main.tex     # writes main.synctex, plain text
pdflatex -synctex=2  main.tex     # writes main.synctex -- still gzip inside!

2 비트에는 작은 함정이 있습니다. -synctex=2가 만드는 파일의 이름은 main.synctex인데 file로 보면 속은 여전히 gzip입니다. 확장자를 믿고 less로 열면 이진 잡음이 쏟아져 SyncTeX 파일이 깨졌다고 오해하게 됩니다. 내용만 읽고 싶다면 순순히 -synctex=-1을 쓰십시오. 명령줄을 만질 수 없는 환경(버튼 하나로 조판하는 GUI 등)에서는 소스 첫머리의 TeX 프리미티브 \synctex=1로도 켤 수 있습니다. 다만 이 경로는 압축본만 내놓습니다. \synctex=-1이라고 써도 이 자리의 TeX Live 2024에서는 main.synctex.gz가 나왔습니다. 순수 텍스트가 필요하면 명령줄뿐입니다.

생성 파일내용
(none)아무것도 쓰이지 않음. 양방향 모두 동작하지 않음
-synctex=0지정하지 않은 것과 동일. 명시적으로 끌 때
-synctex=1main.synctex.gzgzip 압축. 실무에서는 이것
-synctex=-1main.synctex순수 텍스트. 내부를 들여다볼 때
-synctex=2main.synctex이름은 비압축처럼 보이지만 속은 gzip. 헷갈립니다
-synctex=15main.synctex비트 1+2+4+8. form 지원과 강한 압축까지

.synctex.gz를 풀어 속을 읽어 보기

내용은 줄 단위 텍스트라 gunzip -c main.synctex.gz로 넘기면 그대로 읽힙니다. 구조는 서두(preamble)·본문(content)·후미(postamble)·추기(post scriptum)의 네 부분입니다. 서두에는 버전과 Input:가 있고, TeX이 연 모든 파일에 1부터 번호(태그)가 붙습니다. 내 main.tex뿐 아니라 article.clssize10.clo도 온갖 .stymain.aux도 줄줄이 들어갑니다. 지도가 이토록 비대해지는 이유의 절반이 여기 있습니다. 이어지는 Magnification, Unit, X Offset, Y Offset이 좌표계를 정의하며, Unit:1은 아래의 모든 수치가 sp(scaled point, 1pt의 65536분의 1) 단위라는 뜻입니다. X Offset:4736287은 정확히 1인치로, TeX이 예로부터 종이 왼쪽 위에서 잡아 두는 여백입니다.

terminal
$ gunzip -c main.synctex.gz        # abridged; ... marks omitted lines
SyncTeX Version:1
Input:1:/home/you/doc/./main.tex
Input:2:/usr/local/texlive/2024/texmf-dist/tex/latex/base/article.cls
Input:3:/usr/local/texlive/2024/texmf-dist/tex/latex/base/size10.clo
...
Input:10:/home/you/doc/./chap.tex
Output:pdf
Magnification:1000
Unit:1
X Offset:4736287
Y Offset:4736287
Content:
!962
{1
[1,17:4736286,46220574:26673152,41484288,0
(1,4:8799518,8865054:22609920,655359,0
x1,4:9330359,8865054
k1,4:31409438,8865054:11586343
...
)
]
}1

본문은 상자를 중첩해 기록한 것입니다. {1}1이 한 장(sheet), 곧 한 쪽이고, 대괄호 []는 세로 상자, 소괄호 ()는 가로 상자입니다. 각 여는 줄은 태그,행:x,y:너비,높이,깊이 꼴이라, (1,4:8799518,8865054:22609920,655359,0은 “태그 1(곧 main.tex)의 4행에서 나온 가로 상자”라는 뜻입니다. 위 예에서 main.tex의 4행은 \section{Forward and inverse}였습니다. 줄 첫 글자가 기록의 종류로, x는 현재 위치, k는 커닝, g는 글루, $는 수식, f는 pdfTeX의 form 참조, vh는 내용이 없는 세로·가로 상자, !는 파일 중간부터 읽기 위한 바이트 위치입니다.

이 정도 해상도로 모든 쪽을 기록하니 파일은 곧잘 비대해집니다. 서두의 18쪽짜리 글에서는 압축본이 159,347바이트, 풀면 638,962바이트——PDF 본체의 여덟 배가 넘고 줄 수로는 24,717줄이었습니다. 그래서 .synctex.gz는 결과물이 아니라 다시 만들 수 있는 작업 파일입니다. .gitignore에 넣고, latexmk의 @generated_exts에도 더해 청소 대상으로 두는 것이 정석입니다. 참고로 synctex(5) man 페이지는 이 형식을 공개 규격으로 여겨서는 안 되며 synctex 명령과 synctex_parser 라이브러리 외에는 파싱할 필요가 없다고 못 박습니다. 문제를 이해하려고 읽는 것은 괜찮지만, 자작 도구가 여기에 영구히 기대게 해서는 안 됩니다.

perl
# .latexmkrc -- keep -synctex=1 on every build, and clean the map up afterwards
$pdf_mode  = 1;
$pdflatex  = 'pdflatex -synctex=1 -interaction=nonstopmode -file-line-error %O %S';
@generated_exts = (@generated_exts, 'synctex.gz');

정방향 검색과 역검색을 명령줄에서 직접 돌려 보기

정방향 검색(forward search, 원고 → PDF)synctex view, 역검색(inverse search, PDF → 원고)synctex edit입니다. 편집기와 뷰어가 버튼 뒤에서 부르는 것이 바로 이 둘이거나 그 등가물이므로, LaTeX의 역검색이 말을 듣지 않을 때 이 둘을 직접 두드려 보면 “지도가 나쁜가, 편집기와 뷰어의 연결이 나쁜가”가 단번에 갈립니다. 정방향 검색은 -i 행:열:파일-o pdf를 받아 쪽 번호와 사각형을 돌려줍니다.

terminal
$ synctex view -i 5:1:main.tex -o main.pdf
This is SyncTeX command line utility, version 1.5
SyncTeX result begin
Output:main.pdf
Page:1
x:153.195526
y:156.585541
h:133.768356
v:158.522720
W:343.711060
H:8.855677
before:
offset:-1
middle:
after:
SyncTeX result end

xy는 “여기를 보여라”라는 점이고, h, v, W, H는 강조할 사각형의 왼쪽 끝·기준선·너비·높이입니다. 단위는 PDF의 포인트(bp)라서 v:158.52는 쪽 위에서 158.52pt 아래라는 뜻입니다. 뷰어는 이 수치를 받아 스크롤하고 W × H짜리 띠를 잠깐 번쩍입니다. 반대 방향은 그 좌표를 되던지는 것뿐입니다.

terminal
$ synctex edit -o 1:153.19:156.58:main.pdf
This is SyncTeX command line utility, version 1.5
SyncTeX result begin
Output:main.pdf
Input:/home/you/doc/./main.tex
Line:5
Column:-1
Offset:0
Context:
SyncTeX result end

인자는 -o 쪽:x:y:pdf 꼴이고 돌아오는 것은 파일의 절대 경로와 행 번호입니다. 뷰어는 이 Input:Line:을 편집기를 띄우는 명령에 끼워 넣어 호출합니다. 눈에 띄는 것은 Column:-1입니다. 형식 자체는 열을 표현할 수 있지만 엔진이 열을 쓰지 않으므로, 역검색은 사실상 늘 행 단위입니다. 편집기가 커서를 행 앞머리에 두는 것은 그 때문이지 설정 잘못이 아닙니다.

왜 클릭한 단어가 아니라 “행”으로 이동하는가

대응의 최소 단위가 조판된 상자이기 때문입니다. TeX은 문단을 하나의 긴 가로 나열로 만든 뒤 맨 마지막에 한꺼번에 행으로 자릅니다. SyncTeX이 기억하는 것은 잘려 나온 상자와 그 상자를 만든 소스 행뿐이며 단어도 글자도 기억하지 않습니다. 실측해 보면 이 비대칭이 잘 보입니다. 빈 줄 없이 열두 줄에 걸쳐 짧은 낱말을 적은 원고를 조판하면, 열두 줄이 단 두 개의 행 상자에 담깁니다. 정방향 검색을 5행부터 12행까지 차례로 두드리면 돌아오는 좌표가 모두 같습니다.

terminal
# twelve short source lines 5..16, no blank line between them
$ for l in 5 6 7 8 9 10 11 12 13 14 15 16; do
>   printf "src %-3s " $l; synctex view -i $l:1:res.tex -o res.pdf | grep "^v:"
> done
src 5   v:230.405960
src 6   v:230.405960
src 7   v:230.405960
src 8   v:230.405960
src 9   v:230.405960
src 10  v:230.405960
src 11  v:230.405960
src 12  v:230.405960
src 13  v:242.361130
src 14  v:242.361130
src 15  v:242.361130
src 16  v:242.361130

재미있는 것은 반대 방향이 조금 더 영리하다는 점입니다. 같은 행 상자를 왼쪽에서 오른쪽으로 훑으며 synctex edit을 두드리면 수평 위치에 따라 서로 다른 소스 행이 돌아옵니다. 게다가 한 점에 후보가 여럿 돌아오기도 하며, 뷰어는 보통 맨 앞을 택합니다. 즉 정방향 검색은 거칠고 역검색은 세밀합니다. 뒤집어 보면, 긴 소스 한 줄이 여덟 줄로 접힌 문단에서는 어느 줄을 눌러도 늘 3행이 돌아왔습니다. 기억할 소스 행 자체가 하나뿐이기 때문입니다. TikZ 그림 안, 복잡한 매크로의 전개 결과, 표 내부에서 겨냥한 곳에서 한두 낱말 어긋나는 것도 모두 이 상자 해상도의 이야기이지 결함이 아닙니다.

terminal
# same typeset line (baseline v=230.4), scanning left to right
$ for x in 135 185 235 310 360 435 460; do
>   printf "x=%-4s " $x; synctex edit -o 1:$x:229:res.pdf | grep "^Line:" | tr "\n" " "; echo
> done
x=135  Line:5
x=185  Line:5
x=235  Line:6 Line:7
x=310  Line:7 Line:8
x=360  Line:9 Line:10
x=435  Line:10 Line:11
x=460  Line:11 Line:12

여기서 실무적 결론이 하나 나옵니다. 소스를 한 줄로 길게 늘어놓으면 SyncTeX의 분해능은 그 문단 전체에 대해 한 점으로 떨어집니다. 반대로 한 문장 한 줄, 적어도 절 단위로 줄을 바꿔 두면 역검색이 훨씬 잘 맞습니다. 버전 관리의 차이를 읽기 쉽게 하는 작성법과 SyncTeX이 잘 듣는 작성법은 공교롭게도 같습니다.

\input한 자식 파일에서 행 번호가 어긋나는 이유

결론부터 말하면 \input 자체는 어긋남의 원인이 아닙니다. 각 기록은 행 번호뿐 아니라 태그를 가지며, 태그는 Input: 표를 가리킵니다. 자식 파일에는 자식 파일의 태그가 붙고 행 번호도 그 파일 안에서의 번호입니다. 실측에서도 \input{chap}으로 끌어온 장 안을 클릭하니 Input:에는 chap.tex가, Line:에는 그 안에서의 행 번호가 돌아왔습니다. 장을 스무 개 이어 붙여도 번호가 더해지지 않습니다.

진짜 원인은 두 가지입니다. 하나는 지도가 낡은 것입니다. .synctex.gz는 조판한 그 순간의 사진이므로, chap.tex 앞머리에 세 줄을 더하고 다시 컴파일하지 않은 채 역검색을 하면 지도는 여전히 Line:3을 돌려줍니다. 본문은 이미 6행으로 옮겨 갔는데도 말입니다. 어긋난 폭이 “끼워 넣은 줄 수와 정확히 같다”면 거의 틀림없이 이것입니다. 다른 하나는 절대 경로입니다. Input:에 적히는 것은 조판 당시의 전체 경로라서, 프로젝트를 옮기거나 심볼릭 링크를 거쳐 열거나 컨테이너 안에서 조판하고 밖에서 보면, 뷰어는 존재하지 않는 경로로 편집기를 보내려 합니다. 행이 어긋나는 것이 아니라 아예 다른 파일이 열리거나 아무것도 열리지 않는다면 이쪽을 의심하십시오.

뷰어별 역검색 명령과 치환 기호의 차이

역검색 설정은 뷰어 쪽에 씁니다. “클릭되면 이 행 번호와 이 파일 이름을 채워 이 명령을 실행하라”는 틀을 건네는 형태입니다. 성가신 것은 치환 기호의 서식이 뷰어마다 다르다는 점입니다. zathura는 중괄호가 붙은 %{line}%{input}, Skim은 %line%file, SumatraPDF와 Okular는 %l%f를 씁니다. 다른 곳에서 베껴 온 설정이 동작하지 않는 원인의 대부분이 이것으로, 명령 자체는 맞는데 치환 기호만 어긋나 있습니다.

뷰어주요 OS행과 파일의 치환 기호
zathuraLinux / BSD%{line}%{input}. 설정은 set synctex-editor-command
SkimmacOS%line%file. Preferences ▸ Sync ▸ Preset: Custom
SumatraPDFWindows%l%f. Settings ▸ Options의 inverse search 칸
OkularLinux / Windows%l%f. 설정 ▸ 편집기에서 지정(Kile이면 kile --line %l)
Adobe Acrobat / Reader전부SyncTeX 미지원. 역검색은 원리적으로 불가
ini
# zathura (~/.config/zathura/zathurarc)
set synctex true
set synctex-editor-command "nvim --headless -c 'VimtexInverseSearch %{line} %{input}'"

# Skim  -- Preferences > Sync > Preset: Custom
Command:   nvim
Arguments: --headless -c "VimtexInverseSearch %line '%file'"

# SumatraPDF -- Settings > Options > inverse search command-line
cmd /c start /min "" nvim --headless -c "VimtexInverseSearch %l '%f'"

# Okular -- Settings > Configure Okular > Editor
kile --line %l

편집기 쪽에서 정방향 검색을 일으키는 법은 단순합니다. TeXShop과 Skim이면 PDF 위에서 Cmd + 클릭, 반대 방향은 Shift + Cmd + 클릭입니다. TeXstudio는 Ctrl + 클릭 또는 “PDF로 이동”·“소스로 이동” 메뉴입니다. VS Code의 LaTeX Workshop은 Ctrl/Cmd+Alt+J입니다. 여기서 macOS 특유의 함정을 하나 짚어 둡니다. macOS에 딸려 오는 /usr/bin/vim-clientserver로 빌드되어 있어서 바깥에서 편집기를 되부를 통로 자체가 없고, 흔히 쓰이는 역검색 설정을 적어도 조용히 아무 일도 일어나지 않습니다. MacVim이나 Homebrew의 Vim, 또는 Neovim으로 옮기는 것이 해결책입니다.

DVI 경유(pLaTeX·upLaTeX → dvipdfmx)에서는 어떻게 되는가

결론부터 적으면, 기본 설정이라면 아무것도 할 필요가 없고 좌표는 PDF를 직접 뱉는 경로와 일치합니다. -synctex=1은 변환기가 아니라 엔진 쪽(platex / uplatex)에 넘깁니다. 엔진은 DVI를 쓰면서 .synctex.gz도 함께 쓰고, 서두의 Output:pdf가 아니라 dvi가 됩니다. 그 뒤에 dvipdfmx를 돌려도 이 지도에는 손끝 하나 대지 않습니다. 이 자리에서 dvipdfmx 전후를 cmp로 비교했더니 바이트 단위로 동일했습니다. 애초에 dvipdfmx에는 -synctex 옵션이 없습니다. 참고로 TeX Live 2024의 dvipdfmxxdvipdfmx로 가는 심볼릭 링크로, XeTeX용 변환기와 같은 하나의 바이너리입니다.

terminal
# the same source, both routes, same forward-search question
$ pdflatex -synctex=1 cmp.tex && synctex view -i 3:1:cmp.tex -o cmp.pdf
h:133.768356   v:137.554138

$ latex -synctex=1 cmp.tex && dvipdfmx cmp.dvi
$ synctex view -i 3:1:cmp.tex -o cmp.pdf
h:133.768372   v:137.554153

# the two agree to about 2e-5 pt -- nothing needs reconciling

그렇다면 synctex update는 무엇을 위한 것일까요. 매뉴얼의 표현 그대로 “dvi/xdv → pdf 필터를 적용한 뒤 SyncTeX 파일을 갱신하기” 위한 것이며, 필요한 경우는 변환에 배율이나 오프셋을 지정했을 때뿐입니다. -m / -x / -y에는 필터에 넘긴 것과 같은 값을 줍니다. 재미있는 것은 그 구현입니다. synctex update는 지도의 내용을 고쳐 쓰지 않습니다. 실제로 -x 20mm를 붙여 돌린 뒤 앞뒤를 바이트로 비교해 보니, 파일 끝 Post scriptum: 뒤에 gzip 덩어리가 덧붙여져 있을 뿐이었습니다. 풀어 보면 내용은 X Offset:20mm 한 줄. 곧 형식의 제4부 “추기”는 뒤따르는 변환기가 좌표계 보정을 쪽지처럼 붙여 두는 자리입니다. 평소에는 ptex2pdf나 latexmk가 이 일련을 알아서 처리하므로 마주칠 일이 거의 없습니다.

SyncTeX가 효과가 없을 때 위에서부터 볼 곳

먼저 .synctex.gz가 PDF와 같은 폴더에 있는지 봅니다. 없다면 빌드에 -synctex=1이 들어 있지 않습니다. 여기서 놓치기 쉬운 것이 편집기가 갖춰 놓은 기본 빌드 설정입니다. 예컨대 Kile이 함께 제공하는 PDFLaTeX 도구의 기본 옵션에는 -synctex=1이 들어 있지 않아, “설정했는데 아무것도 동기화되지 않는다”의 가장 큰 원인이 됩니다. 편집기 화면에서 SyncTeX 항목에 체크했다고 해서 실제로 실행되는 명령이 늘 바뀌는 것은 아니라는 점을 기억해 두십시오.

  • 지도가 있는가. ls.synctex.gz를 확인합니다. 없으면 빌드 명령에 -synctex=1을 더하고, 편집기의 기본 설정은 일단 의심하십시오.
  • PDF와 지도가 떨어져 있지 않은가. -output-directory를 쓰면 둘 다 출력 폴더에 함께 떨어지므로 문제없지만, PDF만 복사해 내보내면 지도가 따라오지 않아 아무 일도 일어나지 않습니다. 실측에서도 build/에서 main.pdf만 복사한 곳에서는 synctex view가 말없이 끝났습니다.
  • 지도가 낡지 않았는가. 저장한 뒤 다시 빌드했습니까. 어긋난 폭이 방금 끼워 넣은 줄 수와 같다면 확정입니다. latexmk를 -pvc로 돌려 저장할 때마다 다시 조판해 두면 이 실패는 거의 일어나지 않습니다.
  • 조판한 것이 정말 그 문서인가. 장 파일만 따로 컴파일하면 지도는 그 장만의 PDF를 설명합니다. 편집기의 “마스터 파일”·“루트 문서” 지정이 생각한 곳을 가리키는지 확인하십시오.
  • 뷰어가 SyncTeX를 지원하는가. Adobe Acrobat / Reader로는 역검색이 원리적으로 불가능합니다. Skim(macOS), SumatraPDF(Windows), Okular·zathura(Linux)로 바꾸십시오.
  • 치환 기호가 맞는가. %{line}, %line, %l을 혼동하는 것은 명령의 나머지가 옳기에 오히려 알아채기 어려운 실패입니다.
  • 명령줄에서 문제를 가릅니다. synctex viewsynctex edit을 직접 두드립니다. 여기서 옳은 답이 나오면 지도는 건강하고 문제는 편집기와 뷰어의 연결 쪽에 있습니다. 참고로 synctex는 결과를 찾지 못해도 종료 코드 0을 돌려주므로, 스크립트에서는 상태가 아니라 출력의 내용을 보아야 합니다.

끝으로 SyncTeX를 “설정 항목”이 아니라 교정의 작법으로 쓰기 위한 한 바퀴를 적어 둡니다. PDF를 보다가 거슬리는 낱말을 클릭해 원고로 돌아가고, 고치고, 저장하고, 다시 빌드하고, 정방향 검색으로 방금 고친 자리로 돌아옵니다. 이 한 바퀴가 매끄럽게 돌면 긴 문서에서 어디를 고칠지 찾아 헤매는 시간이 0이 됩니다. SyncTeX을 만든 제롬 로랑(Jérôme Laurens)이 붙인 Synchronize TeXnology라는 이름은 다소 거창하게 들리지만, 실제로 손에 쥐는 것은 “찾지 않아도 된다”는 이 한 가지입니다.