Vim / Neovim(vimtex)

Vim으로 LaTeX을 쓰는 사람은 LaTeX 때문에 Vim을 고른 것이 아닙니다. 이미 Vim이나 Neovim에 살고 있고, LaTeX 쪽이 찾아와 주기를 바랄 뿐입니다. 맨 Vim도 .tex가 무엇인지는 압니다. 구문 파일과 파일 타입 플러그인을 함께 배포하니까요. 다만 그 ftplugin/tex.vim의 머리말에는 지금도 Last Change: Wed 19 Apr 2006이라고 적혀 있습니다. 컴파일도, PDF 표시도, \ref 자동 완성도, 절 목록도 없습니다. 그 전부를 가져오는 것이 Karl Yngve Lervåg의 vimtex입니다. 이 페이지에서는 vimtex가 맨 Vim보다 무엇을 더 해 주는지, \ll 뒤에서 상주하는 latexmk 컴파일, 환경과 명령을 Vim 고유의 문법으로 편집하게 해 주는 텍스트 오브젝트, 그리고 Vim과 Neovim이 실제로 갈리는 지점을 다룹니다.

맨 Vim이 LaTeX에 대해 아는 것

맨 Vim이 .tex에 대해 아는 것은 색깔과 작은 재주 세 가지뿐입니다. [d\newcommand\def의 정의로 뛰고, gf[i\include{...} 안의 내용을 파일 이름으로 읽으며, matchit을 켜면 %\begin{...}\end{...} 사이를 오갑니다. 구현 전체가 ftplugin/tex.vim의 마흔 줄 남짓에 들어가고, 오늘의 Vim 9.1도 Benji Fisher의 이름과 Version: 1.4 / Last Change: Wed 19 Apr 2006이라는 줄을 그대로 붙인 채 배포합니다. 구문 파일 syntax/tex.vim은 Version 121까지 자랐지만, 그 머리말에는 이 런타임 파일이 새 관리자를 찾고 있다고 적혀 있습니다.

맨 Vim에는 컴파일러 정의 compiler/tex.vim도 들어 있습니다. :compiler tex를 실행하면 makeprglatex -interaction=nonstopmode가 되고 errorformat에는 LaTeX 로그를 읽는 긴 패턴이 설정되므로, :make 한 줄로 quickfix 목록에 오류가 늘어섭니다. 즉 맨 Vim으로도 “쓰고 :make 하고 오류 줄로 뛰기”까지는 됩니다. 못 하는 것은 PDF를 여는 일, 소스와 PDF를 오가는 일, \ref\cite를 완성하는 일, 문서 구조를 따라 이동하는 일입니다. vimtex는 그 전부를 떠맡고, 파일 타입 tex에 대해서는 Vim 내장 TeX 플러그인을 자기 것으로 갈아 끼웁니다.

vimrc
" What bare Vim gives you, without any plugin at all.
packadd! matchit          " % jumps between \begin{...} and \end{...}
compiler tex              " :make runs latex and fills the quickfix list

새로 만든 .texplaintex가 되어 vimtex가 안 뜨는 이유

답은 단순합니다. Vim이 파일 내용을 읽어서 추측하기 때문입니다. Vim 9.1의 autoload/dist/ft.vim에 있는 FTtex()는 먼저 첫 줄의 %&형식을 보고, 그다음 첫 비주석 줄에서부터 천 줄을 훑으며 \documentclass, \usepackage, \begin{, \newcommand, \renewcommand 중 하나를 찾습니다. 아무것도 없으면 기본값으로 떨어지는데, g:tex_flavor가 설정돼 있지 않으면 그 기본값은 plain입니다. 곧 파일 타입이 tex가 아니라 plaintex가 됩니다. 비어 있는 새 파일이나 아직 \documentclass를 쓰지 않은 조각이 정확히 이 경우입니다.

재미있는 대목은, 이 문제에 흔히 따라붙는 조언——vimrclet g:tex_flavor = 'latex'를 쓰라——이 vimtex에서는 방향이 반대라는 점입니다. vimtex는 자체 ftdetect/tex.vim에서 파일 타입 판정을 아예 덮어쓰고 g:tex_flavorlatex로 설정해 버립니다. 문서도 그 의도를 분명히 밝혀 두었습니다. .tex가 기본적으로 plaintex로 인식되는 예상 밖 동작을 막기 위해서라고요. 그러니 vimtex를 쓴다면 g:tex_flavor를 직접 쓸 필요가 없습니다. 반대로 latex가 아닌 값을 넣어 두면 vimtex의 덮어쓰기를 거부할 수 있습니다. 이 옵션이 쓸모를 갖는 쪽은 그쪽입니다.

vimtex 설치, 그리고 지연 로딩을 하면 안 되는 이유

결론부터: vimtex를 지연 로딩하지 마십시오. 이유는 취향이 아니라 구조입니다. 역검색(PDF에서 소스로 돌아오는 동작)은 :VimtexInverseSearch라는 전역 명령에 의존하고, 뷰어는 그 명령을 편집기 바깥에서 호출합니다. 플러그인 본체가 아직 로드되지 않았다면 그 명령은 존재하지 않습니다. 게다가 vimtex는 파일 타입 플러그인이고 autoload 기법을 쓰므로 애초에 필요할 때만 로드됩니다. 플러그인 관리자가 더 미뤄 줄 이득이 남아 있지 않습니다. lazy.nvim이면 lazy = false, vim-plug이면 for 절을 붙이지 않는 것이 정답입니다.

전제가 둘 더 있습니다. 첫째는 에디터 버전입니다. 2026년 7월에 나온 vimtex 2.18부터는 Vim 9.2 또는 Neovim 0.12.4를 요구하며, 그보다 낮으면 아예 로드되지 않습니다. 옛 에디터에 묶여 있다면 v2.17 태그로 고정하는 것이 바른 대처이고, let g:vimtex_version_check = 0으로 검사를 잠재울 수도 있습니다. 둘째는 filetype plugin onsyntax enable입니다. 앞의 것이 없으면 vimtex 자체가 로드되지 않고, 뒤의 것이 없으면 구문 정보에 기대는 기능——수식 영역 판정, i$ 텍스트 오브젝트——이 멈춥니다. Neovim에서 Tree-sitter에 기대더라도 Vim의 구문 기능은 켜 두는 편이 안전합니다.

vimrc
call plug#begin()
Plug 'lervag/vimtex'
" Pin an older tag if you are stuck on Vim < 9.2:
" Plug 'lervag/vimtex', { 'tag': 'v2.17' }
call plug#end()

filetype plugin indent on   " required (indent is optional)
syntax enable               " required for math zones, i$ and friends
set encoding=utf-8          " needed in Vim, not in Neovim

let maplocalleader = ' '    " Space as <localleader>; default is backslash
let g:vimtex_view_method = 'zathura'

Neovim에서는 같은 내용을 Lua로 씁니다. 설정은 init 안에 두어 플러그인 본체가 로드되기 전에 반영되도록 하는 것이 정석입니다. maplocalleader는 거의 모든 vimtex 명령의 입구입니다. 기본 접두는 g:vimtex_mappings_prefix가 정하고 그 기본값은 <localleader>l, <localleader> 자체의 기본값은 백슬래시입니다. 명시해 두면 나중에 헷갈리지 않습니다. 백슬래시 그대로면 \ll을 치고, 스페이스로 바꿨다면 스페이스에 이어 ll을 누릅니다.

lua
return {
  "lervag/vimtex",
  lazy = false,   -- never lazy-load: it breaks :VimtexInverseSearch
  init = function()
    vim.g.maplocalleader = " "
    vim.g.vimtex_view_method = "zathura"   -- "skim" on macOS
    vim.g.vimtex_compiler_method = "latexmk"
  end,
}

\ll — latexmk가 상주하며 저장할 때마다 PDF가 따라옵니다

\ll을 한 번 누르면 latexmk가 상주 모드로 돌기 시작하고, 다시 누르면 멈춥니다. 토글처럼 동작하는 것은 기본 컴파일러 설정 g:vimtex_compiler_latexmkcontinuous를 켜 두었기 때문이고, 내부에서는 latexmk의 연속 감시가 쓰입니다. 그다음부터는 저장할 때마다 다시 컴파일되고 뷰어의 PDF가 알아서 따라옵니다. 한 번만 돌리고 싶으면 \lS(:VimtexCompileSS), 멈추려면 \lk(모든 프로젝트는 \lK)입니다.

명령하는 일
\ll:VimtexCompile상주 컴파일 시작과 정지(토글)
\lS:VimtexCompileSS단발 컴파일. CI와 같은 한 번짜리 실행
\lv:VimtexViewPDF를 열고 커서 위치로 정방향 검색
\lt:VimtexTocOpen목차 버퍼 열기(\lT는 토글)
\le:VimtexErrors오류와 경고를 quickfix 창에 나열
\lo:VimtexCompileOutput컴파일러의 원본 출력 보기
\lc:VimtexClean보조 파일 정리(\lC는 결과물까지 삭제)
\li:VimtexInfo인식된 루트 파일, 패키지 등 상태 표시
\ls:VimtexToggleMain지금 파일을 루트로 볼지 전환

기본으로 latexmk에 넘어가는 옵션은 -verbose, -file-line-error, -synctex=1, -interaction=nonstopmode 네 가지입니다. -synctex=1이 처음부터 들어 있으므로, 뒤에서 다룰 정방향・역방향 검색에 필요한 동기화 데이터는 따로 손댈 것 없이 만들어집니다. 오류 쪽은 g:vimtex_quickfix_mode가 기본 2——quickfix 창은 저절로 열리지만 포커스는 가져가지 않습니다——라서 쓰면서 곁눈으로 오류를 보는 방식에 맞습니다. 컴파일러 자체를 바꾸려면 g:vimtex_compiler_methodlatexmk(기본), latexrun, tectonic, arara, texpresso, generic 중 하나를 지정합니다.

\ll이 다른 엔진을 부를 때 — -pdf$pdf_mode의 충돌

.latexmkrc$pdf_mode = 3을 썼는데도 DVI 경로로 가지 않습니다. 원인은 vimtex가 매번 엔진 플래그를 명령줄에 덧붙인다는 데 있습니다. 엔진은 대응표 g:vimtex_compiler_latexmk_engines에서 오고, 기본 키 _-pdf로 이어집니다. 명령줄의 -pdf.latexmkrc$pdf_mode보다 세므로 설정 파일이 조용히 덮어써집니다. vimtex가 $pdf_mode로 엔진을 추론해 주기는 하지만, 문서가 밝히듯 1(pdfLaTeX), 4(LuaLaTeX), 5(XeLaTeX) 세 가지뿐이고 DVI 경로인 3은 빠져 있습니다.

올바른 해법은 주 파일 맨 위에 TeX 프로그램 지시 주석을 쓰는 것입니다. 오른쪽 값은 위 대응표의 키여야 하므로 LuaLaTeX은 lualatex, DVI 경로는 pdfdvi(-pdfdvi에 대응)로 적습니다. 그런 다음 upLaTeX과 dvipdfmx의 실제 호출은 .latexmkrc에 씁니다. 일본어 논문의 정석 조합입니다. latexmk 설정 자체는 자동 빌드 페이지가 주인이니 자세한 내용은 그쪽을 보십시오.

latexmkrc
% In the main .tex file, first line: pick the key, not the binary name.
% !TeX program = pdfdvi

# .latexmkrc -- upLaTeX and dvipdfmx do the actual work
$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';
$max_repeat = 5;

$latex에도 -synctex=1을 넘기는 것이 핵심입니다. 그래야 DVI를 거치더라도 동기화 데이터가 PDF까지 실려 갑니다. latexmk를 떠나면 이야기가 달라져서, Tectonic으로 옮길 때는 g:vimtex_compiler_methodtectonic으로 두기만 하면 됩니다. 엔진 선택도 .latexmkrc도 필요 없습니다.

텍스트 오브젝트 — \begin{...}을 Vim의 문법으로 편집하기

이것이 Vim으로 LaTeX을 쓸 대체 불가능한 단 하나의 이유입니다. Vim의 편집은 “동사 + 대상”이라는 문법 위에 서 있습니다. d(삭제)나 c(변경)에 iw(단어 안쪽)나 ap(문단 전체)를 결합하죠. vimtex는 그 어휘에 LaTeX의 구조 자체를 더합니다. dae\begin{...}부터 \end{...}까지 통째로 지우고, cie는 내용만 갈아 끼우며, ci$는 인라인 수식의 안쪽만 다시 칩니다. 서른 줄짜리 align 환경을 지우려고 줄 수를 세는 일은 이제 없습니다.

오브젝트대상쓰는 자리
ie / ae환경(최상위 document 제외)dae로 환경째 삭제, cie로 내용만 교체
i$ / a$수식 환경($...$, \[...\])ci$로 수식 안쪽만 다시 입력
ic / ac명령과 그 인수dac\textbf{...}를 통째로 제거
id / ad짝을 이루는 구분 기호LaTeX판 ci(. \left(...\right)도 잡습니다
iP / aP절(section)daP로 한 절을 통째로 옮기거나 지웁니다
im / am\item 한 항목목록의 한 항목만 정확히 잡습니다

여기에 구조를 고쳐 쓰는 세 짝이 있습니다. ds는 감싼 것을 벗기고, cs는 바꾸고, ts는 전환합니다. dse는 환경을 벗겨 내용만 남기고, cseitemizeenumerate로 바꾸며(후보는 자동 완성이 내놓습니다), dsccsc는 명령에, dsdcsd는 구분 기호에 같은 일을 합니다. tse는 환경 전환, tss는 환경의 별표 유무 전환, tsc는 명령의 별표 전환, tsd(...)\left(...\right) 사이를 오갑니다. 이동은 %가 짝을 오가고, ]][[가 다음・이전 절의 머리로, ][[]가 절의 끝으로, ]m[m이 환경으로, ]n[n이 수식으로 뜁니다. 명령 위에 커서를 두고 K를 누르면 그 패키지의 문서가 열립니다.

긴 문서를 걷기 — 목차 버퍼와 \ref 자동 완성

\lt을 누르면 문서 전체의 목차가 평범한 버퍼로 열립니다. 평범한 버퍼이므로 /로 검색하고 jk로 걸어 다니며 Enter로 그 절로 뜁니다. 여러 파일로 나눈 문서라도 vimtex가 루트 파일에서 \input\include를 따라가므로 장을 가로지르는 한 장짜리 목차가 됩니다. 기본값은 폭 50칸의 분할 창이며, 모양과 동작은 g:vimtex_toc_config로 조정합니다. 지금 편집 중인 파일을 루트로 볼지는 \ls로 전환하고, \li로 vimtex가 어느 파일을 루트로 판단했는지 확인합니다.

자동 완성은 Vim 고유의 기법 위에 올라탑니다. tex 버퍼에서는 omnifunc가 자동으로 vimtex#complete#omnifunc로 설정되므로(g:vimtex_complete_enabled는 기본값이 켜짐), 삽입 모드에서 Ctrl-X Ctrl-O를 누르면 후보가 나옵니다. \cite{ 바로 뒤에서는 .bib\bibitem의 문헌 키가, \ref{ 뒤에서는 문서 안의 \label이, \usepackage{ 뒤에서는 설치된 .sty가, \includegraphics{ 뒤에서는 파일 이름이 늘어섭니다. 매번 그 키를 치는 것은 번거로우니 실무에서는 omnifunc를 자동 완성 엔진에 이어 줍니다. Neovim이면 nvim-cmp의 omni 소스, Vim과 Neovim 양쪽이면 coc.nvim의 coc-omni 확장입니다. 역할 분담은 늘 같습니다. 후보를 만드는 쪽은 vimtex, 언제 어떻게 보일지는 자동 완성 엔진.

lua
local cmp = require("cmp")
cmp.setup({
  sources = cmp.config.sources({
    { name = "omni" },   -- pulls vimtex candidates through omnifunc
  }),
})

Vim과 Neovim이 실제로 다른 지점

편집 기능만 놓고 보면 텍스트 오브젝트도 목차도 자동 완성도 양쪽이 완전히 같습니다. 갈리는 곳은 한 군데, 바깥에서 에디터를 불러들일 수 있는가입니다. 역검색은 뷰어가 에디터를 되부르는 일이라, Vim에서는 그 통로가 +clientserver 기능입니다. vimtex 문서는 Windows나 gVim이면 서버가 자동으로 뜨지만 Linux나 macOS의 터미널에서 도는 Vim에서는 뜨지 않는다고 분명히 밝힙니다. Neovim에는 clientserver가 아예 없고 대신 MessagePack-RPC를 쓰지만, vimtex 쪽의 처리는 같고 어느 쪽이든 상대 주소가 v:servername에 들어갑니다.

게다가 “터미널 Vim이면 서버를 띄우면 된다”로 끝나지 않는 경우도 있습니다. macOS에 기본으로 들어 있는 /usr/bin/vim-clientserver입니다. 기능 자체가 빠진 채 빌드되어 있어서 remote_startserver()가 존재하지 않고, 아래 조각을 써도 아무 일도 일어나지 않습니다. 이때의 선택지는 MacVim이나 Homebrew의 Vim처럼 +clientserver가 붙은 빌드를 쓰거나, Neovim으로 옮기는 것입니다. 뒤집어 말하면 이 절의 실무적 결론은 Neovim 쪽에는 설정이 전혀 필요 없다는 것입니다. 작은 차이가 둘 더 있습니다. Vim은 encodingutf-8로 두어야 하지만 Neovim은 그럴 필요가 없고, 요구되는 에디터 버전도 Vim 9.2와 Neovim 0.12.4로 따로 적혀 있습니다.

vimrc
" Vim only, and only in a build that has +clientserver.
if empty(v:servername) && exists('*remote_startserver')
  call remote_startserver('VIM')
endif

뷰어 선택과 역검색 명령줄

여기서 가장 오해가 많은 것이 g:vimtex_view_method의 기본값입니다. 플랫폼별로 영리하게 골라 주는 것이 아니라 어느 OS에서든 기본값은 general이며, 범용 실행으로 떨어집니다. Linux는 xdg-open, macOS는 open, Windows는 SumatraPDF 등입니다. 그 범용 뷰어는 g:vimtex_view_general_viewer로 지정합니다. 전용 방식으로 마련된 것은 zathura, zathura_simple, skim, mupdf, galley 그리고 sioyek이며, sumatrapdf라는 값은 없습니다. Windows에서 SumatraPDF를 쓰는 경우는 general을 거칩니다. SyncTeX 자체의 원리는 전용 페이지에 있으니 여기서는 설정만 봅니다.

정방향 검색(소스 → PDF)은 \lv만 누르면 되고 설정이 거의 필요 없습니다. 손이 가는 쪽은 역검색(PDF → 소스)이라, 뷰어에게 “클릭하면 이 명령을 실행하라”고 알려 주어야 합니다. 그 명령의 내용이 VimtexInverseSearch <줄> <파일>입니다. 유의할 점은 하나, 줄 번호와 파일 이름을 넣는 자리표시자 철자가 뷰어마다 다르다는 것입니다. zathura는 %{line}%{input}, Skim은 %line%file, SumatraPDF는 %l%f입니다. g:vimtex_view_method = 'zathura'로 두면 vimtex가 -x를 붙여 zathura를 띄우고 그 명령을 대신 넘겨주므로, 많은 환경에서는 아무것도 쓰지 않아도 Ctrl 클릭으로 돌아옵니다.

zathurarc
# Linux: zathura. Ctrl-click in the PDF jumps back to the source.
set synctex true
set synctex-editor-command "nvim --headless -c 'VimtexInverseSearch %{line} %{input}'"

macOS의 Skim에서는 환경설정의 Sync 탭에서 프리셋을 Custom으로 바꾸고 명령과 인수를 등록합니다. 역검색은 Cmd-Shift-클릭입니다. Windows의 SumatraPDF라면 설정의 역검색 명령줄 칸에 같은 모양의 한 줄을 넣습니다. 역검색은 더블 클릭입니다. gVim이라면 nvim --headless 부분을 vim -v --not-a-term -T dumb로 바꾸십시오.

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

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

가장 먼저 굳혀야 할 네 가지 동작

vimtex는 글을 쓰기 전에 통달해야 하는 도구가 아닙니다. \ll로 상주 컴파일을 시작하고, 저장하고, \lv로 PDF의 해당 지점을 보고, \le로 오류만 읽기——이 넷이 손가락에 들어오면 나머지 기능은 필요해진 날에 하나씩 더하면 충분합니다. 텍스트 오브젝트도 daecse 둘에서 시작해 ci$, tsd로 넓혀 가는 편이 좋습니다. 그러면 자기가 매일 어떤 조작을 하는지가 곧 보입니다.

문서를 여러 파일로 나누기 전에 한 번만 확인해 둘 것이 있습니다. 장 파일 안에서 \ll을 눌러도 같은 PDF가 나오는가입니다. \li로 vimtex가 어느 파일을 루트로 잡았는지 보고, 틀렸다면 \ls로 바꾸거나 % !TeX root = main.tex로 주 파일을 가리킵니다. 여기를 먼저 정해 두면 자동 완성 엔진과 스니펫을 얹는 단계에서 빌드 쪽이 흔들릴 걱정이 없습니다.