🎨 Frontend CSS4. 타이포그래피06 — Fluid Typography (clamp·cqi)

06 — Fluid Typography (clamp·cqi)

“이 글자가 모바일에선 14px, 데스크톱에선 18px이어야 한다” — 더 이상 미디어 쿼리 5개를 쓰지 않는다. clamp(min, ideal, max) 한 줄이 연속적 스케일을 만든다.


한 문장 답 (Pyramid Top)

유체 타이포는 clamp(최소값, viewport·container에 비례한 이상값, 최대값) 의 3-인자 공식으로 표현한다. 핵심은 비례 기준vw(뷰포트)에서 cqi(컨테이너 인라인)로 옮기는 모던 패러다임 — 같은 컴포넌트가 어느 컨테이너에 들어가든 그 컨테이너 폭에 맞게 스케일된다.


Why — 왜 유체 타이포인가

1) 미디어 쿼리 폭발

/* 전통 방식 */
h1 { font-size: 24px; }
@media (min-width: 480px)  { h1 { font-size: 28px; } }
@media (min-width: 768px)  { h1 { font-size: 36px; } }
@media (min-width: 1024px) { h1 { font-size: 44px; } }
@media (min-width: 1440px) { h1 { font-size: 56px; } }
  • 5개의 불연속 점프. 481px에서 갑자기 28→36.
  • 사이 사이즈는 외면.
  • 폰트 타입마다 이걸 반복 → 디자인 시스템이 불어남.

2) 디자인 시스템의 연속 스케일

디자이너는 사실 “화면이 커지면 글자도 점진적으로 커진다” 라는 직관을 가진다. CSS도 그 직관 그대로 표현되어야 한다.

3) 접근성: 사용자의 확대

px 고정 → 사용자가 브라우저 줌을 해도 폰트가 안 커짐. rem + clamp → 접근성을 깨지 않으면서 유체.


How — clamp() 의 3-인자 공식

font-size: clamp(MIN, IDEAL, MAX);
  • MIN — 절대 이 아래로는 안 내려감.
  • IDEAL — 이상적인 값 (보통 vw/cqi로 표현되는 동적 값).
  • MAX — 절대 이 위로는 안 올라감.

가장 흔한 패턴

:root {
  font-size: clamp(1rem, 0.5rem + 1vw, 1.25rem);
}
  • 모바일 (320px 뷰포트): 0.5rem + 1vw = 0.5×16 + 320×0.01 = 8 + 3.2 = 11.2px → MIN 16px 적용.
  • 데스크톱 (1280px): 0.5rem + 1vw = 8 + 12.8 = 20.8px → MAX 20px 적용.
  • 사이 (768px): 8 + 7.68 = 15.68px → MIN 16px 적용.

공식 유도

원하는 결과: 320px 화면에서 16px, 1280px 화면에서 20px.

font-size = a × vw + b × rem
320×a + b×16 = 16
1280×a + b×16 = 20

a × (1280 - 320) = 4
a = 4/960 = 0.00417 vw 단위 — 즉 0.417vw
b×16 = 16 - 320×0.00417 = 16 - 1.333 = 14.67
b = 0.917rem
font-size: clamp(1rem, 0.917rem + 0.417vw, 1.25rem);

유틸리티: Utopia.fyi

utopia.fyi에서 (min screen, min size, max screen, max size) 4개 입력하면 clamp 공식을 자동 생성.


How — vw vs cqi (모던 패러다임 전환)

vw (Viewport Width)

h1 { font-size: clamp(2rem, 1rem + 3vw, 4rem); }
  • 뷰포트 폭에 비례. 사이드바·콘텐츠 영역 폭과 무관.
  • 사이드바 안에 들어간 h1전체 뷰포트 기준으로 커짐 → 사이드바를 뚫고 나갈 수 있음.

cqi (Container Query Inline)

.card {
  container-type: inline-size;
}
 
.card h1 {
  font-size: clamp(1.5rem, 1rem + 3cqi, 3rem);
}
  • 컨테이너의 인라인 폭(보통 가로) 에 비례.
  • 카드가 300px이면 h1이 작고, 600px이면 큼.
  • 같은 카드 컴포넌트가 어디 들어가도 적응.

단위 변환

  • 1vw = 뷰포트 폭의 1%.
  • 1cqi = 컨테이너 인라인 폭의 1%.
  • 1cqw = 컨테이너 폭 (containers query width).
  • 1cqh = 컨테이너 높이 (vertical writing mode 시 inline).
  • 1cqmin, 1cqmax = 컨테이너 인/블록 중 최소/최대.

