designpaca/packages/skill/references/typography.md
Yun Chan 6805fb2be7 feat(skill): absorb external design skills, restore interview gate, add review route
- Restore the step-0 interview as a mechanical gate the skill explicitly
  depends on; add harness.md (per-harness question tools, limits,
  fallbacks) and brief-interview.md (slots, question cards, rounds).
- Add 10 reference docs absorbed from external design skills
  (accessibility, interaction-feel, elevation, color, icons, product-copy,
  component-systems, critique, change-review, print-email) and extend
  existing references.
- Add a review-only route and two hard-gate clauses (truncated content
  reachability, three-flashes limit).
- design-gate: split tap targets into WCAG 2.5.8 and 44px contract layers,
  run axe-core when available, and fix false positives found on a real
  site (decorative alt="", stacked wordmark line count, url-only pages).
- lint-skill: fail if the interview gate section or its links disappear.
- Ship agents/openai.yaml and THIRD_PARTY_NOTICES.md.
2026-09-24 13:26:03 +09:00

37 KiB

typography — 폰트를 고르고 싣는 법

3단계에서 폰트를 정할 때, 4-1에서 적용할 때 읽는다.

타입 스케일과 간격은 tokens.md 가 다룬다. 이 문서는 어떤 글자를 쓰고 그것을 어떻게 화면에 올리는가만 다룬다.

이 문서를 읽는 법

상황 읽을 곳
폰트를 아직 안 골랐다 §1 선택 절차. 대부분 여기서 끝난다
웨이트를 두 개 이상 쓴다 §2 가변 폰트
로딩이 느리거나 글자가 늦게 뜬다 §3 로딩
폰트 로드 전후로 레이아웃이 흔들린다 §4 폴백 메트릭. 가장 자주 빠뜨리는 곳이다
숫자가 흔들리거나 자간이 이상하다 §5 OpenType
한글이 들어간다 §6. 조판 규칙 자체는 antipatterns.md §8
배포 전 §7 라이선스, §8 체크리스트
헤딩 크기·자간·행간을 어떻게 조정할지 원칙이 필요하다 §9
폰트 기능(font-synthesis, OpenType, 동적 숫자)을 더 쓰고 싶다 §10
줄바꿈·위도우·justify 문제가 있다 §11
밑줄이나 text-box 트림이 필요하다 §12
텍스트가 잘려서 전체 값을 볼 수 없다 §13. 하드 게이트
폰트 스무딩·선택 가능성·장식 텍스트 디테일이 필요하다 §14
배포 직전 타이포 전용 빠른 점검이 필요하다 §15

1. 고르는 절차

1-1. 먼저 개수를 정한다

디스플레이 하나와 본문 하나, 필요할 때 모노 하나는 관리하기 쉬운 시작점이다. 추가 패밀리는 언어 범위·역할·전송량·라이선스와 실제 위계 효과를 근거로 제한한다. 가변 폰트가 정적 파일보다 항상 작거나 모든 필요한 축을 제공하는 것은 아니므로 실제 subset과 로딩 결과를 비교한다.

1-2. 성격을 브리프에서 끌어낸다

폰트를 고르기 전에 한 줄로 답한다: 이 글자는 무엇처럼 보여야 하는가.

브리프가 요구하는 것 글자에서 찾을 성질
정확함, 계기, 데이터 좁은 폭, 낮은 대비, 열린 카운터, 모노와 잘 붙는 것
편집, 읽는 시간, 권위 세리프 또는 고대비 산세, 넉넉한 x-height
도구, 중립, 신뢰 그로테스크. 성격은 웨이트와 여백이 만든다
인상, 기억 디스플레이 전용 서체를 우선 검토한다. 본문 사용은 긴 읽기·언어 범위·확대 상태에서 검증한 경우에만 채택한다

1-3. 후보를 실제 값으로 검증한다

이름과 인상으로 고르지 마라. 다음 넷을 본다.

실제 proof에는 패밀리, 계산된 크기, 행간, 본문 폭을 따로 기록한다. 같은 CSS px도 글꼴의 x-height·획·글자틀에 따라 같은 광학 크기로 보이지 않는다. 선호와 읽기 성능도 같은 값이 아니므로 Wallace et al.의 조건처럼 실제 독자·문자 체계·과업에서 분리해 확인한다.

  • x-height 비율 — 소문자 높이와 본문 크기의 관계. 단일 임계값으로 가독성을 판정하지 말고 실제 크기·언어·렌더링에서 확인한다
  • 대비 — 획 굵기 차이. 큰 크기에서만 살아나는 폰트가 있다
  • 웨이트 범위 — 필요한 웨이트가 실제로 있는가. 없는 웨이트를 브라우저가 합성하면 뭉개진다
  • 숫자 형태 — 라이닝인가 올드스타일인가, tabular 가 있는가

브라우저에서 재는 법:

// 미로딩 매칭 폰트를 사용해야 하는지 점검한다
document.fonts.check('16px "Pretendard Variable"', '가나다ABC0123');
[...document.fonts].map(f => `${f.family} ${f.weight} ${f.status}`);

