패키지와 클래스 작성

LaTeX 패키지 배포물을 열어 보면 .sty가 들어 있지 않은 경우가 있습니다. 대신 있는 것은 .dtx.ins 두 파일——코드와 해설이 같은 하나의 파일에 함께 사는, 문예적 프로그래밍의 구현입니다. 실제로 booktabs.dtx를 docstrip에 통과시키면 Lines processed: 1053 / Comments removed: 882 / Codelines passed: 161이라고 나옵니다. 1053행 가운데 실제 코드는 161행뿐이고, 나머지 85 %는 산문입니다. 같은 파일을 pdflatex에 걸면 그 산문이 조판된 PDF 매뉴얼이 됩니다. 이 페이지는 매번 프리앰블에 붙여 넣던 매크로를 자기 .sty로 묶는 일에서 시작해, .dtx를 쓰고 시험하고 CTAN에 내놓는 데까지를 다룹니다.

.sty의 골격 — 날짜를 적으면 버전 검사가 작동합니다

패키지는 프로젝트 안이나 설치된 어딘가에 있는 .sty 파일입니다. 첫 두 줄은 자기 신고입니다. \NeedsTeXFormat{LaTeX2e}가 필요한 포맷을, \ProvidesPackage{name}[date version description]가 패키지 이름과 판을 선언합니다. 이름은 반드시 파일의 basename과 일치해야 합니다. 대괄호 안은 생략할 수 있지만, 적어 두면 구체적인 이득이 있습니다. 사용자가 \usepackage{mypackage}[2027/01/01]처럼 최소한 필요한 날짜를 지정할 수 있게 되고, 손안의 판이 낡았다면 LaTeX Warning: You have requested, on input line 2, version '2027/01/01' of package mypackage, but only version '2026/08/17 v1.0 ...' is available.라는 경고가 나옵니다. 날짜는 YYYY/MM/DD 형식으로 적으세요. 이 한 줄이 몇 년 뒤 “왜인지 안 됩니다”라는 문의를 하나 줄여 줍니다.

본체에서는 다른 패키지에 대한 의존을 \RequirePackage{...}로 불러옵니다. .sty 안에서의 \usepackage에 해당합니다. 색을 쓰려면 xcolor, 그림을 그리려면 tikz를 이렇게 불러옵니다. 사용자에게 무언가를 알리고 싶을 때는 \PackageWarning{name}{message}를, 계속할 수 없는 상황이라면 \PackageError{name}{message}{help}를 씁니다. 둘 다 패키지 이름을 첫 인수로 받으므로, 로그를 본 사람이 어디서 온 메시지인지 한눈에 압니다. \usepackage[option]{name} 형태로 옵션을 받는 장치——\DeclareOption\ProcessOptions, 그리고 현재 커널이 갖춘 \DeclareKeys / \ProcessKeyOptions——는 클래스와 공통이라 클래스 페이지에 정리해 두었습니다. 패키지를 쓸 때도 절차는 그대로 같습니다. 오래된 .sty에서 만나게 되는 kvoptions 패키지는 같은 발상의 앞선 세대 도구입니다.

mypackage.sty
\NeedsTeXFormat{LaTeX2e}
\ProvidesPackage{mypackage}[2026/08/17 v1.0 My helpers]

\RequirePackage{xcolor}

