\documentclass[unknownoption]{article} 라고 써도 컴파일은 통과합니다. 클래스 옵션은 철자를 틀려도 오류가 되지 않고, LaTeX은 로그 깊숙한 곳에 LaTeX Warning: Unused global option(s): 를 남길 뿐입니다. PDF가 나와 버리니 좀처럼 알아차리지 못합니다. 그런데 \usepackage[unknownoption]{color} 쪽은 곧바로 멈춥니다. 이 비대칭은 사양의 변덕이 아니라 인식되지 않은 옵션이 클래스와 패키지에서 기본적으로 향하는 곳이 다르다는 의도된 설계에서 나옵니다. 이 페이지에서는 그 구조를 입구 삼아 \DeclareOption 과 \ProcessOptions, 새로운 \DeclareKeys, 그리고 \LoadClass 로 기존 클래스 위에 자기 클래스를 세우는 데까지 쌓아 올립니다.
왜 철자를 틀린 클래스 옵션은 멈추지 않는가
답은 공식 안내서 clsguide에 적혀 있습니다. 클래스 파일에 \DeclareOption* 가 없으면, 선언되지 않은 옵션은 조용히 모든 패키지로 넘어갑니다. 패키지 파일에 \DeclareOption* 가 없으면, 선언되지 않은 옵션마다 오류가 납니다. 즉 클래스 옵션은 “아직 누군가 쓸지도 모른다”는 전제로 돌려지고, \begin{document} 까지 아무도 가져가지 않았을 때 비로소 LaTeX이 LaTeX Warning: Unused global option(s): 라고 보고하며 남은 이름을 대괄호로 늘어놓습니다. 반면 패키지 옵션은 갈 곳이 그곳뿐이므로, 모르는 이름은 곧바로 ! LaTeX Error: Unknown option 'unknownoption' for package 'color'. 가 됩니다.
이 설계는 이치에 맞습니다. 클래스 자신은 모르지만 나중에 불러올 패키지가 받아 가는 옵션(전역 옵션), 예컨대 \documentclass[dvipsnames]{article} 같은 것이 일상적으로 쓰이기 때문입니다. 대가는 오타가 침묵한다는 점입니다. 그래서 실무에서는 두 가지 습관이 이득입니다. 하나는 빌드할 때마다 로그에서 Unused global option 을 검색하는 것. 다른 하나는 프리앰블 맨 앞에 \listfiles 를 두어 로그 끝에 읽힌 파일과 버전 목록이 나오게 하는 것입니다. 참고로 옵션 코드 안에서 \OptionNotUsed 를 부르면 그 옵션을 일부러 같은 “미사용” 목록으로 보낼 수 있습니다.
클래스(.cls)와 패키지(.sty)의 차이
clsguide가 제시하는 판단 기준은 한 줄입니다. 그 명령들을 어떤 문서 클래스와도 함께 쓸 수 있다면 패키지로 만들고, 그렇지 않다면 클래스로 만든다. 클래스는 문서 종류 자체를 정의하며 \documentclass 로 하나만 불러옵니다. 패키지는 \usepackage 로 몇 개든 겹칠 수 있고, 문서 종류와 무관하게 기능을 더합니다. 안내서가 드는 예가 알기 쉽습니다. 어떤 회사가 자사 편지지에 편지를 조판하려고 만드는 ownlet 은 letter 클래스 위에 세워지지만 다른 클래스에서는 쓸 수 없으므로 ownlet.cls 이고, 그림을 넣는 graphics 는 어느 클래스에서나 쓸 수 있으므로 graphics.sty 입니다.
클래스에도 두 종류가 있습니다. article, report, letter 처럼 자립한 것과, 기존 클래스의 확장·변종인 것입니다. clsguide는 후자의 예로 article 위에 세워진 proc 을 듭니다. 직접 쓰는 클래스는 거의 확실히 후자입니다. 판면을 처음부터 짜는 일은 수지가 맞지 않기 때문입니다. .cls 와 .sty 의 작성 방식은 거의 같고, 명령이 Class 용과 Package 용 쌍으로 마련되어 있을 뿐입니다(\ProvidesClass ↔ \ProvidesPackage, \LoadClass ↔ \RequirePackage, \PassOptionsToClass ↔ \PassOptionsToPackage).
자작 클래스가 받아야 할 표준 옵션
사용자는 표준 클래스에 하듯 자작 클래스에도 옵션을 넘깁니다. 최소한 갖추고 싶은 얼굴들은 본문 기준 크기를 고르는 10pt / 11pt / 12pt, 용지의 a4paper / letterpaper, 단 구성의 onecolumn / twocolumn, 면 구성의 oneside / twoside, 그리고 넘친 줄을 검은 표시로 보여 주는 draft(반대는 final)입니다. 다만 이것들을 직접 구현할 필요는 없습니다. 뒤에서 보듯 기반 클래스로 그대로 전달하는 것이 정석입니다.
| 옵션 | 의미 | 기본값 |
|---|---|---|
10pt / 11pt / 12pt | 본문 기준 글자 크기 | 10pt |
a4paper / letterpaper | 용지 크기(b5paper, legalpaper 등도) | letterpaper |
onecolumn / twocolumn | 1단 / 2단 | onecolumn |
oneside / twoside | 단면 / 양면 레이아웃 | oneside(book 만 twoside) |
draft / final | 넘친 줄에 검은 표시를 붙일지 여부 | final |
사용자가 아무것도 지정하지 않았을 때의 기본값을 클래스 쪽에서 정하고 싶다면 \ProcessOptions 앞에 \ExecuteOptions{a4paper,11pt} 를 씁니다. “이 옵션들의 코드를 미리 실행해 두라”는 선언이며, clsguide도 클래스에 기본 설계를 주는 방법으로 바로 이 형태를 듭니다. 이 옵션들을 문서의 \documentclass[...] 쪽에서 사용하는 이야기와 book 특유의 openright 같은 것은 문서 클래스와 프리앰블 페이지의 담당입니다. 여기서부터는 받는 쪽의 클래스를 어떻게 쓸지에 집중합니다.
파일 맨 앞에서 자신을 밝히기 — \NeedsTeXFormat 과 \ProvidesClass
클래스 파일(myclass.cls)의 처음 두 줄은 거의 정형입니다. 먼저 \NeedsTeXFormat{LaTeX2e} 로 이 파일이 LaTeX2e용임을 선언합니다. 이어 \ProvidesClass{myclass}[2026/01/01 v1.0 My example class] 로 클래스 이름, 릴리스 날짜, 버전, 설명을 밝힙니다. 이 한 줄이 값을 하는 곳은 로그입니다. 컴파일하면 Document Class: myclass 2026/01/01 v1.0 My example class 라는 줄이 남습니다. 공저자가 “조판이 안 된다”고 할 때 이 한 줄만 받아 보면 상대가 낡은 .cls 를 쥐고 있는지 바로 알 수 있습니다.
대괄호 안은 생략할 수 있지만, 써 두면 사용자가 날짜(YYYY/MM/DD 형식) 를 근거로 \documentclass{myclass}[2026/01/01] 처럼 최소 버전을 요구할 수 있습니다. 패키지를 쓴다면 대응 명령은 \ProvidesPackage{mypackage}[2026/01/01 v1.0 ...] 이고, \NeedsTeXFormat 은 둘 다 공통입니다. 철칙으로, \ProvidesClass 의 이름은 실제 파일명과 일치시킵니다. myclass.cls 안에는 반드시 \ProvidesClass{myclass} 라고 쓰십시오.
\NeedsTeXFormat{LaTeX2e}
\ProvidesClass{myclass}[2026/01/01 v1.0 My example class]옵션을 선언한다 — \DeclareOption 과 \CurrentOption
클래스가 받을 수 있는 옵션은 \DeclareOption{option}{code} 로 하나씩 선언합니다. 사용자가 그 옵션을 지정하면 뒤에서 설명할 \ProcessOptions 에 도달한 시점에 code 가 실행됩니다. 내용은 어떤 LaTeX 구성이어도 되지만, 실제로 가장 흔한 것은 \newif 로 만든 참거짓 플래그를 세우는 한 줄입니다. 무거운 처리는 플래그를 보고 나중에 하는 편이 순서 사고를 막아 줍니다.
선언하지 않은 옵션을 받는 곳은 별표 붙은 \DeclareOption*{code} 이고, 그 안에서 \CurrentOption 은 “지금 처리하려는 옵션 이름”으로 펼쳐집니다. 자작 클래스에서 가장 자주 쓰이는 한 줄이 이것으로 알 수 없는 옵션을 기반 클래스에 그대로 전달하는 것입니다. 이 한 줄 덕분에 10pt 나 a4paper 를 직접 선언하지 않아도 사용자가 자연스럽게 넘길 수 있습니다. 앞에서 본 “클래스는 조용히 넘긴다”는 기본값을 원하는 목적지로 돌려놓은 셈입니다.
% pass anything we do not handle ourselves on to article
\DeclareOption*{\PassOptionsToClass{\CurrentOption}{article}}옵션을 처리하고 기반 클래스를 불러온다 — \ProcessOptions 와 \LoadClass
선언만으로는 아무 일도 일어나지 않습니다. \ProcessOptions 를 호출해야 비로소 선택된 옵션의 코드가 실행됩니다. 실무에서는 거의 항상 \ProcessOptions\relax 라고 씁니다. 별표 붙은 \ProcessOptions* 라는 다른 형태가 있기 때문에, 끝의 \relax 로 “별표 없는 쪽”을 확실히 고르고 불필요한 미리 읽기와 혼란스러운 오류를 막는 것입니다. clsguide도 이를 명시적으로 권합니다. 별표 없는 쪽은 선언한 순서 로, 별표 붙은 쪽은 호출자가 늘어놓은 순서 로 옵션을 처리합니다.
판면을 처음부터 짜는 일은 수지가 맞지 않으므로, 대부분의 자작 클래스는 기존 클래스를 토대로 삼습니다. 그것이 \LoadClass[options]{article} 이며 article.cls 의 명령과 체재를 통째로 읽어 옵니다. 이 명령은 클래스 파일 안에서만, 그리고 한 클래스 파일에서 한 번만 쓸 수 있습니다. 순서도 중요합니다. 사용자가 \documentclass[...] 로 넘긴 옵션이 기반 클래스에 닿게 하려면 \LoadClass 를 옵션 처리(\ProcessOptions) 뒤에 두어야 합니다. 전달 설정을 쓰고 → \ProcessOptions 로 나누고 → 기반을 불러오는 순서입니다. 받은 옵션을 그대로 넘기고 싶을 뿐이라면 \LoadClassWithOptions{article} 가 편하고, 패키지를 쓸 때는 \LoadClass 대신 \RequirePackage, 그대로 넘기려면 \RequirePackageWithOptions 를 씁니다.
\LoadClass 뒤가 드디어 이 클래스다운 부분 을 쓰는 자리입니다. \renewcommand 로 제목 체재를 바꾸고, \setlength 로 여백을 조정하고, \newcommand / \newenvironment 로 새 명령과 환경을 정의합니다. 필요한 추가 패키지도 여기서 \RequirePackage 로 불러옵니다. 거꾸로 말하면, \LoadClass 앞에 두어도 되는 것은 옵션의 선언과 처리뿐이라고 기억해 두면 순서로 고민할 일이 없어집니다.
완성 예: article 을 확장하는 최소 클래스
이상을 하나로 모으면 다음의 최소 .cls 가 됩니다. article 을 토대로 자체 draft 옵션을 더하고, 알 수 없는 옵션은 article 로 전달하며, 기본값으로 a4paper 를 흘려 넣고, 마지막에 여백과 절 번호 체재를 취향대로 맞춥니다. 이것을 myclass.cls 로 원고와 같은 폴더에 두고 문서에서 \documentclass[11pt,a4paper,draft]{myclass} 라고 쓰면 사용할 수 있습니다.
\NeedsTeXFormat{LaTeX2e}
\ProvidesClass{myclass}[2026/01/01 v1.0 My example class]
% --- declare options ---
\newif\if@my@draft \@my@draftfalse
\DeclareOption{draft}{\@my@drafttrue}
% forward everything else to article
\DeclareOption*{\PassOptionsToClass{\CurrentOption}{article}}
% --- defaults, then execute, then load the base class ---
\ExecuteOptions{a4paper}
\ProcessOptions\relax
\LoadClass{article}
% --- this class's own character ---
\RequirePackage[margin=25mm]{geometry}
\setlength{\parindent}{0pt}
\renewcommand{\thesection}{\Alph{section}}
\if@my@draft
\AtBeginDocument{\typeout{myclass: DRAFT MODE}}
\fi
\endinput끝의 \endinput 은 “이 파일은 여기까지”라고 LaTeX에 알리는 구분자로, 관례적으로 붙입니다. 그 뒤에 적은 메모나 샘플은 읽히지 않습니다. 패키지로 만들고 싶다면 \ProvidesClass 를 \ProvidesPackage 로, \LoadClass 를 \RequirePackage 로 바꾸면 같은 뼈대가 그대로 .sty 가 됩니다.
새로운 방식 — \DeclareKeys 와 \ProcessKeyOptions
\DeclareOption 은 지금도 완전히 유효하지만, 다루는 것은 “있다/없다” 스위치가 중심이라 logo=acme.pdf 처럼 값을 가진 옵션 은 직접 해석해야 합니다. 그래서 LaTeX 커널이 자체 key-value 인터페이스를 마련했습니다. \DeclareKeys 로 키를 선언하고 \ProcessKeyOptions 로 처리합니다. 선언할 때 키 이름에 붙이는 “속성”이 동작을 결정하며, 기본 속성은 .code(임의의 코드 실행), .if / .ifnot(TeX 참거짓 스위치 설정), .store(값을 매크로에 저장), .usage(불러올 때만/프리앰블 어디서나/제한 없음)입니다. 알 수 없는 키는 \DeclareUnknownKeyHandler 가 받고, \ProcessKeyOptions 를 부르면 \ProcessOptions 를 따로 부를 필요가 없습니다.
\NeedsTeXFormat{LaTeX2e}
\ProvidesClass{keyclass}[2026/01/01 v1.0 Key-value demo class]
\DeclareKeys[keyclass]{
draft.if = @keyclass@draft,
logo.store = \@keyclass@logo,
logo.usage = load
}
% anything that is not one of our keys goes to article
\DeclareUnknownKeyHandler[keyclass]{%
\PassOptionsToClass{\CurrentOption}{article}}
\ProcessKeyOptions[keyclass] % no \ProcessOptions needed
\LoadClass{article}
\endinput이 구조는 원래 l3keys2e 패키지가 제공하던 것이고, 그 핵심이 LaTeX2ε 커널에 들어갔습니다(TeX Live 2024에 실린 커널은 LaTeX2e 2023-11-01이며 \DeclareKeys, \ProcessKeyOptions, \SetKeys 를 갖추고 있습니다). 다만 기존 패키지에는 l3keys2e 를 불러오는 방식이 남아 있어서, 예컨대 jlreq.cls 는 앞부분에서 \RequirePackage{l3keys2e} 를 실행합니다. 구분은 간단합니다. 값을 받는 옵션이 하나라도 있으면 \DeclareKeys, 켜고 끄는 스위치뿐이라면 \DeclareOption 으로 충분합니다. 불러온 뒤 설정을 바꾸고 싶으면 \SetKeys 를 씁니다.
배포 전에 돌려 볼 최소 테스트
클래스는 읽히는 순간 문서 전체에 영향을 주므로, 본문을 쓰기 전에 작은 테스트 문서로 동작을 굳혀 둡니다. 확인할 것은 두 가지뿐입니다. 11pt 나 twocolumn 같은 표준 옵션이 기반 클래스까지 닿는지, 그리고 자신의 독자 옵션만 자작 코드에서 처리되는지입니다. 여기서 기대대로 움직이지 않는다면 원인은 거의 확실히 \ProcessOptions 의 위치, \DeclareOption* 의 전달, \LoadClass 의 순서 중 하나입니다.
\listfiles % log every file and version that is loaded
\documentclass[11pt,a4paper,draft]{myclass}
\begin{document}
\section{Smoke test}
Check the body size, the paper, the draft switch,
the heading style and the margins.
\end{document}- 로그에 클래스 정보가 나오나요?
Document Class: myclass ...줄과\ProvidesClass에 적은 날짜·버전이.log에 나오는지 확인합니다. 파일명과 클래스명이 어긋나면 나중에 반드시 혼란이 생깁니다. - 표준 옵션을 깨뜨리지 않았나요?
11pt나twocolumn이 무시된다면\DeclareOption*의 전달이나\LoadClass의 위치를 다시 봅니다. - 일부러 철자를 틀려 보십시오.
\documentclass[nosuchoption]{myclass}를 컴파일해 로그에Unused global option(s)가 나오는지 확인합니다. 나오지 않는다면 전달받은 어떤 패키지가 조용히 삼키고 있는 것입니다. \endinput이후는 비워 둡니다. 끝에 남긴 메모나 샘플은 그 표시가 사라지는 순간 입력으로 읽힙니다.
배포하는 형태까지 생각하기
자작 클래스의 진가는 내 손에서 처음 동작한 순간이 아니라 다른 사람이 다른 환경에서 불러 들이는 순간 에 검증됩니다. 최소한 .cls, 짧은 샘플 문서, README, 변경 이력을 같은 디렉터리에 두고 샘플이 그대로 컴파일되는지 확인하십시오. README에는 “기반 클래스로 전달하는 옵션”과 “자작 클래스만 받는 옵션”을 나누어 적습니다. 그래야 사용자가 11pt 가 어디서 적용되는지 따라갈 수 있습니다. 규모가 커지면 LaTeX 표준의 doc 과 docstrip 을 써서 .dtx 에 소스와 해설을 모으고 .ins 에서 .cls 를 생성하는 형태로 옮기면, 배포와 문서화가 한 줄기로 정리됩니다.
myclass/
myclass.cls
sample.tex
README.md
CHANGELOG.md마지막으로 샘플에 \listfiles 를 넣어 둡니다. 그러면 로그 끝에 읽힌 파일과 버전이 한 목록으로 나오므로, 사용자가 낡은 myclass.cls 를 로컬에 안고 있는지, 기대한 패키지가 실제로 읽히는지를 한눈에 가려낼 수 있습니다. 클래스는 문서 전체의 토대입니다. 오래 쓰이는 문서에서는 본문용 매크로를 하나 더 넣는 것보다 읽는 순서, 옵션 처리, 로그 정보를 꼼꼼히 다듬는 편이 훨씬 확실하게 효과를 냅니다.