true는 특정 폰트의 존재·실제 사용·개별 글리프 지원을 보증하지 않는다. MDN FontFaceSet.check()을 2026-09-20에 확인했다. 화면에 필요한 문자·언어·숫자·기호와 웨이트를 실제 카피로 확인하고, 가능한 브라우저의 rendered-font 확인 또는 필요한 글리프 검증과 실렌더 관찰을 결합한다. heading·line box 밖 rect만으로 글리프 잘림을 확정하지 않는 절차와 artifact 조건은 audit-gate.md의 렌더 측정 계약을 따른다.

1-4. 기본값을 피한다

Inter 는 금지가 아니라 기본값 금지다. 다국어 UI나 초고밀도 대시보드처럼 이유가 있으면 쓴다. 이유 없이 쓰면 그건 고른 게 아니다.

같은 이유로 Fraunces 와 Instrument Serif 도 기본 선택에서 뺀다. 생성 도구가 가장 자주 뱉는 두 서체다.

못 대면 기본값이다. "왜 이 폰트인가"에 한 문장으로 답하지 못하면 아직 고른 것이 아니다.


2. 가변 폰트

여러 웨이트가 필요하면 가변 폰트를 후보로 둔다. 필요한 glyph·축·subset·캐시 조건에 따라 정적 파일 묶음이 더 작거나 호환성이 나을 수 있으므로 전송량과 실제 렌더를 비교한다.

축

축 뜻 쓸 곳
wght 굵기 위계. 가장 많이 쓴다
opsz 광학 크기 큰 글자와 작은 글자의 획 대비를 자동 조정
wdth 폭 좁은 컬럼, 긴 제목
slnt / ital 기울기 강조

쓰는 법

/* 표준 속성을 쓴다. font-variation-settings 는 마지막 수단이다 */
h1 { font-weight: 640; }          /* 100 단위가 아니어도 된다 */
.lead { font-weight: 450; }

/* opsz 가 있으면 자동 적용된다. 끄지 마라 */
h1 { font-optical-sizing: auto; }

/* 표준 속성으로 못 여는 축만 이걸로 */
.wide { font-variation-settings: "wdth" 112; }

font-variation-settings 로 wght 를 지정하지 마라. 상속이 끊기고 font-weight 와 충돌한다.

애니메이션

가변 축 애니메이션은 레이아웃 계산 비용을 만들 수 있다. 자동 금지가 아니라, 3단계의 성능 예산 안에서 비용·입력 간섭·감소 모션 경로·상태 피드백을 실제 기기와 브라우저에서 측정해 채택한다. 글자 굵기 변화가 필요하지만 그 조건을 충족하지 못하면 opacity로 두 겹을 교차시키는 대안을 검토한다.


3. 로딩

3-1. font-display 를 고른다

값 동작 언제
swap 즉시 폴백, 로드되면 교체 본문 기본값. 글자가 먼저 읽힌다
optional 아주 짧게 기다리고, 늦으면 이번 방문은 폴백으로 끝 CLS 를 0으로 만들고 싶을 때. 첫 방문에 브랜드 서체가 안 보일 수 있다
fallback 짧은 블록 + 짧은 교체 창 절충안
block 최대 3초 블록 아이콘 폰트 외에는 쓰지 마라

3-2. preload 는 조건부다

첫 화면에 실제로 그려지는 폰트 파일만 preload 한다. 그 외에는 다른 자원의 대역을 뺏을 뿐이다.

<!-- 본문 한 벌만. crossorigin 을 빠뜨리면 두 번 받는다 -->
<link rel="preload" as="font" type="font/woff2" crossorigin
      href="/fonts/body-var.woff2" />

모노는 보통 코드 블록에만 쓰이므로 preload 하지 않는다.

3-3. 서브셋

라틴은 unicode-range 로 쪼갠다. 브라우저가 실제로 쓰인 범위만 받는다.