\newcommand{\greet}[1][world]{Hello, \textcolor{blue}{#1}!}

\endinput

.dtx.ins를 직접 쓰기 — %<*package>의 의미

.dtx의 장치는 지극히 단순합니다. 행 첫머리의 %가 코드와 해설을 가른다는 것뿐입니다. %로 시작하는 줄은 해설, 나머지는 코드입니다. 그래서 같은 파일을 두 갈래로 읽을 수 있습니다. tex mypackage.ins를 돌리면 docstrip이 해설을 버리고 .sty를 써 내고, pdflatex mypackage.dtx를 돌리면 이번에는 해설이 본문으로 조판되고 코드가 번호와 함께 인용됩니다. 잘라 낼 범위는 %<*package>%</package>라는 가드로 둘러 표시하고, .ins 쪽의 \generate{\file{mypackage.sty}{\from{mypackage.dtx}{package}}}가 “package 가드 안의 내용을 mypackage.sty에 써 내라”고 지시합니다. 가드 이름은 자유이므로 하나의 .dtx에서 .sty.cls와 설정 파일을 한꺼번에 만들어 낼 수도 있습니다. \preamble\endpreamble 사이에 적은 문구는 생성되는 모든 파일 앞머리에 주석으로 들어갑니다. 라이선스 표시를 둘 자리입니다.

mypackage.dtx
% \iffalse meta-comment
% Copyright (C) 2026 Example Author
% This work may be distributed and/or modified under the conditions of
% the LaTeX Project Public License, version 1.3c or later.
% \fi
%
% \iffalse
%<*driver>
\documentclass{ltxdoc}
\usepackage{mypackage}
\EnableCrossrefs
\CodelineIndex
\begin{document}
\DocInput{mypackage.dtx}
\PrintIndex
\end{document}
%</driver>
%<package>\NeedsTeXFormat{LaTeX2e}
%<package>\ProvidesPackage{mypackage}
%<package>  [2026/08/17 v1.0 A demonstration package]
% \fi
%
% \title{The \textsf{mypackage} package}
% \author{Example Author}
% \maketitle
%
% \section{Usage}
% \DescribeMacro{\greet}
% |\greet| prints a greeting; the optional argument sets the name.
%
% \StopEventually{}
%
% \section{Implementation}
%    \begin{macrocode}
%<*package>
\RequirePackage{xcolor}
\newcommand{\greet}[1][world]{Hello, \textcolor{blue}{#1}!}
%</package>
%    \end{macrocode}
% \Finale
\endinput

해설 쪽에서 쓰는 명령은 몇 개 되지 않습니다. \DocInput{파일}.dtx 자신을 다시 읽어 들이는 주역이고, %<*driver>%</driver>로 둘러싼 부분이 그것을 위한 작은 문서 설정입니다(ltxdoc을 씁니다). 구현은 \begin{macrocode}\end{macrocode}로 감싸 인용하며, 울타리 줄의 앞을 네 칸 들여쓰는 것이 관례입니다. \DescribeMacro{\명령}은 사용자용 설명에서 명령을 도드라지게 하고 색인에도 등록합니다. \StopEventually{}는 “여기서부터 구현”이라는 경계로, 사용자용의 짧은 판을 만들 때 효력을 발휘합니다. 마지막의 \Finale이 색인과 변경 이력을 마무리합니다. 위의 .dtx와 여섯 줄짜리 .ins를 실제로 돌리면 Lines processed: 41 / Comments removed: 24 / Codelines passed: 10이 나오고, 생성된 .sty의 첫머리에는 %% This is file 'mypackage.sty', generated with the docstrip utility.라는 자동 생성 안내가 들어갑니다.

mypackage.ins
\input docstrip.tex
\keepsilent
\preamble
Generated from mypackage.dtx -- do not edit this file directly.
\endpreamble
\generate{\file{mypackage.sty}{\from{mypackage.dtx}{package}}}
\endbatchfile

.dtx는 저절로 낡습니다 — booktabs가 조판되지 않는 건

.dtx를 쓰기 전에 알아 둘 함정이 있습니다. .sty는 계속 동작하는데 .dtx 쪽이 조판되지 않게 되는 일이 있습니다. TeX Live 2024에서 pdflatex booktabs.dtx를 실행하면 오류 127개를 내고 PDF가 전혀 생성되지 않습니다. 첫 오류는 ! Improper alphabetic constant이며 \CharacterTable 부근에서 멈춥니다. 정작 booktabs.sty 자체는 완벽하게 동작하는데도 말입니다. 원인은 문서 쪽 바탕이 바뀐 데 있습니다. TeX Live 2024의 doc.styv3.0m(2022/11/13), 프랑크 미텔바흐가 새로 쓴 “V3”인데, booktabs.dtx는 2020년 1월 그대로입니다. 배포물에 함께 담긴 booktabs.pdf도 2020년 1월 날짜이며 그 뒤로 한 번도 재생성되지 않았습니다. 대조적으로 같은 TeX Live 2024에서 multirow.dtx(30쪽), tabularx.dtx(12쪽), array.dtx(35쪽)는 모두 경고 없이 조판되므로, 이는 구조의 결함이 아니라 관리의 문제입니다. 자기 .dtx는 릴리스할 때마다 실제로 조판해 볼 것.

백지에서 시작할 필요는 없습니다. TeX Live에는 dtxtut라는 입문서가 함께 들어 있고, 거기에 skeleton.dtxskeleton.ins라는 본보기가 딸려 있습니다. 많은 패키지가 이것을 베껴 출발한 흔적이 뜻밖의 곳에 남아 있습니다. skeleton.ins의 18행에는 \usedir{tex/latex/skeleton}이라는 줄이 있고, 똑같은 줄이 booktabs.ins의 36행에도 그대로 남아 있습니다. 본보기의 skeleton을 자기 패키지 이름으로 고치는 것을 잊은 것입니다. TeX Live 2024의 source/latex에 있는 1402개의 .ins를 뒤져 보면 이 잔재를 안고 있는 것은 booktabs 하나뿐입니다. 흐뭇한 이야기지만 교훈은 분명합니다. 본보기에서 시작했다면, 내보내기 전에 본보기의 이름이 남아 있지 않은지 검색할 것.

expl3로 쓴다면 \ProvidesExplPackage

내용을 expl3로 쓸 생각이라면 선언 줄을 \ProvidesExplPackage{name}{date}{version}{description}로 바꿉니다. 인수가 넷으로 나뉘는 것 외에 또 하나 중요한 성질이 있습니다. 이 명령은 마지막에 \ExplSyntaxOn을 실행합니다. 곧 선언한 다음 줄부터는 \ExplSyntaxOn을 한 번도 쓰지 않고 expl3 문법을 쓸 수 있습니다. 아래 .sty에는 \ExplSyntaxOn이 한 군데도 없지만 \tl_new:N\NewDocumentCommand도 그대로 동작합니다. expl3 자체를 읽는 법은 expl3 페이지에 있습니다.

expldemo.sty
\NeedsTeXFormat{LaTeX2e}
% four arguments, and it turns on expl3 syntax by itself
\ProvidesExplPackage{expldemo}{2026/08/17}{1.0}{Expl demo}

\tl_new:N \l_expldemo_tl
\tl_set:Nn \l_expldemo_tl { from~expl3 }

\NewDocumentCommand \shout { } { \tl_use:N \l_expldemo_tl }

시험과 배포물 만들기 — l3build

tex mypackage.inspdflatex mypackage.dtx를 매번 손으로 치는 대신, LaTeX Project가 관리하는 l3build에 맡길 수 있습니다(TeX Live 2024에 담긴 것은 2024-02-08 릴리스). 프로젝트 뿌리에 build.lua를 두고 모듈 이름과 파일 목록을 적으면, l3build unpack.ins를 돌려 .stybuild/unpacked/에 만들고, l3build doc.dtx를 조판해 build/doc/에 PDF를 만듭니다. 생성물이 작업 디렉터리를 어지럽히지 않는 것이 장점입니다. l3build check는 시험 장치로, testfiles/에 둔 .lvt(시험 문서)와 .tlg(기대되는 로그)를 맞춰 보고 출력이 바뀌면 차이를 보여 줍니다. 조판 결과를 눈이 아니라 기계로 지킬 수 있게 되면 리팩터링이 두렵지 않게 됩니다.

build.lua
module       = "mypackage"
sourcefiles  = {"mypackage.dtx", "mypackage.ins"}
installfiles = {"mypackage.sty"}
uploadconfig = { pkg = "mypackage" }

-- l3build unpack   -> build/unpacked/mypackage.sty
-- l3build doc      -> build/doc/mypackage.pdf
-- l3build check    -> run testfiles/*.lvt against *.tlg
-- l3build ctan     -> mypackage-ctan.zip, ready to upload

CTAN에 내놓기 — zip의 내용물과 라이선스

l3build ctan을 실행하면 그대로 업로드할 수 있는 zip이 하나 만들어집니다. 안에는 패키지 이름을 딴 mypackage/ 디렉터리가 있고, 그 안에 소스(.dtx.ins)와 조판된 PDF 매뉴얼이 담깁니다. .sty를 넣지 않는 것이 이 바닥의 관례이며, 받는 쪽이 .ins에서 생성합니다. 여기에 README와 변경 이력을 더하면 체재가 갖춰집니다. 다만 l3build ctan은 작업 디렉터리에 있는 PDF를 주워 모으므로, 시험 삼아 뽑아 둔 PDF가 굴러다니면 함께 포장됩니다. 만든 뒤 unzip -l로 내용을 훑어 확인하세요. 업로드 자체는 CTAN의 웹 양식으로 하지만, TeX Live에는 ctan-o-mat도 함께 들어 있어 설명・연락처・라이선스를 적은 설정 파일로 검증과 업로드를 명령줄에서 할 수 있습니다.

라이선스는 정해 두어야 합니다. CTAN은 배포 조건을 명시할 것을 요구하고, TeX Live의 패키지 정보에도 표시됩니다. 이 바닥의 사실상 표준은 LPPL(LaTeX Project Public License)이며, 고르는 법과 판별 차이——특히 “고쳤으면 이름을 바꾸라”는 조항이 어느 판 어디에 있는지——는 라이선스 페이지에 자세히 적혀 있습니다. 정한 조건은 세 곳에 적으세요. .dtx 첫머리의 메타 주석, .ins\preamble(생성되는 모든 파일 앞머리에 들어가므로 .sty만 받은 사람에게도 닿습니다), 그리고 README입니다. 여기까지 갖추면 여러분의 패키지는 몇 해 뒤 누군가가 texdoc으로 열 수 있는 물건이 됩니다.

  • \ProvidesPackage의 날짜를 반드시 YYYY/MM/DD로 적습니다. 이것이 없으면 사용자가 판을 요구할 수 없습니다.
  • .dtx는 매번 조판해 봅니다. .sty는 되는데 .dtx가 떨어지는 일은 실제로 일어납니다(booktabs).
  • 본보기에서 시작했다면 본보기의 이름을 검색합니다. skeleton 잔재는 TeX Live 2024에도 한 건 살아 있습니다.
  • l3build ctan이 만든 zip은 unzip -l로 내용을 확인합니다. 작업 디렉터리의 PDF를 주워 담습니다.
  • 라이선스를 .dtx, .ins\preamble, README 세 곳에 적습니다. .sty만 받은 사람에게도 닿아야 합니다.