공동 작업과 변경 추적

.docx는 ZIP으로 묶은 XML 뭉치입니다. 그 두 초고를 Git에 보여 줘도 돌아오는 대답은 “바이너리 파일이 다릅니다” 한 줄뿐입니다. 반면 LaTeX 원고는 일반 텍스트여서 git diff가 공동 저자가 옮긴 바로 그 문장을 짚어 줍니다. 버전 관리와 공동 집필이 LaTeX과 잘 맞는 이유는 결국 이 한 가지에 있습니다. 다만 저절로 맞아떨어지지는 않습니다. Git이 비교하는 것은 문장이 아니라 줄이며, 문단 전체를 한 줄로 쓰면 그것은 쪼갤 수 없는 한 줄입니다. 더 나쁜 것은, <<<<<<< HEAD가 남아 있는 파일도 LaTeX은 아무 불평 없이 컴파일해 충돌 흔적을 그대로 PDF에 찍어 낸다는 점입니다. 이 페이지에서는 무엇을 커밋할지, 어디에서 줄을 바꿔야 diff가 읽히는지, 병합 충돌을 어떻게 넘길지, 그리고 latexdifftodonoteschanges가 소스 트리를 “공동 저자가 실제로 검토할 수 있는 것”으로 바꾸는 방법을 다룹니다.

git diff.tex에는 통하고 .docx에는 통하지 않는 이유

git diff가 두 파일을 줄 단위로 대조하는 도구에 지나지 않기 때문입니다. .tex에는 사람이 친 글자가 그대로 순서대로 들어 있으므로, 대조 결과는 “이 줄이 저 줄로 바뀌었다”는 읽히는 보고서가 됩니다. .docx 내부는 압축된 XML이라 쉼표 하나만 더해도 압축된 바이트가 통째로 달라지고, Git은 “다르다” 이상을 말하지 못합니다. 워드프로세서 문서의 공동 작업이 첨부 파일을 주고받다가 마지막에 누군가 손으로 합치는 일이 되기 쉬운 것도 이 때문입니다. LaTeX에서는 그 왕복이 사라지고 브랜치와 병합이 그 역할을 대신합니다. 뒤집어 말하면, 저장소에는 텍스트로 읽을 가치가 있는 파일만 두고 diff에 아무 의미도 없는 생성물은 처음부터 빼 둔다는 것입니다. 공동 집필 저장소의 설계는 거의 이 한 가지로 끝납니다.

Git에는 한 걸음 더 나아간 배려가 준비되어 있습니다. 각 diff 덩어리의 머리글 줄(@@ -3,2 +3,2 @@로 시작하는 줄)에는 보통 근처에 있던 아무 줄이나 붙습니다. 그런데 .gitattributes*.tex diff=tex 한 줄을 넣어 두면 Git이 내장된 TeX 규칙을 써서 그 덩어리를 감싸는 \section의 이름을 머리글에 실어 줍니다. 수백 쪽짜리 원고에서는 diff를 훑기만 해도 각 변경이 어느 절에 속하는지 보이므로 효과가 작지 않습니다. 설정은 한 줄이고 부작용은 없습니다.

terminal
# .gitattributes — teach git the structure of a .tex file
*.tex diff=tex

# hunk headers now name the enclosing sectioning command:
#   @@ -3,2 +3,2 @@ \section{First}
# without it, git prints an arbitrary nearby line instead.

무엇을 커밋하고 무엇을 .gitignore에 넣는가

커밋하는 것은 사람이 손으로 쓴 것뿐입니다. .tex 파일, .bib 데이터베이스, 그림의 원본 형식, latexmkrcMakefile, 그리고 문서가 의존하는 클래스 파일과 스타일 파일이 그것입니다. 이것들만 있으면 누구의 환경에서든 같은 PDF가 만들어집니다. 반대로 컴파일하면 다시 생성되는 파일은 하나도 빠짐없이 .gitignore에 넣습니다. biblatexbiber를 쓴 최소 문서에 latexmk를 한 번 돌리기만 해도 .aux, .bbl, .bcf, .blg, .fdb_latexmk, .fls, .log, .run.xml, .toc가 생깁니다. 색인을 만들면 .idx, .ilg, .ind가, hyperref를 쓰면 .out이, SyncTeX을 켜면 .synctex.gz가 더해집니다. 이것들을 추적하면 본문을 한 글자도 건드리지 않은 커밋조차 매번 수백 줄의 잡음을 실어 나릅니다.

