🧩 Design System3. Typography & Spacing01 — Modular Scale & Type System

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.067Minor Second거의 차이 없음 (너무 조밀)
1.125Major Second조용·정보 밀도 높음 (대시보드, 문서)
1.2Minor Third균형
1.25Major Third표준 (콘텐츠 사이트의 BP)
1.333Perfect Fourth활기·헤드라인 강조 (마케팅)
1.414Augmented Fourth극적
1.5Perfect Fifth매우 극적 (포스터·랜딩)
1.618Golden Ratio클래식 (출판물)

비율이 클수록 대비가 커지고, 작을수록 조밀해진다. 하나만 골라 끝까지 민다 — 이게 핵심.

풀려는 문제이전의 해법한계
사이즈 결정 기준 부재디자이너의 직관일관성 깨짐·확장 어려움
비례 무너짐8/12/16/24/32 임의음악적 리듬 없음
새 사이즈 추가즉흥 결정다음 사이즈 예측 불가

How — 등비수열로 스케일 생성

공식

size(n) = base × ratio^n
  • base: 본문 크기 (보통 16px = 1rem)
  • ratio: 1.125 / 1.25 / 1.333 중 하나
  • n: 정수. 음수면 더 작은 사이즈, 양수면 더 큰 사이즈

1.25(Major Third) 적용 예시

n계산원값반올림토큰 이름
-216 × 1.25⁻²10.2410text.2xs
-116 × 1.25⁻¹12.812text.xs
016 × 1.25⁰1616text.base
116 × 1.25¹2020text.lg
216 × 1.25²2524text.xl
316 × 1.25³31.2530text.2xl
416 × 1.25⁴39.0636text.3xl
516 × 1.25⁵48.8348text.4xl
616 × 1.25⁶61.0460text.5xl

반올림은 보통 4 또는 2의 배수에 스냅한다. 시각적 정합과 spacing grid와의 합치를 위해.

3가지 ratio를 동시에 본 예시

base = 16px 고정, n = 0 ~ 6:

n1.1251.251.333
0161616
1182021
2202528
3233138
4263951
5294968
6326190
  • 1.125: 문서·대시보드처럼 정보 밀도가 높은 화면.
  • 1.25: 콘텐츠 사이트의 안전한 default.
  • 1.333: 마케팅·랜딩처럼 시각적 위계가 강해야 하는 곳.

What — type role + token 정의

스케일이 을 만들었다면, role그 값에 의미를 주는 의미적 이름이다.

Role tier (semantic naming)

Role일반 사이즈용도예시 컴포넌트
display.2xl60~72영웅 헤드라인Hero
display.xl48~56페이지 타이틀PageHeader
heading.xl36~40h1ArticleTitle
heading.lg28~32h2SectionHeader
heading.md22~24h3CardTitle
heading.sm18~20h4SubsectionHeader
body.lg18본문 강조Lead paragraph
body.md16본문 defaultParagraph
body.sm14본문 약화Description
caption12보조·라벨Caption, Hint
overline11카테고리·tagEyebrowText

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-xs12
text-sm141.167
text-base161.143
text-lg181.125
text-xl201.111
text-2xl241.2
text-3xl301.25
text-4xl361.2
text-5xl481.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 양쪽에 주입.