01 — Modular Scale & Type System
이 문서가 답하는 질문:
text-xs,text-sm,text-base,text-lg,text-xl,text-2xl… 이 값들은 누가 어떤 수학으로 골랐는가? 그리고 왜 그 수학이 시각적으로 “맞아 보이는가”? 한 줄 답 (Pyramid Top): 좋은 type scale은 하나의 기준값(base) × 하나의 비율(ratio)을 거듭제곱하여 얻은 유한한 등비수열이다 — 비율은 1.125(Major Second) / 1.2(Minor Third) / 1.25(Major Third) / 1.333(Perfect Fourth) 중 하나를 골라 끝까지 민다. 비율을 섞으면 스케일이 깨진다.
Why — 왜 ratio 기반 스케일인가
1) “임의의 16/18/22/27px”의 문제
/* 실패 모드 — 디자이너가 즉흥적으로 정한 값 */
.h1 { font-size: 27px; }
.h2 { font-size: 22px; }
.h3 { font-size: 19px; }
.body { font-size: 16px; }- 22→19, 19→16, 27→22 — 사이 간격이 불규칙. 시각적 리듬이 없다.
- 새 사이즈를 추가할 때 어디에 끼울지 결정할 기준이 없다.
- 다크모드/모바일에서 비례가 무너진다.
2) 음악의 음정에서 빌려온 해법
타이포그래피의 modular scale은 음악의 음정(interval) 비율에서 왔다. Major Second(1.125)는 도-레, Perfect Fourth(1.333)는 도-파 같은 청각적 비례를 시각에 가져온 것이다.
| 비율 | 음정 이름 | 시각적 인상 |
|---|---|---|
| 1.067 | Minor Second | 거의 차이 없음 (너무 조밀) |
| 1.125 | Major Second | 조용·정보 밀도 높음 (대시보드, 문서) |
| 1.2 | Minor Third | 균형 |
| 1.25 | Major Third | 표준 (콘텐츠 사이트의 BP) |
| 1.333 | Perfect Fourth | 활기·헤드라인 강조 (마케팅) |
| 1.414 | Augmented Fourth | 극적 |
| 1.5 | Perfect Fifth | 매우 극적 (포스터·랜딩) |
| 1.618 | Golden Ratio | 클래식 (출판물) |
비율이 클수록 대비가 커지고, 작을수록 조밀해진다. 하나만 골라 끝까지 민다 — 이게 핵심.
| 풀려는 문제 | 이전의 해법 | 한계 |
|---|---|---|
| 사이즈 결정 기준 부재 | 디자이너의 직관 | 일관성 깨짐·확장 어려움 |
| 비례 무너짐 | 8/12/16/24/32 임의 | 음악적 리듬 없음 |
| 새 사이즈 추가 | 즉흥 결정 | 다음 사이즈 예측 불가 |
How — 등비수열로 스케일 생성
공식
size(n) = base × ratio^nbase: 본문 크기 (보통16px = 1rem)ratio: 1.125 / 1.25 / 1.333 중 하나n: 정수. 음수면 더 작은 사이즈, 양수면 더 큰 사이즈
1.25(Major Third) 적용 예시
| n | 계산 | 원값 | 반올림 | 토큰 이름 |
|---|---|---|---|---|
| -2 | 16 × 1.25⁻² | 10.24 | 10 | text.2xs |
| -1 | 16 × 1.25⁻¹ | 12.8 | 12 | text.xs |
| 0 | 16 × 1.25⁰ | 16 | 16 | text.base |
| 1 | 16 × 1.25¹ | 20 | 20 | text.lg |
| 2 | 16 × 1.25² | 25 | 24 | text.xl |
| 3 | 16 × 1.25³ | 31.25 | 30 | text.2xl |
| 4 | 16 × 1.25⁴ | 39.06 | 36 | text.3xl |
| 5 | 16 × 1.25⁵ | 48.83 | 48 | text.4xl |
| 6 | 16 × 1.25⁶ | 61.04 | 60 | text.5xl |
반올림은 보통 4 또는 2의 배수에 스냅한다. 시각적 정합과 spacing grid와의 합치를 위해.
3가지 ratio를 동시에 본 예시
base = 16px 고정, n = 0 ~ 6:
| n | 1.125 | 1.25 | 1.333 |
|---|---|---|---|
| 0 | 16 | 16 | 16 |
| 1 | 18 | 20 | 21 |
| 2 | 20 | 25 | 28 |
| 3 | 23 | 31 | 38 |
| 4 | 26 | 39 | 51 |
| 5 | 29 | 49 | 68 |
| 6 | 32 | 61 | 90 |
- 1.125: 문서·대시보드처럼 정보 밀도가 높은 화면.
- 1.25: 콘텐츠 사이트의 안전한 default.
- 1.333: 마케팅·랜딩처럼 시각적 위계가 강해야 하는 곳.
What — type role + token 정의
스케일이 값을 만들었다면, role은 그 값에 의미를 주는 의미적 이름이다.
Role tier (semantic naming)
| Role | 일반 사이즈 | 용도 | 예시 컴포넌트 |
|---|---|---|---|
display.2xl | 60~72 | 영웅 헤드라인 | Hero |
display.xl | 48~56 | 페이지 타이틀 | PageHeader |
heading.xl | 36~40 | h1 | ArticleTitle |
heading.lg | 28~32 | h2 | SectionHeader |
heading.md | 22~24 | h3 | CardTitle |
heading.sm | 18~20 | h4 | SubsectionHeader |
body.lg | 18 | 본문 강조 | Lead paragraph |
body.md | 16 | 본문 default | Paragraph |
body.sm | 14 | 본문 약화 | Description |
caption | 12 | 보조·라벨 | Caption, Hint |
overline | 11 | 카테고리·tag | EyebrowText |
DTCG JSON으로 표현
{
"font": {
"size": {
"scale": {
"_meta": { "base": "16px", "ratio": 1.25 },
"2xs": { "$value": "10px", "$type": "dimension" },
"xs": { "$value": "12px", "$type": "dimension" },
"sm": { "$value": "14px", "$type": "dimension" },
"base":{ "$value": "16px", "$type": "dimension" },
"lg": { "$value": "20px", "$type": "dimension" },
"xl": { "$value": "24px", "$type": "dimension" },
"2xl": { "$value": "30px", "$type": "dimension" },
"3xl": { "$value": "36px", "$type": "dimension" },
"4xl": { "$value": "48px", "$type": "dimension" },
"5xl": { "$value": "60px", "$type": "dimension" }
},
"role": {
"body": { "md": { "$value": "{font.size.scale.base}" } },
"heading": {
"md": { "$value": "{font.size.scale.xl}" },
"lg": { "$value": "{font.size.scale.2xl}" }
},
"display": {
"xl": { "$value": "{font.size.scale.5xl}" }
}
}
}
}
}scale.*는 원시 primitive — 비율로 만든 닫힌 집합.role.*는 semantic — primitive를 alias 한다.- 컴포넌트는 항상
role.*만 참조한다 (primitive 직접 참조 금지).
Panda CSS
// panda.config.ts
import { defineConfig } from "@pandacss/dev";
export default defineConfig({
theme: {
tokens: {
fontSizes: {
"2xs": { value: "0.625rem" }, // 10px
xs: { value: "0.75rem" }, // 12px
sm: { value: "0.875rem" }, // 14px
base: { value: "1rem" }, // 16px
lg: { value: "1.25rem" }, // 20px
xl: { value: "1.5rem" }, // 24px
"2xl": { value: "1.875rem" }, // 30px
"3xl": { value: "2.25rem" }, // 36px
"4xl": { value: "3rem" }, // 48px
"5xl": { value: "3.75rem" } // 60px
}
},
semanticTokens: {
fontSizes: {
"body.md": { value: "{fontSizes.base}" },
"heading.lg": { value: "{fontSizes.2xl}" },
"display.xl": { value: "{fontSizes.5xl}" }
}
}
}
});Tailwind (v4 @theme directive)
@theme {
--text-2xs: 0.625rem;
--text-xs: 0.75rem;
--text-sm: 0.875rem;
--text-base: 1rem;
--text-lg: 1.25rem;
--text-xl: 1.5rem;
--text-2xl: 1.875rem;
--text-3xl: 2.25rem;
--text-4xl: 3rem;
--text-5xl: 3.75rem;
}두 설정은 같은 DTCG JSON에서 생성되어야 한다 —
08-pipeline-distribution참고.
What — Fluid type with clamp()
스케일이 단계를 만들었다면, clamp()는 그 단계 사이를 연속적으로 보간한다.
/* 320px 화면에서 16px, 1280px 화면에서 20px */
font-size: clamp(1rem, 0.75rem + 0.78vw, 1.25rem);- 모바일 (320px):
0.75rem + 0.78vw = 12 + 2.5 = 14.5px→ MIN 16px 적용. - 데스크톱 (1280px):
0.75rem + 0.78vw = 12 + 10 = 22px→ MAX 20px 적용. - 사이는 연속적으로 변함.
자세한 공식과 container query 기반 fluid는 06-responsive-type-and-fluid-scale에서.
What-if — 잘못 쓰면 어떻게 깨지는가
- 함정 1: 비율 섞기 — 본문은 1.25로 만들고 헤드라인은 1.333으로 한 디자이너의 직관 → 두 스케일이 교차하면서 같은 사이즈가 두 번 등장하거나 인접 사이즈끼리 거의 같아짐. 대응: 하나만 골라 끝까지 민다.
- 함정 2: T-shirt 사이즈 늘려 쓰기 —
5xl다음에6xl,7xl, …. 6개 이상이 되면 디자이너가 어느 게 큰지 못 외움. 대응:display.xl같은 role로 전환하거나, 숫자 기반(text.60,text.72)으로 바꿔라. - 함정 3:
px고정 — 사용자가 브라우저 폰트 크기를 크게 설정해도 안 변함 (접근성 위반). 대응: 항상rem. - 함정 4: 본문 사이즈를 너무 작게 —
text.sm = 14px를 본문 default로 쓰는 사이트가 흔한데, 16px 이하는 접근성·가독성 모두 위험. 본문 default는 최소 16px. - 함정 5:
font-size만 토큰화 —line-height,letter-spacing이 빠지면 role 정의가 깨진다. 항상 3-튜플로 정의 (04-line-height-and-leading-trim 참고).
Insight — 왜 1.25가 사실상 default가 되었나
Tim Brown의 2011년 글 “More Meaningful Typography” (A List Apart)가 modular scale을 웹에 가져왔다. 그가 만든 modularscale.com은 비율을 값으로 자동 변환해주는 첫 도구였다.
이후 Tailwind CSS의 text-xs/sm/base/lg/xl/... 기본 스케일은 사실상 1.125(13→14·14→16 부근)와 1.25(16→20·20→24)를 혼합한 실용 변형이다 — 작은 사이즈는 시각적 차이를 유지하려고 1.125로, 큰 사이즈는 위계를 살리려고 1.25 이상으로 점차 늘어진다.
| Tailwind | 값(px) | 인접 비율 |
|---|---|---|
text-xs | 12 | — |
text-sm | 14 | 1.167 |
text-base | 16 | 1.143 |
text-lg | 18 | 1.125 |
text-xl | 20 | 1.111 |
text-2xl | 24 | 1.2 |
text-3xl | 30 | 1.25 |
text-4xl | 36 | 1.2 |
text-5xl | 48 | 1.333 |
순수한 1.25 등비수열이 아니라, 작은 쪽은 더 촘촘하게 큰 쪽은 더 벌어지게 조정한 반-등비다. 이게 실무에서 가장 자연스럽게 느껴지는 이유이기도 하다 — 인간의 시각 인지는 절대값이 작을수록 상대차이에 둔감하기 때문 (Weber-Fechner 법칙).
요약
- Type scale은
base × ratio^n의 등비수열이다. - 비율은 1.125 / 1.25 / 1.333 중 하나를 끝까지 민다. 섞지 말 것.
- 값은
scale.*(primitive), 의미는role.*(semantic)로 분리. rem우선,px금지. 본문 default는 최소 16px.- 같은 스케일을 DTCG → Panda + Tailwind 양쪽에 주입.