terminal
# .gitignore — everything below is regenerated by a build
*.aux
*.log
*.out
*.toc
*.lof
*.lot
*.fls
*.fdb_latexmk
*.synctex.gz
*.bbl
*.blg
*.bcf
*.run.xml
*.idx
*.ilg
*.ind

# generated PDFs: ignore the working build, keep tagged releases by hand
main.pdf
*-diff*.tex

생성된 PDF는 이 방침에서 유일하게 예외가 될 만한 후보입니다. 빌드할 때마다 바뀌는 main.pdf를 추적하면 diff는 읽히지 않고 저장소만 불어납니다. 평소에는 무시하고, 나중에 한 글자도 다르지 않게 다시 꺼내야 하는 판본, 즉 투고본이나 릴리스본만 태그나 Release에 붙여 두는 편이 다루기 쉽습니다. .bbl도 같은 논리입니다. 어디까지나 빌드 산물이지만, 투고처가 .bbl을 포함한 소스 묶음을 요구한다면 제출 직전에 만들어 따로 동봉합니다. 저장소에 상시로 둘 이유는 되지 않습니다. 한 가지 더, latexdiff가 만들어 내는 *-diff*.tex도 무시 대상입니다. 이것은 원고가 아니라 산출물이며, 본류에 섞여 들어가면 다음 판에서 \DIF 명령으로 뒤덮인 원고를 편집하게 됩니다.

한 문장 한 줄 — diff의 단위는 줄바꿈입니다

원고를 한 문장에 한 줄로 쓰십시오. LaTeX에서 줄바꿈 하나는 공백 하나에 지나지 않으므로 조판 결과는 한 글자도 달라지지 않습니다. 달라지는 것은 diff의 가독성뿐입니다. 문단을 한 줄로 쓰면 쉼표 하나를 고쳐도 Git은 그 문단 전체를 “지우고 다시 넣었다”고 보고합니다. 문장마다 줄을 바꿔 두면 보고되는 것은 바뀐 그 한 문장뿐입니다. 변경을 검토하는 공동 저자에게는 이 차이 하나만으로도 상황이 완전히 달라집니다. 줄바꿈은 조판에 영향을 주지 않으므로 기존 원고를 언제든 한 문장 한 줄로 바꿔도 PDF는 그대로입니다. 다만 이 전환은 모든 줄을 움직이므로 반드시 별도의 커밋으로 하고, 내용 변경과 섞지 않습니다.

terminal
# whole paragraph on one line: git rewrites the entire paragraph
-The fox jumps over the dog. The morning was fine. Nobody minded.
+The fox jumps over the dog. The morning was cold. Nobody minded.

# one sentence per line: git points at the sentence that moved
 The fox jumps over the dog.
-The morning was fine.
+The morning was cold.
 Nobody minded.

일본어・중국어・한국어에서는 이 조언이 단순히 유용한 정도를 넘어 절실해집니다. 영어에는 도피처가 있습니다. git diff --word-diff는 줄이 길어도 바뀐 단어만 [-옛것-]{+새것+} 형태로 보여 줍니다. 그런데 --word-diff가 단어 경계로 삼는 것은 공백입니다. 띄어쓰기가 없는 일본어에서 시험해 보면, 吾輩は猫である。名前はまだ無い。 에서 글자 하나만 바꿔도 Git은 여전히 줄 전체를 지우고 줄 전체를 다시 넣습니다. 그렇다면 --word-diff-regex=.로 한 글자씩 비교하게 해도 소용이 없습니다. 패턴이 바이트 단위로 적용되어 멀티바이트 UTF-8 문자가 잘려 나가고, 吾輩は?[-??-]{+??+}である。 같은 깨진 글자가 돌아옵니다. 즉 CJK 원고에는 빠져나갈 길이 없습니다. 한 문장 한 줄은 영어권에서는 좋은 습관이지만, 일본어・중국어・한국어에서는 사실상 유일한 수단입니다.