언제 vw, 언제 cqi

  • 사이트 전체 본문, 헤더vw (브라우저 폭에 따라 일관).
  • 카드, 사이드바 안의 텍스트, 재사용 컴포넌트cqi (컨테이너에 따라).

2024년 BP는 “기본 cqi, 특수한 경우만 vw”. Container query 지원이 모든 모던 브라우저에 도착했기 때문.


How — Modular Scale

유체 타이포의 개별 사이즈들을 공통 비율로 묶는다 — modular scale.

비율 (Type Scale)

이름비율특징
Minor Second1.067좁은 차이
Major Second1.125본문 위주
Minor Third1.2디폴트 (편안)
Major Third1.25흔히 사용
Perfect Fourth1.333표제 강조
Golden Ratio1.618극적

적용

:root {
  --ratio: 1.25;
  --step-0: 1rem;
  --step-1: calc(var(--step-0) * var(--ratio));   /* 1.25rem */
  --step-2: calc(var(--step-1) * var(--ratio));   /* 1.5625rem */
  --step-3: calc(var(--step-2) * var(--ratio));
  --step-4: calc(var(--step-3) * var(--ratio));
  --step--1: calc(var(--step-0) / var(--ratio));  /* 0.8rem */
}
 
h1 { font-size: var(--step-4); }
h2 { font-size: var(--step-3); }
h3 { font-size: var(--step-2); }
body { font-size: var(--step-0); }
small { font-size: var(--step--1); }

Fluid Modular Scale (조합)

각 step을 clamp으로 감싼다:

:root {
  --step-0: clamp(1rem, 0.9rem + 0.5vw, 1.125rem);
  --step-1: clamp(1.25rem, 1.1rem + 0.75vw, 1.5rem);
  --step-2: clamp(1.5rem, 1.3rem + 1vw, 2rem);
  --step-3: clamp(2rem, 1.7rem + 1.5vw, 3rem);
  --step-4: clamp(2.5rem, 2rem + 2.5vw, 4rem);
}

→ 모든 스케일이 연속적·비율적·접근성 보장.


What — 완전한 BP 토큰

:root {
  /* 1. 기준 크기 (모바일·데스크톱) */
  --fs-min: 1rem;
  --fs-max: 1.125rem;
  
  /* 2. 비율 */
  --ratio: 1.25;
  
  /* 3. fluid scale */
  --fs-0: clamp(var(--fs-min), 0.9rem + 0.5vw, var(--fs-max));
  --fs-1: clamp(calc(var(--fs-min) * var(--ratio)), 1.1rem + 0.6vw, calc(var(--fs-max) * var(--ratio)));
  --fs-2: clamp(calc(var(--fs-min) * pow(var(--ratio), 2)), 1.4rem + 0.8vw, calc(var(--fs-max) * pow(var(--ratio), 2)));
  --fs-3: clamp(calc(var(--fs-min) * pow(var(--ratio), 3)), 1.8rem + 1.2vw, calc(var(--fs-max) * pow(var(--ratio), 3)));
  --fs-4: clamp(calc(var(--fs-min) * pow(var(--ratio), 4)), 2.2rem + 2vw, calc(var(--fs-max) * pow(var(--ratio), 4)));
}
 
body { font-size: var(--fs-0); }
h3 { font-size: var(--fs-2); }
h2 { font-size: var(--fs-3); }
h1 { font-size: var(--fs-4); }

pow()은 CSS Values 4에서 등장 — Chrome 112+, Safari 16.4+ 지원. 미지원 시 직접 곱.


What — 접근성: 사용자 줌과 rem

px 고정의 문제

body { font-size: 16px; }  /* 사용자가 브라우저에서 폰트 키워도 변하지 않음 */

rem + clamp의 해결

body { font-size: clamp(1rem, 0.9rem + 0.5vw, 1.125rem); }
  • 사용자가 브라우저 기본 폰트를 18px로 키우면rem = 18px이 되어 모든 사이즈가 함께 커짐.
  • 시각 장애 사용자, 노인, 고해상도 모니터 사용자에게 핵심 접근성.

WCAG 2.1 권장

  • 본문 텍스트는 최소 16px (모바일).
  • 사용자가 200%까지 줌해도 콘텐츠 손실 없음rem + clamp로 자동.

What — cqi 실전 예시