@font-face {
  font-family: "Body";
  src: url("/fonts/body-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+2000-206F, U+2190-21BB;
  font-display: swap;
}

한글은 §6 을 보라. 범위가 아니라 글자 단위로 쪼개야 한다.

3-4. 셀프 호스팅 vs CDN

장점 대가
셀프 호스팅 요청 도메인 하나, 캐시 통제, 서드파티 장애 무관 업데이트를 직접 한다
CDN 설치가 한 줄 도메인이 늘고(각각 DNS+TLS), 그 서비스가 죽으면 글자가 죽는다

CDN을 쓸 때 preconnect는 실제 첫 화면 요청·연결 비용을 측정한 뒤 선택한다. 도메인 수만으로 결함을 판정하지 말고, 권한·캐시·장애 경로·전송 waterfall을 함께 검토한다.


4. 폴백 메트릭 — 가장 자주 빠뜨리는 곳

폰트가 늦게 오면 표시 전략에 따라 폴백과 웹폰트가 교체될 수 있다. 두 폰트의 행 높이·폭이 다르면 레이아웃 이동의 원인이 될 수 있지만, swap이 반드시 CLS를 만든다고 단정하지 않는다. 실제 네트워크 조건에서 측정한다.

해결은 폴백 폰트를 실제 폰트의 치수에 맞추는 것이다.

/* 실제 폰트 */
@font-face {
  font-family: "Body";
  src: url("/fonts/body-var.woff2") format("woff2-variations");
  font-weight: 300 800;
  font-display: swap;
}

/* 같은 치수로 보정한 폴백. 이름을 따로 준다 */
@font-face {
  font-family: "Body Fallback";
  src: local("Arial");
  size-adjust: 107%;          /* 글자 크기를 맞춘다 */
  ascent-override: 92%;       /* 행 위 여백 */
  descent-override: 24%;      /* 행 아래 여백 */
  line-gap-override: 0%;
}

body {
  font-family: "Body", "Body Fallback", sans-serif;
}

숫자는 폰트마다 다르다. 재는 방법:

// 두 폰트로 같은 문자열을 그려 폭을 비교한다. 비율이 size-adjust 값이다
const measure = (family) => {
  const el = document.createElement("span");
  el.style.cssText = `position:absolute;visibility:hidden;font:100px ${family};white-space:nowrap`;
  el.textContent = "가나다ABCabc0123";
  document.body.appendChild(el);
  const w = el.getBoundingClientRect().width;
  el.remove();
  return w;
};
(measure("Arial") / measure('"Pretendard Variable"') * 100).toFixed(1) + "%";

ascent-override / descent-override 는 실제 폰트의 hhea 또는 OS/2 값을 unitsPerEm 으로 나눈 백분율이다. 도구가 없으면 행 높이가 눈에 띄게 튀지 않을 때까지 조정하고 그 값을 design.md 에 적어라.

font-display: optional은 교체 가능성을 줄이는 한 선택지다. 폴백 메트릭 보정, 표시 전략, preload 여부는 실제 첫 방문·재방문·느린 망에서 조합해 검증한다.


5. OpenType 기능

숫자

/* 표에 들어가는 수치. 자리가 흔들리지 않는다 */
.nums { font-variant-numeric: tabular-nums; }

/* 본문 속 숫자. 소문자 높이에 맞는다 */
.prose { font-variant-numeric: oldstyle-nums; }

/* 분수, 서수 */
.frac { font-variant-numeric: diagonal-fractions ordinal; }

수치를 나열하는 곳에 tabular-nums 가 없으면 숫자가 춤춘다. 대시보드·가격표·측정값 전부 해당한다.

그 외

/* 커닝과 리가처는 기본으로 켜져 있다. 끄지 마라 */
body { font-kerning: normal; }

/* 대문자 사이 여백 보정. 전부 대문자인 짧은 라벨에만 */
.caps { font-variant-caps: all-small-caps; letter-spacing: 0.06em; }

/* 폰트가 제공하는 대체 글자. 무엇이 있는지 확인하고 쓴다 */
.alt { font-feature-settings: "ss01" 1; }

font-variant-* 표준 속성이 있으면 그걸 쓴다. font-feature-settings 는 표준 속성이 없는 기능에만 쓴다 — 한 번 쓰면 다른 기본 기능이 꺼지기 때문이다.


6. 한글

조판 규칙(줄바꿈·행간·자간·한 줄 길이)은 antipatterns.md §8 이 정본이다. 여기서는 폰트 자체만 다룬다.

6-1. 서브셋은 선택이 아니다

한글 완성형은 11,172자다. 전체 폰트가 클 수 있으므로 subset 전략을 검토한다. 모든 프로젝트가 글자 빈도 기반의 동적 subset을 반드시 써야 하는 것은 아니며, 콘텐츠 범위·cache·운영 복잡도와 실제 전송량으로 결정한다.

쪼개는 단위가 라틴과 다르다. 라틴은 unicode-range 로 블록을 나누면 되지만, 한글은 자주 쓰는 글자가 블록에 흩어져 있다. 그래서 글자 빈도로 나눈 수십 개 조각을 만들고 각각에 unicode-range 를 건다. 브라우저가 페이지에 실제로 쓰인 조각만 받는다.

이 방식을 동적 서브셋이라 부른다. Pretendard 의 dynamic-subset 빌드가 그것이고, 파일이 수십 개로 보이는 이유다.

파일 개수로 폰트 예산을 세지 마라. 한글 웹폰트가 파일 40개인 것은 정상이고 올바른 최적화다. 세야 할 것은 패밀리 수와 실제 전송량이다(tokens.md §5).

6-2. 웨이트가 라틴과 다르게 보인다

같은 700 이라도 한글은 획이 많아 더 무겁게 보인다. 라틴 기준으로 고른 웨이트를 한글에 그대로 쓰면 본문이 답답해진다. 한글 본문은 400500, 제목은 600700 정도에서 시작해 눈으로 맞춘다.

한글 폰트에는 x-height 개념이 없다. 대신 글자틀 대비 속공간을 본다. 속공간이 좁은 서체는 작은 크기에서 막힌다.

6-3. 라틴과 섞을 때 순서

/* 라틴 폰트를 앞에, 한글 폰트를 뒤에 둔다.
   순서를 바꾸면 숫자와 영문까지 한글 폰트로 그려져 조악해진다 */
font-family: "Switzer", "Pretendard Variable", system-ui, sans-serif;

두 폰트의 크기감이 다르면 size-adjust 를 쓴 별도 @font-face 로 맞춘다. §4 와 같은 기법이다.

6-4. 한글 폰트를 지정하지 않는 것

그 자체가 완성도 미달 신호다. 지정하지 않으면 맑은 고딕이나 애플 SD 산돌고딕으로 떨어지고, 그 둘은 서로 크게 달라서 화면마다 다른 디자인이 된다.


세로쓰기에 가로쓰기 자간을 그대로 쓰지 마라

writing-mode: vertical-rl 로 세운 글자에 라벨용 자간(letter-spacing: 0.14em)을 그대로 물리면 글자 사이 세로 간격이 되어 낱글자가 흩어진다.

한글은 자소가 모여 한 글자를 이루기 때문에 라틴보다 심하다 — "플로럴 스튜디오" 가 낱글자 일곱 개로 읽히기 시작한다.

.vertical {
  writing-mode: vertical-rl;
  letter-spacing: 0.02em;   /* 가로쓰기 라벨의 0.14em 을 그대로 쓰면 안 된다 */
}

세로쓰기는 자간이 아니라 line-height 로 숨을 만든다. 세로쓰기에서 line-height 는 글자 사이가 아니라 줄(세로 기둥) 사이를 벌린다.

좁은 화면에서는 세로 글자가 자리를 못 얻는다. writing-mode: horizontal-tb 로 눕히되, 그 자리의 그리드 칼럼도 같이 손봐라 — 가로로 누운 긴 문장이 auto 칸을 통째로 먹는다.


7. 라이선스

배포 전에 확인한다. 폰트 라이선스 위반은 조용히 있다가 청구서로 온다.

확인할 것 왜
웹폰트 임베딩이 허용되는가 데스크톱 설치는 되는데 웹 임베딩은 금지인 폰트가 많다
페이지뷰 상한이 있는가 상업 폰트에 흔하다
도메인이 묶여 있는가 스테이징·프리뷰 도메인이 빠질 수 있다
수정·서브셋이 허용되는가 서브셋도 파생물이다
크레딧이 필요한가 CC-BY 계열

한국 무료 폰트의 함정: "상업적 이용 무료"가 곧 "웹폰트 임베딩 무료"는 아니다. 눈누(noonnu.cc)는 허용 범위를 표로 보여주니 웹폰트 항목을 직접 확인해라. SIL OFL 은 임베딩·수정·재배포가 모두 허용되지만 폰트 자체를 판매할 수 없고, 개명 조항이 있는 경우가 있다.

확인 결과를 design.md 에 한 줄로 남긴다: 본문 Pretendard(SIL OFL, 웹 임베딩 가능) / 디스플레이 ___.


8. 배포 전 체크리스트

  • 폰트 패밀리 수와 실제 첫 화면 전송량이 프로젝트 예산 안이다(tokens.md §5)
  • "왜 이 폰트인가"에 한 문장으로 답할 수 있다
  • 필요한 웨이트의 가변/정적 후보를 실제 subset 전송량·지원 범위로 비교했다
  • 없는 웨이트를 브라우저가 합성하고 있지 않다(font-synthesis-weight: none 으로 확인)
  • font-display 를 의식적으로 골랐다
  • preload 한 파일이 첫 화면에 실제로 쓰인다. crossorigin 이 있다
  • 실제 첫 방문·재방문·느린 망에서 폴백 메트릭과 font-display 선택이 의도한 줄바꿈·레이아웃을 유지한다
  • 수치를 나열하는 곳에 tabular-nums 가 있다
  • 한글이 있으면 서브셋을 쓴다. 파일 개수가 아니라 전송량으로 쟀다
  • 라틴 폰트가 폴백 스택 앞에 있다
  • 한글 폰트를 명시했다
  • 웹폰트 임베딩 라이선스를 확인하고 design.md 에 적었다
  • 폰트를 못 받은 상태로 페이지를 열어봤다. 그 상태로도 읽힌다

근거: research/references/03-trends-2026.md §7, 04-ai-slop-signatures.md §8 (조사일 2026-08-20)


9. 위계와 자간·행간 보강

9-1. 자간·행간은 크기의 함수다 — 통합 원칙

이 문서와 tokens.md에 이미 흩어져 있는 개별 규칙은 하나의 원리를 공유한다. 글자가 커질수록 자간은 좁아지고 행간은 타이트해지며, 글자가 작아질수록 자간은 넓어지고 행간은 느슨해진다. 큰 디스플레이 텍스트에서 음수 자간을 후보로 두는 이유(tokens.md §1 "함께 정해야 하는 것")도, 작은 대문자 라벨에 양의 자간을 주는 이유(§5의 all-small-caps 예시)도, 헤딩 행간을 좁히고(1.11.2대) 본문 행간을 넓히는(1.51.6, 한글 1.6~1.8, tokens.md §1) 이유도 같은 함수의 다른 지점일 뿐이다.

이 절은 새 강제 수치를 만들지 않는다. tokens.md·antipatterns.md §2가 이미 정한 값과 "보편 하한을 선언하지 않는다"는 방향은 그대로 유지한다. 여기서 더하는 것은 흩어진 개별 사례를 하나의 원리로 묶어 읽는 프레임뿐이다 [SKILL-BETTER-TYPE].

9-2. 타입을 능동적 디자인 요소로 쓰기 — 관찰 후보

언제: 헤드라인이나 숫자 자체가 화면의 시각적 주인공일 때(히어로 숫자, 잡지형 표지, 데이터 하나를 강조하는 카드).

이런 자리에서는 타입 처리 자체가 콘텐츠를 담기만 하는 중립적 그릇이 아니라 디자인의 능동적인 부분이 될 수 있다[SKILL-FRONTEND-DESIGN]. 예: 큰 사이즈, 의도적 절단(뷰포트 경계 밖으로 흘러나가는 글자), 겹침, 회전, 색 분리 같은 처리는 타이포그래피가 "읽히는" 것을 넘어 그 자체로 이미지가 되게 한다 — 이 나열 자체는 출처 없음(designpaca 확장)이다. 관찰 후보다 — 본문·UI 라벨·폼처럼 기능이 우선인 텍스트에는 적용하지 않는다. 가독성이 과업에 실제로 필요한 자리(§13의 하드 게이트가 도는 자리)에서는 장식이 접근을 이기지 않는다.

9-3. Display와 Text는 다른 폰트다

"Display"라는 이름이 큰 사이즈용 최적화를 보장하지 않는다. 반대도 마찬가지다. SF Pro, Heldane 같은 일부 패밀리는 큰 사이즈용 Display 변형과 작은 사이즈용 Text 변형을 별도 파일로 낸다 — Text 변형은 작은 크기에서도 읽히도록 획이 굵고 속공간이 넓게 그려지고, Display 변형은 큰 크기에서만 의도가 살도록 더 섬세하게 그려진다 [SKILL-BETTER-TYPE]. 가변 폰트라면 font-optical-sizing: auto(§2)가 이 전환을 자동으로 처리하지만, Display/Text가 별도 정적 파일로만 나오는 패밀리는 설정하는 크기에 맞는 변형을 직접 골라야 한다. 헤딩에 Display 파일을, 본문에 Text 파일을 매핑하지 않으면 한쪽에서 항상 맞지 않는 파일을 쓰게 된다.

9-4. 헤딩 위계 — 내림차순과, 본문보다 작아지지 않는 하한 (프로젝트 계약)

프로젝트 계약이다. 타입 스케일을 정했다면(tokens.md §1) 그 스케일에 헤딩 레벨을 매핑하는 기본 규칙이지, 위계가 "실제로 읽힌다"는 것을 증명하는 검사 자체는 아니다. 위계가 실제로 구별되는지는 tokens.md "제목 역할이 실제로 구분되는지 확인한다" 절의 실측 방법(여러 폭에서 계산된 크기를 재는 것)을 그대로 따른다 — 단일 비율 하나로 위계를 증명할 수 있다는 태도는 design-foundations.md §1이 이미 잘못된 일반화로 지목했다.

규칙은 두 가지다 [SKILL-BETTER-TYPE].

  1. 헤딩 레벨은 타입 스케일의 내림차순 단계에 매핑한다. h1이 h2보다, h2가 h3보다 시각적으로 작아서는 안 된다. 스케일 단계가 부족해 인접 레벨이 같은 크기를 공유해야 하면, 굵기나 자간으로 구별을 유지한다.
  2. 헤딩은 본문보다 작아지지 않는다. 예외는 의도적으로 라벨형 오버라인(예: "CASE STUDY"처럼 본문 위에 놓이는 작은 대문자 태그)으로 쓸 때뿐이다. 어떤 시맨틱 요소를 쓸지는 accessibility.md 소관이고, 이 규칙은 고른 요소의 시각적 크기만 다룬다 — <h3>를 골랐다고 브라우저 기본 크기를 그대로 두지 않는다.

10. 폰트 렌더링 제어 보강

10-1. font-synthesis — shorthand 대신 longhand

font-synthesis: none은 굵기·기울임·small-caps·상하첨자 합성을 한 번에 다 끈다. 폴백 스택 전체에서 필수 강조(굵게, 기울임)가 실제 글리프로 구별되는지 검증하기 전에 이 shorthand를 쓰면, 폴백으로 떨어졌을 때 강조가 조용히 사라진다 — 오류도 보고도 없이.

한 모드만 끄고 싶으면 shorthand 대신 개별 속성을 쓴다 [SKILL-BETTER-TYPE].

/* 브랜드 워드마크처럼 합성된 굵기가 눈에 띄게 다를 때만, 검증 후 좁게 적용 */
.brand-wordmark {
  font-synthesis-weight: none;
}

/* shorthand는 폴백 전체의 강조 표현을 확인한 뒤에만 */
.isolated-treatment {
  font-synthesis: none;
}

10-2. OpenType 확장 기능

§5가 이미 다루는 tabular-nums·oldstyle-nums·diagonal-fractions·ordinal·all-small-caps·ss01 외에, 자주 쓰는 나머지 기능이다 [SKILL-BETTER-TYPE].

/* 0과 O를 구별해야 하는 곳 — 일련번호, 인증코드, 주문번호 */
.serial { font-variant-numeric: slashed-zero; }

/* 리가처(fi, fl 합자 등)를 명시적으로 켠다. 대부분 기본 켜짐이지만 폰트가 다르면 확인한다 */
.prose { font-variant-ligatures: common-ligatures; }

/* 위·아래 첨자. 폰트가 실제 첨자 글리프를 담고 있어야 한다 — 없으면 인위적 축소로 대체된다 */
.formula-sup { font-variant-position: super; }  /* x² 의 2 */
.formula-sub { font-variant-position: sub; }    /* H₂O 의 2 */

/* 문자 변형(character variant). 슬롯 번호의 의미는 폰트마다 다르다 */
.logo { font-feature-settings: "cv11" 1; }

ss01ss20(스타일 세트), cv01cv99(문자 변형)는 번호가 폰트마다 다른 의미를 가지므로 쓰기 전에 그 폰트의 OpenType 기능 문서를 확인한다. 표준 속성(font-variant-*)이 있는 기능은 그것을 먼저 쓴다 — font-feature-settings는 표준 속성이 없는 기능에만 쓰는 마지막 수단이라는 §5의 원칙은 여기서도 그대로다.

10-3. 숫자가 흔들리면 안 되는 곳 — tabular-nums 확장

§5는 이미 "수치를 나열하는 곳"(표·대시보드·가격표)에 tabular-nums를 요구한다. 같은 이유가 값이 실시간으로 바뀌는 자리에도 그대로 적용된다 — 타이머, 카운트다운, 실시간 가격, 진행률 퍼센트처럼 숫자가 갱신될 때마다 폭이 바뀌면 주변 레이아웃이 갱신마다 흔들린다 [SKILL-BETTER-TYPE].

/* 정적인 표뿐 아니라, 매초 값이 바뀌는 타이머에도 같은 이유로 필요하다 */
.countdown-digit { font-variant-numeric: tabular-nums; }

11. 줄바꿈과 정렬

11-1. 자동 줄바꿈 선언 — text-wrap:balance / pretty

지원 상태 (확인 2026-09-24) [WEB-BASELINE].

선언 상태 지원 브라우저
text-wrap: balance Baseline Newly available(아직 Widely 아님) Chrome/Chrome Android 114, Edge 114, Firefox/Firefox Android 121, Safari/Safari iOS 17.5
text-wrap: pretty Baseline Limited(Baseline 아님) — Firefox 미구현 Chrome/Chrome Android 117, Edge 117, Safari/Safari iOS 26

두 값 모두 미지원 브라우저에서는 기본 줄바꿈으로 조용히 저하된다(레이아웃이 깨지지 않는다) — 점진적 향상으로 쓴다. Firefox 비중이 큰 프로젝트라면 pretty가 주요 엔진 셋 중 Firefox만 미지원이라는 점을 감안한다.

/* 두 줄 헤딩이 한쪽으로 쏠리지 않게 폭을 맞춘다 */
h1, h2, h3 { text-wrap: balance; }

/* 설명 문단의 위도우를 줄인다. 장문에는 쓰지 않는다(아래 §11-2) */
.lead, .description { text-wrap: pretty; }

balance는 브라우저가 몇 줄을 넘으면 스스로 적용을 포기한다 — 헤딩·소제목처럼 짧은 텍스트에 쓴다. 장문 본문에는 balance도 pretty도 쓰지 않는다. 문단 전체를 고르게 만들려는 시도는 공간을 낭비하고 가독성을 해칠 수 있다 [SKILL-BETTER-TYPE].

11-2. widow · orphan · river

  • widow(과부행): 문단이나 헤드라인의 마지막 줄에 단어 하나만 남는 것.
  • orphan(고아행): 문단의 첫 줄이 혼자 떨어져 남는 것.
  • river(리버): justify 정렬에서 여러 줄에 걸쳐 흰 공백이 세로로 이어져 보이는 것.

세 가지 모두 관찰 후보다. 헤드라인·리드 문단에서는 text-wrap: balance/pretty(§11-1)로 대부분 해소되고, 미지원 브라우저나 예외적인 문구 길이에서는 수동 개행(&nbsp;로 마지막 두 단어를 묶기)으로 보정한다. river는 justify 정렬(§11-4)에서만 나타나므로 인터페이스 본문에 justify를 쓰지 않으면 대부분 발생하지 않는다 [SKILL-DESIGN-REVIEW]. 실제 콘텐츠 길이와 뷰포트 폭으로 렌더해 확인한다 — 코드만 보고는 판정할 수 없다.

11-3. overflow-wrap과 white-space

  • overflow-wrap: anywhere — 긴 단어·URL·ID가 컨테이너를 밀어내지 않게 강제로 끊는다. preflight.md의 배포 전 체크리스트가 이미 이 값을 요구한다. break-word보다 레이아웃 계산에서 더 안전한 값이다.
  • white-space: nowrap — 배지·라벨처럼 줄바꿈되면 의미가 깨지는 짧은 텍스트에 쓴다. 컨테이너보다 라벨이 길어질 가능성이 있으면 text-overflow: ellipsis(§13)를 함께 둔다.

한글 문서에서 word-break가 자소를 끊는 문제와 그 대응은 antipatterns.md §8이 정본이다 — 여기서는 라틴 문자열·URL·ID에 쓰는 overflow-wrap만 다룬다. 둘은 다른 문제를 푼다. overflow-wrap은 끊을 지점이 없는 긴 토큰의 예외 처리이고, 한글 word-break 규칙은 정상적인 어절 단위 줄바꿈 자체를 다룬다.

11-4. justify 사용 제한

관찰 후보다 — antipatterns.md §3의 판정과 같다. text-align: start를 인터페이스 기본으로 하고, justify는 자동 하이픈네이션이 걸린 좁은 에디토리얼 컬럼처럼 특정 레이아웃에서만 검토한다 [SKILL-BETTER-TYPE]. 인터페이스의 나머지 자리(카드 설명, 폼 도움말, 버튼 라벨)에 justify를 쓰면 단어 사이 공백이 불규칙해지고 §11-2의 river가 나타나기 쉽다. 한글은 어절 단위로 줄바꿈되므로 river가 나타나는 양상이 라틴과 다르다 — 실제 렌더로 확인한다.

11-5. 문장부호

한국어 문장부호(따옴표, 줄임표, 붙임표, 값과 단위 사이 공백 처리)의 관례는 product-copy.md §11이 정본이다. 이 문서는 줄바꿈·렌더 규칙만 다루고 문장부호 자체는 다루지 않는다.


12. 밑줄과 트리밍

12-1. 밑줄 메트릭

폰트가 내장한 밑줄 위치·두께를 기본값으로 쓴다.

a {
  text-underline-position: from-font;
  text-decoration-thickness: from-font;
  text-decoration-skip-ink: auto;  /* 하강부(g, y, p)를 밑줄이 가로지르지 않게 */
}

from-font가 원하는 두께를 안 주거나 폰트가 메트릭을 제공하지 않으면 수동으로 조정한다 [SKILL-BETTER-TYPE].

a {
  text-decoration-thickness: 1px;    /* 실측 조정 — 시작값 */
  text-underline-offset: 3px;        /* 실측 조정 — 시작값 */
  text-decoration-skip-ink: auto;
}

관찰 후보 — 실측으로 확인한다. 밑줄에서 애니메이션이 안정적으로 동작하는 속성은 color뿐이라는 관찰이 있다. text-decoration-thickness나 text-underline-offset을 트랜지션에 넣으면 브라우저마다 다르게(또는 전혀) 움직일 수 있다. 두께·오프셋이 변하는 밑줄 애니메이션(왼쪽에서 자라나는 효과 등)이 필요하면 text-decoration을 쓰지 말고 별도 요소(가상 요소 ::after의 transform: scaleX())로 구축하는 대안을 검토한다. 점선 밑줄(text-decoration-style: dotted)은 약어·정의어 같은 부가 정보 힌트의 관례로 쓴다.

12-2. text-box trim — 점진적 향상

지원 상태 (확인 2026-09-24) — 두 추적기가 서로 다르게 보고한다 [WEB-BASELINE]. MDN은 Baseline Newly available(2026년 8월부터)로 표시하지만, webstatus.dev는 여전히 Limited로 표시하며(Chrome/Chrome Android 133, Edge 133, Safari/Safari iOS 18.2, Firefox 지원 정보 없음) 두 출처가 어긋난다. Firefox의 실제 지원 시점은 이 조사 시점 기준 확인되지 않았다.

text-box는 글자 위·아래의 행간 여백(폰트가 내장한 ascender/descender 공간) 중 시각적으로 불필요한 부분을 잘라낸다 — 배지·필처럼 텍스트를 컨테이너에 광학적으로 정확히 맞춰야 하는 좁은 자리에 쓴다.

/* 위아래 모두 트림 */
.badge { text-box: trim-both cap alphabetic; }
/* 위쪽만 (큰 헤딩이 컨테이너 상단에 붙어야 할 때) */
.heading { text-box: trim-start cap; }
/* 아래쪽만 */
.label { text-box: trim-end alphabetic; }

미지원 브라우저에서는 이 선언이 무시되고 기존 여백이 그대로 남는다 — 레이아웃이 깨지지 않으므로 점진적 향상으로 쓴다. 광학 정렬이 하드 게이트인 자리는 없다. 지원 브라우저에서 더 정확해지는 정도로 다룬다.


13. 잘린 텍스트 — 전체 값에 도달하는 수단 (하드 게이트)

하드 게이트다. 정보 접근 자체가 걸린 문제이기 때문이다. 이 규칙은 SKILL.md의 규범·기능 하드 게이트, accessibility.md, critique.md의 에스컬레이션 표와 같은 판정을 공유한다.

절단 자체는 금지가 아니다.

/* 한 줄 절단 */
.truncate {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* 여러 줄 절단 */
.clamp {
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

금지되는 것은 잘린 정보가 어디서도 완전한 형태로 확인될 수 없는 상태다. 잘려나간 값이 의미를 가지면(전체 파일명, 긴 이메일 제목, 잘린 가격 설명), 다음 중 하나로 전체 값에 도달할 수단을 남긴다.

  • 네이티브 title 속성 또는 커스텀 툴팁으로 hover·focus 시 전체 텍스트 노출(키보드 포커스에서도 동작해야 한다 — accessibility.md의 포커스 규칙을 따른다)
  • 클릭·탭으로 여는 확장 뷰(상세 페이지, 모달, 아코디언)
  • aria-label로 스크린리더에 전체 값을 노출(시각 텍스트와 스크린리더가 읽는 텍스트가 달라지는 것이므로 남용하지 않는다)

전체 값에 도달할 수단이 하나도 없는 절단은 통과시키지 않는다. 검증은 실제 콘텐츠 중 가장 긴 값으로 렌더한 뒤, 마우스 없이 키보드만으로 전체 값에 닿을 수 있는지 확인하는 것이다 [SKILL-BETTER-TYPE].


14. 렌더링과 선택 디테일

14-1. 폰트 스무딩은 루트에서 한 번

:root {
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

관찰 후보. macOS에서 글자를 얇게 렌더하는 이 두 속성은 쓸지 말지부터 실제 렌더로 정한다(가는 서체·작은 크기에서는 획이 약해질 수 있다). 쓰기로 했다면 루트에 한 번만 선언한다. 컴포넌트마다 반복해서 적용하면 스무딩 방식이 요소 경계에서 갈라져 보이는 자리가 생긴다 [SKILL-BETTER-TYPE].

14-2. 시스템 폰트 폴백 스택 예시

브랜드 서체를 아직 못 구했거나 로딩 실패에 대비할 때, 또는 브리프가 "네이티브 느낌"을 요구할 때 쓸 수 있는 시작 스택이다 [SKILL-APPLE-DESIGN]. 기본값으로 강요하지 않는다 — 브랜드 서체가 정해졌으면 그 폴백 체인(§4)을 우선한다.

font-family:
  system-ui, -apple-system, "Segoe UI",
  Roboto, "Helvetica Neue", Arial,
  "Apple SD Gothic Neo", "Malgun Gothic",
  sans-serif;

한글이 섞이는 프로젝트는 §6-3의 순서 규칙(라틴 폰트를 먼저, 한글 폰트를 뒤에)이 시스템 스택에도 그대로 적용된다.

14-3. 텍스트 선택 가능성 유지

텍스트는 기본적으로 선택 가능해야 한다. ::selection으로 선택 영역의 배경·글자색을 브랜드에 맞게 바꾸는 것은 괜찮다 — 단, 그 조합으로도 선택된 글자가 읽혀야 한다(대비를 확인한다) [SKILL-BETTER-TYPE].

::selection {
  background: var(--accent);
  color: var(--accent-ink);
}

user-select: none은 드래그·제스처 표면에서만 쓴다 — 정렬 핸들, 캔버스 위 라벨, 스와이프 카드처럼 선택이 제스처와 충돌하는 자리다. 인터페이스 전역에 걸거나, 버튼 라벨이 드래그 중 하이라이트될 가능성만으로 걸지 않는다. 본문·카드 설명·에러 메시지처럼 사용자가 복사하고 싶어할 수 있는 텍스트에 user-select: none이 걸려 있으면 그 자체가 완성도 미달 신호다.

14-4. 장식적 텍스트 기법 — 관찰 후보

언제: 에디토리얼 인트로, 인용구, 잡지형 섹션 오프닝처럼 타이포그래피가 §9-2의 능동적 요소로 쓰이는 자리.

기법 용도
::first-letter 드롭캡
::first-line 첫 줄만 다른 스타일(작은 대문자 등)
-webkit-text-stroke 텍스트 외곽선. 가변 폰트에서는 겹치는 글자 모양이 병합되지 않은 채 보일 수 있다 — 정적 폰트로 확인한다
text-shadow 텍스트 그림자. elevation.md의 그림자 원칙(광원 하나로 통일)과 같은 기준을 따른다

본문·UI 텍스트에는 쓰지 않는다. background-clip: text(그라디언트 텍스트)는 이미 antipatterns.md가 다룬다 [SKILL-BETTER-TYPE].


15. 타이포 마감 전 빠른 점검

§8의 배포 전 체크리스트가 로딩·라이선스를 다룬다면, 이 표는 렌더된 실수를 잡는다. 실제 콘텐츠로 채운 화면에서 확인한다.

발견 조치
합성된 굵기·기울임이 디자인과 다르게 보인다 실제 페이스를 로드한다. 검증된 모드만 font-synthesis-*: none(§10-1)
Display 전용 파일을 작은 본문 크기에 그대로 쓴다 크기에 맞는 Text/Display 변형을 고른다(§9-3)
자식 헤딩이 부모보다 시각적으로 강하다 해당 섹션의 위계를 스케일 내림차순에 다시 매핑한다(§9-4)
헤딩 요소를 시각 크기만 보고 골랐다 시맨틱을 먼저 고르고(accessibility.md) 시각 크기는 CSS로 정한다(§9-4)
문단 마지막 줄에 단어 하나(위도우) text-wrap: pretty(§11-1, 지원 상태 확인) 또는 수동 개행
두 줄 헤딩이 한쪽으로 쏠린다 text-wrap: balance(§11-1)
인터페이스에 justify 정렬이 쓰인다 text-align: start로. justify는 에디토리얼 컬럼에만(§11-4)
밑줄이 하강부를 가로지른다 text-decoration-skip-ink: auto, from-font 메트릭(§12-1)
밑줄 두께·오프셋을 트랜지션에 걸어 애니메이션이 브라우저마다 다르게 움직인다 color만 트랜지션하거나 별도 요소로 구축한다(§12-1)
절단된 텍스트에 전체 값 도달 수단이 없다 §13 — 하드 게이트, 배포 차단
인터페이스 전역에서 텍스트 선택이 막혀 있다 복원하고, 드래그·제스처와 충돌하는 자리에만 남긴다(§14-3)
갱신되는 숫자(타이머·카운터·실시간 가격)의 폭이 매번 바뀐다 tabular-nums(§10-3)

근거: [SKILL-BETTER-TYPE], [SKILL-FRONTEND-DESIGN], [SKILL-DESIGN-REVIEW], [SKILL-APPLE-DESIGN], [WEB-BASELINE](확인 2026-09-24)