.tex의 병합 충돌 해결 — LaTeX은 경고해 주지 않습니다

충돌 표시를 지우는 것을 잊은 채 pdflatex을 돌려도 오류는 하나도 나지 않습니다. <<<<<<<, =======, >>>>>>>는 모두 본문 모드에서 합법적인 문자열이며, LaTeX에게는 그저 문장부호일 뿐입니다. 그래서 컴파일은 종료 코드 0으로 성공하고, 두 판본과 그 사이의 충돌 표시가 함께 인쇄된 PDF가 만들어집니다. 기본 OT1 인코딩에서는 <>가 뒤집힌 문장부호로 대응되므로, 지면에는 ¡¡¡¡¡¡¡ HEAD¿¿¿¿¿¿¿ feature라는 낯선 두 줄이 나타납니다. 이런 줄을 보면 먼저 해결하지 못한 충돌을 의심하십시오.

latex
% what git leaves behind - and what LaTeX happily typesets
\begin{document}
<<<<<<< HEAD
Main branch sentence.
=======
Feature branch sentence.
>>>>>>> feature
\end{document}

% check before every build:
%   git grep -n "^<<<<<<< " -- "*.tex"

해결 작업 자체는 평범한 Git 작업과 같습니다. git status가 알려 준 파일을 열고, <<<<<<<부터 >>>>>>>까지 중 어느 쪽을 남길지(또는 양쪽을 하나로 다시 쓸지) 정한 뒤, 표시를 지우고 git add 합니다. LaTeX 특유의 주의점은 둘입니다. 첫째, 충돌이 \begin{itemize}\end{itemize} 사이에 떨어지면 한쪽만 남겼을 때 열고 닫는 짝이 무너져 이번에는 정말로 컴파일이 실패합니다. 표시를 지울 때는 모든 환경이 제대로 열리고 닫히는지 눈으로 확인하십시오. 둘째, 충돌은 예방할 수 있습니다. 한 문장 한 줄로 써 두면 Git이 문장 단위로 자동 병합하므로, 두 사람이 서로 다른 문장을 고친 한 충돌 자체가 일어나지 않습니다. 두 사람이 같은 절을 동시에 다시 쓸 예정이라면 \include로 파일을 나누고 파일 단위로 담당을 가르는 편이 확실합니다.

latexdiff — 두 판본을 “변경이 보이는 PDF”로 바꾸기

latexdiff가 내놓는 것은 PDF가 아니라 변경 표시를 심어 넣은 새 .tex 파일입니다. 그것을 직접 컴파일하면 워드프로세서의 변경 추적 화면 같은 PDF가 나옵니다. 기본 스타일에서 추가된 단어는 파란 물결 밑줄(ulem\uwave), 삭제된 단어는 빨간 취소선(\sout)으로 나타나며, 이에 필요한 \RequirePackage는 생성 파일의 전처리부에 자동으로 더해집니다. 삽입되는 명령은 모두 \DIF로 시작합니다. \DIFadd, \DIFdel, \DIFaddbegin, 그리고 플로트 안에서는 \DIFaddFL이므로 나중에 기계적으로 가려낼 수 있습니다. 제작자는 F. J. Tilmann이고, TeX Live 2024에 들어 있는 버전은 1.3.3입니다.

terminal
latexdiff --flatten old.tex new.tex > diff.tex
pdflatex diff.tex        # additions blue and underlined, deletions red and struck out

# what latexdiff actually writes into the body:
#   The quick \DIFdelbegin \DIFdel{brown fox jumps }\DIFdelend
#   \DIFaddbegin \DIFadd{red fox leaps }\DIFaddend over the lazy dog.

