머릿속에 책 한 권 분량의 주제가 있다. 몇 년째 다뤄 온 기술이든, 반복해서 설명해 온 노하우든, 강의로 여러 번 풀어 본 내용이든. 그런데 그것이 책이 되지 못하는 이유는 대개 두 곳에서 막히기 때문이다.
앞에서는 원고가 안 나온다. 목차까지는 어떻게 잡는데, 12장짜리 초고를 끝까지 쓰는 데 몇 달이 걸리고 대개 4장쯤에서 멈춘다. 뒤에서는 파일이 안 나온다. 원고를 다 썼다 해도 그 마크다운 더미를 팔 수 있는 EPUB·PDF로 바꾸는 구간이 남아 있고, 여기서 그림과 판형과 한글 조판이 차례로 무너진다.
이 책은 그 양쪽을 모두 자동화한다. 주제 한 줄에서 시작해 AI와 함께 목차를 잡고 초고를 쓰고, 그 원고 폴더를 입력으로 받아 EPUB과 PDF를 동시에 뽑아내는 로컬 파이프라인을 실제로 동작하는 코드와 함께 처음부터 끝까지 만든다. 지금 읽고 있는 이 책 자체가 그 파이프라인으로 만들어졌다.
직접 해 보면 순서대로 무너진다.
첫째, 그림이다. 기술서에는 구조도와 흐름도가 들어간다. 이걸 그림판이나 Figma로 그려서 PNG로 붙이면, 원고를 고칠 때마다 그림을 다시 열어야 한다. 3장의 다이어그램에서 상자 하나의 이름을 바꾸는 데 도구를 켜고 편집하고 내보내고 파일을 갈아끼우는 네 단계가 든다. 개정판을 낼 때쯤이면 원본 파일이 어디 갔는지도 모른다.
둘째, 두 판형이다. EPUB은 글자 크기가 독자 손에서 바뀌는 리플로우 판형이고, PDF는 페이지가 고정된 판형이다. 요구가 정반대다. EPUB에서 폭 100%로 예쁘게 걸린 다이어그램이 PDF에서는 페이지를 넘겨 버리고, PDF에서 벡터로 선명한 그림이 구형 킨들에서는 아예 안 뜬다. 두 판형을 각각 손으로 맞추면 작업량이 두 배가 아니라, 고칠 때마다 두 배가 된다.
셋째, 한글이다. 한글 전자책은 영문 전자책과 실패하는 지점이 다르다. 폰트를 임베드하지 않으면 리더마다 다른 글꼴로 뜬다. 코드 블록의 긴 줄은 종이에서 잘린 채 인쇄된다 — 가로 스크롤이 없는 매체에서는 복구 불가능한 결함이다. 표는 폭이 제각각으로 놓이고, LaTeX은 한글 조판을 위해 별도의 설정을 요구한다.
세 가지 모두 “한 번 잘 맞춰 놓으면 되는” 문제처럼 보인다. 실제로는 원고를 고칠 때마다 다시 무너지는 문제다. 그래서 답은 조판을 잘하는 것이 아니라, 조판을 코드로 만드는 것이다.
이 파이프라인의 설계 원칙은 하나다.
그림을 이미지로 생성하지 않고, 텍스트 코드로 다룬다.
원고 안에 Mermaid·D2·Vega-Lite 코드를 그대로 써 두고, 빌드 시점에 렌더러가 이미지로 바꾼다. 이 결정 하나에서 나머지가 따라온다.
git diff에 텍스트로 남는다.여기에 한 겹을 더 얹는다. 다이어그램 코드를 직접 쓰지 않아도 되게 하는 것이다. 자연어로 “오케스트레이터가 렌더러에 코드를 보내고, 실패하면 에러를 LLM에 넘겨 고친다”라고 쓰면 LLM이 Mermaid 코드를 만들어 준다. 그리고 그 코드가 문법 오류로 렌더에 실패하면, 파서 에러를 다시 LLM에 넣어 스스로 고치게 한다. 이 자기교정 루프가 이 파이프라인에서 가장 재미있는 부분이고, 10장에서 통째로 다룬다.
전체 흐름은 이렇게 생겼다. 왼쪽 절반이 AI와 함께 원고를 만드는 구간이고, 오른쪽 절반이 그 원고를 파일로 굽는 구간이다.
오른쪽 절반은 명령 하나로 돈다.
uv run python -m pipeline.run --chapters manuscripts/my-book --title "책 제목"산출물은 build/ 아래에 떨어진다. EPUB은 폰트가
임베드되고 표지가 붙은 완성본이고, PDF는 속표지·장 오프너·러닝
헤더·목차가 들어간 조판본이다. 둘 다 손댈 곳이 없는 상태로 나온다.
이 파이프라인은 전부 로컬에서 돈다. Docker도, 워크플로 엔진도, SaaS 조판 서비스도 쓰지 않는다. 파이썬 오케스트레이터가 로컬 CLI들을 부르는 구조다.
| 역할 | 도구 | 왜 이걸 골랐나 |
|---|---|---|
| 조판 | pandoc |
마크다운에서 EPUB·LaTeX·HTML로 가는 가장 짧은 경로 |
| PDF 엔진 | xelatex |
한글 조판에 필요한 xeCJK를 쓸 수 있는 사실상 유일한
선택 |
| 다이어그램 | mmdc·d2 |
텍스트 문법이 안정적이고 CLI로 렌더된다 |
| 차트 | vl-convert |
브라우저 없이 Vega-Lite를 벡터로 렌더한다 |
| SVG 변환 | rsvg-convert |
D2의 SVG를 PNG·PDF로 바꾼다 |
| 검증 | epubcheck |
EPUB 표준 위반을 잡는다 |
로컬을 고집하는 이유는 취향이 아니다. 조판은 되돌아가는 작업이다. 표 하나를 고치고 다시 빌드하는 일을 하루에 수십 번 한다. 원격 빌드로는 이 반복 속도가 나오지 않는다. 여기서 만든 흐름을 나중에 CI나 워크플로 엔진으로 옮기는 건 어렵지 않지만, 순서는 반드시 로컬이 먼저다.
IT를 어느 정도 아는 사람이면 충분하다. 구체적으로는 이렇다.
| 내용 | |
|---|---|
| 필요한 것 | 터미널에서 명령을 실행할 수 있다. 마크다운으로 글을 써 봤다. |
| 있으면 좋은 것 | 파이썬 코드를 읽을 수 있다 (쓸 필요는 없다). |
| 필요 없는 것 | LaTeX, EPUB 내부 구조, 조판 용어, 디자인 경험 |
본문의 코드는 전부 그대로 복사해 쓸 수 있는 완성된 형태다. 원리가 궁금하지 않다면 붙여 넣고 넘어가도 책은 나온다. 새 용어는 처음 나올 때 한 문장으로 설명하고 지나간다.
3장까지 읽으면 이미 전자책 파일이 손에 있다. 코드 없이 도구만으로 만드는 투박한 버전이고, 거기서부터 하나씩 제대로 만든다.
| 부 | 장 | 무엇을 얻나 |
|---|---|---|
| 준비 | 1~3장 | 도구 설치, 그리고 첫 EPUB·PDF |
| 원고 | 4~6장 | AI로 목차·초고를 만들고 규약에 맞춰 정리한다 |
| 그림 | 7~12장 | 다이어그램 자동 생성·자기교정, 이미지 조달 판단 |
| 조판 | 13~15장 | 디자인 토큰, EPUB·PDF 조판과 한글 함정 |
| 마무리 | 16~19장 | 여러 권 배치, 미리보기·목차, 검증 절차 |
읽는 방식은 둘 중 하나를 고르면 된다.
코드는 파이썬 3.13 기준이고 패키지 관리는 uv를 쓴다.
설명은 macOS 환경을 전제하지만, 도구 설치 명령만 바꾸면 리눅스에서도
그대로 동작한다.
자동화는 검수를 대체하지 않는다. 18장에서 만드는 검증 절차 — EPUB 표준 검사, PDF 페이지 이미지 확인, 리더 렌더 재현 — 는 선택 사항이 아니다. 파이프라인이 조용히 잘못된 결과를 낼 수 있는 지점들이 있고, 그게 어디인지는 각 장에서 표시해 둔다.
준비가 됐으면 도구부터 깔자.
파이프라인은 파이썬 코드가 로컬 CLI들을 부르는 구조다. 그래서 첫 작업은 그 CLI들을 깔고, 깔렸는지 기계가 확인해 주는 장치를 만드는 것이다. 두 번째가 더 중요하다. 이 파이프라인의 실패 대부분은 코드 버그가 아니라 “그 도구가 이 머신에 없다”이기 때문이다.
| 도구 | 용도 | 설치 |
|---|---|---|
uv |
파이썬 환경·실행 | brew install uv |
pandoc |
EPUB·PDF·HTML 조판 | brew install pandoc |
xelatex |
PDF 엔진 (한글) | brew install --cask mactex-no-gui |
mmdc |
Mermaid 렌더 | npm i -g @mermaid-js/mermaid-cli |
d2 |
D2 렌더 | brew install d2 |
rsvg-convert |
SVG → PNG·PDF | brew install librsvg |
epubcheck |
EPUB 표준 검증 | brew install epubcheck |
각 도구가 파이프라인의 어느 자리에 붙는지 보면 목록이 덜 임의적으로 보인다.
차트 렌더러(vl-convert-python)는 파이썬 패키지라
uv sync로 따라 들어온다. 브라우저가 필요 없는 Vega-Lite
구현이라는 점이 중요하다 — 다른 선택지들은 헤드리스 크롬을 요구하고,
그러면 빌드가 느려지고 CI로 옮기기도 어려워진다.
MacTeX 전체는 5GB가 넘는다. mactex-no-gui는 GUI 앱을 뺀
배포판이고, 우리가 쓰는 건 xelatex 바이너리와 한글
패키지뿐이다. TeX Live를 쓴다면 최소 설치 후 필요한 패키지만
tlmgr로 받아도 된다.
pdflatex가 아니라
xelatex여야 한다. 한글 조판에 필요한
xeCJK, 그리고 뒤에서 표지에 쓸 fontspec의 자간
조절 기능이 xelatex 전용이다. 엔진을 바꾸면 조판이 통째로 깨진다.
본문 폰트는 Pretendard로 간다. 한글 기술서에서 화면과 종이 양쪽에 무리 없이 걸리는 무료 폰트이고, 웨이트가 충분히 나뉘어 있어 제목과 본문의 위계를 만들 수 있다.
brew install --cask font-pretendard여기서 이 파이프라인 전체를 관통하는 제약이 하나 생긴다.
폰트를 정하는 곳은 단 하나여야 한다. 폰트 이름은 EPUB의
CSS, 표지 SVG, PDF의 mainfont와 CJKmainfont,
Mermaid 테마, Vega-Lite 설정까지 여섯 군데로 퍼진다. 이걸 각자
하드코딩하면 어느 하나를 바꿨을 때 나머지 다섯이 조용히 어긋난다.
13장에서 만드는 디자인 토큰이 이 문제의 답이다.
D2에는 별도의 함정이 있다. D2의 --font-regular 옵션은
깨끗한 TTF만 받는다. OTF도 TTC도 거부하고, 거부할 때
나오는 메시지는 expected .ttf뿐이라 원인을 알기 어렵다.
문제는 Homebrew의 font-pretendard cask가
~/Library/Fonts에는 OTF만 심는다는 점이다.
그대로 넘기면 실패한다. TTF는 Caskroom 안에 남아 있으므로 그걸
프로젝트로 복사해 오는 스크립트를 만든다.
#!/usr/bin/env bash
# D2 한글용 TTF 준비. D2는 '깨끗한 TTF'만 받는다 (OTF/TTC 거부).
set -euo pipefail
cd "$(dirname "$0")/.."
PRETENDARD_VER="v1.3.9"
CDN="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@${PRETENDARD_VER}/packages/pretendard/dist/public/static/alternative"
mkdir -p fonts
fetch_weight() {
local weight="$1" dest="$2"
[ -f "$dest" ] && { echo "이미 있음: $dest"; return 0; }
# Caskroom 은 버전 디렉터리가 바뀌므로 glob 으로 가장 최근 것을 고른다.
local cask
cask=$(find /opt/homebrew/Caskroom/font-pretendard /usr/local/Caskroom/font-pretendard \
-path "*/public/static/alternative/Pretendard-${weight}.ttf" 2>/dev/null \
| sort | tail -1 || true)
if [ -n "$cask" ]; then
cp "$cask" "$dest"; echo "Caskroom 에서 복사: $dest"; return 0
fi
curl -fSL -o "$dest" "${CDN}/Pretendard-${weight}.ttf"
}
fetch_weight Regular fonts/Pretendard.ttf
fetch_weight Bold fonts/Pretendard-Bold.ttf조달 순서가 세 단계인 게 핵심이다. 이미 있으면 건너뛰고, Caskroom에 있으면 복사하고(오프라인에서도 된다), 없으면 CDN에서 개별 파일만 받는다. 배포 zip은 55MB라 쓰지 않는다.
find는 존재하지 않는 경로를 만나면 종료 코드 1을 낸다.
set -e 아래에서는 이것만으로 스크립트가 죽으므로
|| true가 필요하다. Intel 맥과 Apple Silicon의 Homebrew
접두사가 달라 두 경로를 모두 뒤지는 것도 같은 이유다.
도구가 하나라도 빠지면 파이프라인은 빌드 중간에 죽는다. 그것도 pandoc 스택 트레이스나 puppeteer 에러 같은, 원인과 거리가 먼 메시지로. 그래서 빌드 전에 한 번에 확인하는 스크립트를 만든다.
#!/usr/bin/env bash
# 로컬 도구 점검 — 파이프라인이 돌 수 있는지 확인
set -u
ok=0; miss=0
check() {
printf " %-12s" "$1"
if command -v "$1" >/dev/null 2>&1; then
echo "OK ($($1 --version 2>&1 | head -1))"; ok=$((ok+1))
else
echo "MISSING — $2"; miss=$((miss+1))
fi
}
echo "[필수]"
check uv "brew install uv"
check mmdc "npm i -g @mermaid-js/mermaid-cli"
check pandoc "brew install pandoc"
check xelatex "brew install --cask mactex-no-gui"
echo "[선택 · D2 엔진]"
check d2 "brew install d2"
check rsvg-convert "brew install librsvg"
printf " %-12s" "d2 한글 TTF"
[ -f fonts/Pretendard.ttf ] && echo "OK" || echo "MISSING (bash scripts/prepare_d2_font.sh)"
echo "[LaTeX 한글 패키지]"
for sty in kotex xeCJK; do
printf " %-12s" "$sty"
kpsewhich "$sty.sty" >/dev/null 2>&1 && echo "OK" || echo "MISSING (tlmgr install $sty)"
done
echo "[한글 폰트]"
for name in "Pretendard"; do
printf " %-20s" "$name"
fc-list 2>/dev/null | grep -qi "$name" && echo "OK" || echo "MISSING"
done
echo "---"; echo "필수 OK=$ok MISSING=$miss"세 층으로 나눈 게 의도다.
command -v로 본다. 있으면
버전까지 찍어 준다 — 버전 차이로 생기는 문제가 실제로 있다(11장의 D2
사례)..sty
파일이라 kpsewhich로 찾아야 한다. xelatex이 깔려 있어도
kotex이 없으면 한글 PDF는 안 나온다.~/Library/Fonts에 파일이
있어도 fontconfig 캐시에 안 잡혔으면 조판에서 못 쓴다.점검 스크립트에 set -e를 넣지 않는다. 이 스크립트의
목적은 빠진 것을 전부 모아서 한 번에 보여 주는 것이다.
첫 번째 누락에서 죽으면 설치를 한 번에 하나씩 하게 되고, 그게 가장
짜증나는 실패 방식이다. 같은 원칙이 뒤에서 원고 검증에도 그대로
적용된다.
pipeline/ 오케스트레이터 파이썬 패키지
filters/ pandoc lua 필터
book/
metadata.yaml 제목·저자·언어
chapters/*.md 원고
manuscripts/ 다권 배치용 원고 루트
assets/diagrams/ 렌더된 다이어그램 캐시
build/ 산출물
fonts/ D2용 TTF
scripts/ doctor.sh · prepare_d2_font.sh
tokens.yaml 디자인 토큰
파이썬 환경은 uv로 만든다.
uv init
uv add pyyaml openai vl-convert-python
uv add --dev pytest의존성이 이게 전부다. 조판도 렌더도 전부 외부 CLI가 하기 때문에, 파이썬 쪽은 YAML을 읽고 LLM을 부르고 서브프로세스를 돌리는 얇은 층이다. 이 얇음이 의도된 설계다 — 무거운 일은 각 분야에서 이미 검증된 도구에 맡기고, 우리 코드는 그것들을 어떤 순서로 어떤 인자로 부를지만 안다.
LLM 코드 생성(9장)에 OpenAI 키가 필요하다. 저장소 루트의
.env에 둔다.
OPENAI_API_KEY=sk-...키가 없어도 파이프라인은 돈다 — 자연어 스펙 블록만 건너뛰고 경고 노트로 대체한다. 이건 의도된 동작이다. 키 하나 때문에 원고 전체가 빌드되지 않으면 파이프라인으로서 쓸모가 없다. 이 “한 부분이 실패해도 전체는 계속된다”는 원칙은 뒤에서 여러 번 다시 나온다.
bash scripts/doctor.sh전부 OK가 뜨면 준비가 끝났다. 하나라도 MISSING이면 옆에 적힌 설치 명령을 실행하고 다시 돌린다. 이 스크립트를 통과하지 못한 상태에서 빌드를 시도하면, 원인이 도구 누락인 실패를 코드 버그로 착각하며 시간을 쓰게 된다.
다음 장부터 실제 코드를 쓴다.
이 문서는 미리보기(샘플)입니다. 전체 내용은 길리랩에서 구매할 수 있습니다.