카드 컴포넌트

.card {
  container-type: inline-size;
  container-name: card;
}
 
.card__title {
  font-size: clamp(1rem, 4cqi, 2rem);
}
 
.card__body {
  font-size: clamp(0.875rem, 2.5cqi, 1rem);
}
  • 카드가 그리드의 한 셀로 200px에 들어가도, 전체폭 hero로 1200px에 들어가도 각각 적절한 폰트 크기.
  • 미디어 쿼리 없이.

Grid + cqi의 시너지

.layout {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 30ch), 1fr));
  gap: 1rem;
}
 
.card {
  container-type: inline-size;
}
  • 그리드가 컨테이너 폭을 자동 분배.
  • 카드가 자기 container query로 그 폭에 맞게 폰트 스케일.
  • 반응형이 컴포넌트의 책임으로 캡슐화.

What-if — 잘못 쓰면

1) vw만 사용 (clamp 없이)

h1 { font-size: 5vw; }
  • 1920px 화면: 96px (너무 큼).
  • 320px 화면: 16px (제목치고 너무 작음).
  • 사용자 줌 무시.

clamp(1.5rem, 5vw, 4rem)처럼 경계를 잡는다.

2) MIN/MAX에 vw 단위

font-size: clamp(2vw, 3vw, 5vw);  /* 의미 없음 */
  • MIN/MAX 자체가 vw → 작은 화면에선 MIN도 작아진다.

→ MIN/MAX는 절대 단위(rem/px), IDEAL만 vw/cqi.

3) Container 설정 없이 cqi

.card h1 { font-size: clamp(1rem, 4cqi, 2rem); }
/* .card에 container-type 없음 */
  • cqi가장 가까운 viewport로 fallback (브라우저별 다름).

반드시 부모에 container-type: inline-size.

4) em 누적

:root { font-size: 1.25em; }
.large { font-size: 1.25em; /* 부모의 1.25em = 1.5625em */ }
.large .larger { font-size: 1.25em; /* 1.95em */ }

→ 누적 폭발. 항상 rem.

5) Fluid한 line-height

h1 {
  font-size: clamp(1.5rem, 4vw, 3rem);
  line-height: 4vw;  /* 망함 — 폰트보다 작아질 수도 */
}

line-height단위 없는 숫자. 폰트와 함께 자동 비례.


Insight — 흥미로운 이야기

“clamp는 2020년 도착, 4년 만에 미디어 쿼리를 절반으로 줄였다”

2020년 Chrome 79, Safari 13.1, Firefox 75에 clamp()가 동시에 들어갔다. 같은 해 4월 CSS Tricks에 “Linearly Scale font-size with CSS clamp() Based on the Viewport”라는 글이 올라왔고, 곧 utopia.fyi가 등장하며 clamp 계산기가 디자이너의 표준 도구가 됐다. 2023년 통계로 상위 1000개 사이트의 60%가 clamp() 사용 — 가장 빠르게 채택된 CSS 함수 중 하나. 미디어 쿼리는 죽지 않았지만, 폰트 크기에서만은 멸종에 가까워졌다.

“container query는 1999년부터 외친 꿈이었다”

CSS-Tricks의 Chris Coyier가 “Element Queries” (지금의 container query)를 2010년부터 주장해 왔다. 답답한 이유는 브라우저 구현이 무한 루프 위험을 안고 있었기 때문 — 컨테이너가 자식의 크기에 영향받고 자식이 다시 컨테이너에 의존하면 순환 의존. 2021년 Miriam Suzanne(CSSWG 멤버)이 container-type: inline-size라는 해결책을 설계 — 컨테이너의 인라인 폭만 쿼리 대상으로 한정해 사이클을 차단. 2022년 Chrome 105, 2023년 Safari 16에 도착. 12년 만에 등장한 표준이 미디어 쿼리의 패러다임을 컴포넌트 단위로 바꿨다.


요약 + Mermaid

  • 유체 타이포 공식: clamp(MIN, IDEAL = a*rem + b*vw|cqi, MAX).
  • MIN/MAX는 절대 단위, IDEAL만 동적 단위.
  • vw(사이트 전체) → cqi(컴포넌트) 로 패러다임 이동.
  • Modular scale + clamp = fluid modular scale (연속 + 비율).
  • 반드시 rem 기반 — 접근성 줌 보장.

다음 문서 → 07-opentype-features: 폰트가 이미 가진 미세 조판 기능들.