여기서 눈여겨볼 점은 latexdiff단어 단위로 비교한다는 것입니다. 위 예에서는 “brown fox jumps”와 “red fox leaps”를 맞세울 뿐, 줄 전체가 교체되었다고 보고하지 않습니다. 줄 단위인 git diff와 대조적이며, 둘은 경쟁하기보다 역할을 나눕니다. 기록과 자동 병합은 Git이, 공동 저자에게 “무엇이 어떻게 바뀌었는지”를 보여 주는 일은 latexdiff가 맡습니다. 실무에서 걸리는 함정은 셋입니다. \input이나 \include로 나눈 문서는 --flatten을 붙이지 않으면 최상위 파일만 비교됩니다. 별행 수식 안의 표시 세밀도는 --math-markup=level로 조절할 수 있으며, 수식이 망가질 때는 값을 낮춥니다. 그리고 생성된 diff.tex는 원고가 아닙니다. 반드시 별도 이름 그대로 두고, 수정은 원래의 .tex에 넣습니다.

latexdiff-vc로 Git 리비전과 직접 비교하기

옛 판본을 일부러 꺼내 놓을 필요는 없습니다. 함께 설치되는 latexdiff-vc--git -r HEAD~3처럼 리비전을 건네면, 그 판본을 일시적으로 꺼내 비교한 뒤 차이 파일을 main-diffHEAD~3.tex라는 이름으로 씁니다. 버전 관리 시스템을 지정하지 않으면 알아서 추측하지만, --git, --svn, --hg, --cvs, --rcs로 명시하는 편이 확실합니다. --pdf를 더하면 차이 파일에 대해 pdflatex을 두 번 돌려 PDF까지 만들어 줍니다. “투고본과 수정본 사이에 무엇이 바뀌었는지”를 심사자에게 보이는 일이 명령 한 줄로 끝납니다. 생성된 *-diff*.tex.gitignore에 넣어 두는 것을 잊지 마십시오.

terminal
latexdiff-vc --git -r HEAD~3 main.tex     # writes main-diffHEAD~3.tex
latexdiff-vc --git --pdf -r v1.0 main.tex # ...and builds the PDF as well

# output of the run:
#   Running: latexdiff "main-oldtmp-15378.tex" "main.tex" > "main-diffHEAD~3.tex"
#   Generated difference file main-diffHEAD~3.tex

todonotes — 최종본에서 사라지는 여백 메모

\todo{...}는 여백에 색이 있는 쪽지를 붙이고, \listoftodos는 그것들을 한 장의 목록으로 모읍니다. 원고에 “나중에 고칠 것”을 남기기 위한 가장 작은 도구이며, % TODO 주석보다 나은 점은 딱 하나, 인쇄물에 보인다는 것입니다. 보이기 때문에 잊지 않습니다. 메모가 본문 흐름에 끼어들어야 한다면 \todo[inline]{...}, 아직 그리지 않은 그림의 자리를 잡아 두려면 \missingfigure{...}를 쓰고, TODO 목록 자체를 차례에 올리고 싶으면 \todototoc을 넣습니다. 최종본에서는 \usepackage[disable]{todonotes}로 바꾸기만 하면 \todo 호출을 하나도 지우지 않은 채 모든 표시가 지면에서 사라집니다. 대신 obeyFinal을 넘기면 문서 클래스의 final 옵션을 자동으로 따르게 할 수도 있습니다.

latex
\usepackage{todonotes}          % [disable] hides every note in the final build
...
\todo{Citation needed here}
\todo[inline]{Rewrite this paragraph before submission}
\missingfigure{Circuit diagram goes here}
\listoftodos

changes — 저자별 수정 표시와 Undefined changes author 오류

여러 사람이 한 원고에 수정 표시를 넣는다면 changes 패키지가 알맞습니다. \added{...}, \deleted{...}, \replaced{새것}{옛것}, \highlight{...}, \comment{...}로 각 편집의 의도를 밝힐 수 있고, \listofchanges가 전체 변경 목록을 만들며, draft 옵션은 표시를 보여 주고 final로 바꾸면 흔적 없이 사라집니다. 다만 누구나 처음에 한 번은 걸려 넘어지는 곳이 있습니다. 저자 AB를 정의하지 않은 채 \added[id=AB]{...}를 쓰면 컴파일이 이렇게 멈춥니다. ! Package changes Error: Undefined changes author: AB. 이어서 xcolorUndefined color 오류가 줄줄이 따라오지만 원인은 모두 같습니다. 전처리부에 \definechangesauthor[name={...}, color=blue]{AB} 한 줄을 더하면 해결됩니다. 저자마다 다른 색을 배정하는 것이 이 패키지의 요점이므로, 공동 저자가 늘 때마다 한 줄씩 더해 가게 됩니다.

latex
\usepackage[draft]{changes}     % swap draft for final to hide all markup
\definechangesauthor[name={Ada Byron}, color=blue]{AB}
\definechangesauthor[name={Bob Lane}, color=orange]{BL}
...
\added[id=AB]{A sentence the reviewer asked for.}
\replaced[id=AB]{new wording}{old wording}
\deleted[id=BL]{This clause has to go.}
\listofchanges

Overleaf의 공동 저자, Word만 여는 공동 저자

모두에게 Git을 강요할 필요는 없습니다. Overleaf는 브라우저만으로 공동 편집할 수 있고 자체 기록과 변경 추적 화면을 갖추고 있어, LaTeX에 익숙하지 않은 공동 저자에게는 이쪽이 더 빠른 경우가 많습니다. 게다가 Overleaf 프로젝트는 Git 저장소로도 다룰 수 있어 git clone이 가능합니다. 이 다리를 놓아 두면 분업이 성립합니다. 공동 저자는 브라우저에서 쓰고, 여러분은 로컬에서 git pull 한 뒤 latexdiff-vc로 diff PDF를 만드는 식입니다. 주의할 점은 앞서 말한 .gitignore의 원칙이 그대로 적용된다는 것입니다. Overleaf 쪽에서 생성된 PDF와 로그가 소스와 함께 넘어오지 않도록 동기화 설정을 처음에 확인해 두십시오.

더 까다로운 쪽은 공동 저자가 Word만 쓰는 경우입니다. 여기서 현명한 수는 LaTeX 쪽을 정본으로 정해 버리는 것입니다. 넘길 때는 pandoc으로 .tex에서 .docx를 만들고, 변경 내용이 담겨 돌아온 .docx는 다시 pandoc에 넣습니다. pandoc--track-changesaccept(기본값), reject, all 세 가지로 Word 변경 내용의 처리를 정합니다. accept는 모든 삽입과 삭제를 반영하고, reject는 무시하며, all은 삽입・삭제・메모를 작성자와 시각까지 함께 남겨 특정 검토자의 수정만 골라 받아들일 수 있게 합니다. 이 옵션은 .docx를 읽을 때에만 작동합니다. 왕복할 때마다 서식은 다소 손실되지만, 정본이 LaTeX에 있는 한 잃는 것은 서식이지 원고가 아닙니다.

도구무엇을 보여 주는가언제 쓰는가
git diff소스의 줄 단위 차이기록, 브랜치, 분담, 자동 병합이 필요할 때
latexdiff단어 단위 차이를 조판한 PDF공동 저자나 심사자에게 지면 위에서 변경을 보여야 할 때
todonotes여백 메모와 모아 놓은 TODO 목록미완성인 곳을 스스로에게 계속 보이게 해야 할 때
changes저자별로 색을 나눈 편집과 메모여러 사람이 한 원고에 수정 표시를 넣을 때
pandoc.tex.docx 사이의 상호 변환공동 저자가 Word에서만 작업할 때

최종 PDF를 보내기 전의 확인

  • todonotesdisable로, changesfinal로 바꾼 뒤 PDF를 실제로 열어 TODO와 수정 표시가 남지 않았는지 확인합니다.
  • git grep -n "^<<<<<<< " -- "*.tex"로 남아 있는 충돌 표시를 찾습니다. LaTeX은 이에 대해 경고해 주지 않습니다.
  • latexmk -C로 생성물을 지운 뒤 깨끗한 상태에서 다시 빌드해, 문서가 낡은 중간 파일에 의존하지 않음을 확인합니다.
  • latexdiff가 만든 *-diff*.tex가 원고 디렉터리에 섞여 들어가지 않았는지 git status로 마지막에 한 번 봅니다.
  • 공동 저자에게 보낼 때는 “소스 묶음”, “diff PDF”, “최종 PDF”를 나누고 어느 것을 검토해 주기 바라는지 분명히 밝힙니다.