- route table: extension route for products with an existing visual language, feedback row, derived state tokens stay local - completion: blocking 0 plus reported unverified; hard-gate surface is the touched scope; delivery report starts with what and how to open - design.md created at step 0 with a skeleton, fingerprint line and change log; product-surface rubric reading; direction delegation states the chosen card and reason - layout: hero families, nav forms, footer forms; art-direction page convergence 6 axes; genre sections; image treatments; placeholder conventions; multi-screen order; Tailwind theme mapping - platform: scroll-driven examples, anchor positioning, canvas OT, three/R3F pins, shadcn style prefixes, Safari 26 notes, subgrid - design-gate: four result states, exit codes, cwd module resolution, --url/--out-dir/--strict, oklch contrast, dead selector word match - Korean font budget split from the Latin baseline
56 KiB
typography — 폰트를 고르고 싣는 법
3단계에서 폰트를 정할 때, 4-1에서 적용할 때 읽는다.
타입 스케일과 간격은 tokens.md 가 다룬다. 이 문서는 어떤 글자를 쓰고 그것을 어떻게 화면에 올리는가만 다룬다.
이 문서를 읽는 법
| 상황 | 읽을 곳 |
|---|---|
| 폰트를 아직 안 골랐다 | §1 선택 절차. 대부분 여기서 끝난다 |
| 웨이트를 두 개 이상 쓴다 | §2 가변 폰트 |
| 로딩이 느리거나 글자가 늦게 뜬다 | §3 로딩 |
| 폰트 로드 전후로 레이아웃이 흔들린다 | §4 폴백 메트릭. 가장 자주 빠뜨리는 곳이다 |
| 숫자가 흔들리거나 자간이 이상하다 | §5 OpenType |
| 한글이 들어간다 | §6. 조판 정본은 §6-5(줄바꿈·행간·자간·강조·measure·혼식). 서체 후보는 §6-6 |
| 배포 전 | §7 라이선스, §8 자기 점검(납품 판정은 preflight.md P7·P8) |
| 헤딩 크기·자간·행간을 어떻게 조정할지 원칙이 필요하다 | §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 가 있는가
브라우저에서 재는 법:
// 미로딩 매칭 폰트를 사용해야 하는지 점검한다. "Body KR"은 §6-6에서 고른 서체 이름으로 바꾼다
document.fonts.check('16px "Body KR"', '가나다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: var(--weight-display); } /* 가변 폰트면 640 같은 비정수 값도 되지만, 토큰으로만 쓴다(tokens.md "웨이트") */
.lead { font-weight: var(--weight-medium); }
/* 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"); /* "woff2-variations"는 구식 문법이다. 필요하면 format(woff2) tech(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('"Body KR"') * 100).toFixed(1) + "%"; // "Body KR" = 고른 한글 서체
Safari는 size-adjust만 적용한다. ascent-override·descent-override·line-gap-override는 Chrome·Firefox에서만 동작하고 Safari 안정판(iOS 포함)은 무시한다(확인 2026-10-06)[WEB-BASELINE]. iPhone에서 폰트 교체 때 행 높이가 튀면 line-height를 단위 없는 비율로 명시했는지 먼저 보고, 남는 이동은 font-display: optional이나 preload로 줄인다. 교체 시점 레이아웃 측정(P8a)은 WebKit에서도 따로 한다.
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 는 표준 속성이 없는 기능에만 쓴다 — 이 속성은 기능별로 합쳐지지 않고 선언 전체가 덮어써지므로, 하위 선택자에서 "ss01" 1만 다시 쓰면 상위에서 켠 다른 기능("tnum" 등)이 조용히 사라진다. 커닝·기본 리가처 같은 폰트 기본 기능은 끄지 않는 한 유지된다.
6. 한글
§6-1~§6-4는 폰트 자체(서브셋·웨이트·혼식·지정), §6-5는 조판 정본, §6-6은 서체 후보다. 한글 조판 규칙은 이 문서 §6-5 하나만 정본이다 — layout.md §3, tokens.md §1, antipatterns.md §8은 요약과 링크만 둔다. 값이 서로 달라 보이면 §6-5를 따르고 다른 쪽을 고친다.
6-1. 서브셋과 로딩
한글 완성형은 11,172자다. 전체 폰트가 클 수 있으므로 subset 전략을 검토한다. 모든 프로젝트가 글자 빈도 기반의 동적 subset을 반드시 써야 하는 것은 아니며, 콘텐츠 범위·cache·운영 복잡도와 실제 전송량으로 결정한다.
쪼개는 단위가 라틴과 다르다. 라틴은 unicode-range 로 블록을 나누면 되지만, 한글은 자주 쓰는 글자가 블록에 흩어져 있다. 그래서 글자 빈도로 나눈 수십 개 조각을 만들고 각각에 unicode-range 를 건다. 브라우저가 페이지에 실제로 쓰인 조각만 받는다.
이 방식을 동적 서브셋이라 부른다. Pretendard 의 dynamic-subset 빌드가 그것이고, 파일이 수십 개로 보이는 이유다. Google Fonts의 한글 패밀리도 같은 방식으로 잘려 나온다.
파일 개수로 폰트 예산을 세지 마라. 한글 웹폰트가 파일 40개인 것은 정상이고 올바른 최적화다. 세야 할 것은 패밀리 수와 실제 전송량이다(
tokens.md§5).
용도별 선택:
| 상황 | 방식 | 주의 |
|---|---|---|
| 본문·UI(문구가 바뀌거나 사용자 입력이 섞임) | 동적 서브셋 | 조각을 하나하나 preload 하지 않는다. 어느 조각이 필요할지 미리 모른다 |
| 디스플레이 서체를 고정 문구(로고타입·히어로 한 줄)에만 씀 | 그 문구의 글자만 담은 정적 서브셋(pyftsubset --text-file=... 등) |
문구가 바뀌면 빠진 글자가 폴백 서체로 섞여 그려진다. 문구 변경 때 서브셋을 다시 만드는 절차를 design.md에 적는다. CMS 문구면 동적 서브셋으로 간다 |
| 고정 범위 서브셋(KS X 1001 완성형 2,350자 + 라틴) | 파일 하나로 단순하게 | 일상 문장 대부분을 덮지만 이름·신조어에서 빠지는 글자가 생긴다. 고유명사·사용자 입력이 많은 화면에는 쓰지 않는다 |
본문 한글 웹폰트는 font-display: swap + 폴백 메트릭 보정(§4)에서 시작한다. 한글 폴백(Apple SD Gothic Neo·맑은 고딕)은 웹폰트와 글자 폭·행 높이가 달라 교체 때 줄바꿈이 바뀔 수 있으므로, 느린 망에서 히어로·내비가 다시 흐르는지 본다.
6-2. 웨이트가 라틴과 다르게 보인다
같은 700 이라도 한글은 획이 많아 더 무겁게 보인다. 라틴 기준으로 고른 웨이트를 한글에 그대로 쓰면 본문이 답답해진다. 한글 본문은 400500, 제목은 600700 정도에서 시작해 눈으로 맞춘다. 이 값들은 tokens.md "웨이트"의 역할 토큰(--weight-body·--weight-medium·--weight-strong, 필요 시 --weight-display)에 담아 쓴다.
한글 폰트에는 x-height 개념이 없다. 대신 글자틀 대비 속공간을 본다. 속공간이 좁은 서체는 작은 크기에서 막힌다.
없는 굵기를 브라우저가 합성(가짜 볼드)하지 않게 실제로 쓰는 웨이트 파일을 싣는다. 확인은 font-synthesis-weight: none을 잠시 걸어 굵기 차이가 사라지는 자리가 있는지 보는 것이다(§10-1).
6-3. 라틴과 섞을 때(혼식)
조건부 규칙이다. 순서는 "라틴 디자인을 어느 서체가 맡는가"로 정한다.
| 조건 | 스택 | 이유 |
|---|---|---|
| 한글 패밀리 하나가 라틴·숫자까지 디자인했고 그 품질을 렌더로 확인했다(예: Pretendard·SUIT·IBM Plex Sans KR) | 그 패밀리 단독 + 폴백 | 한 패밀리 안에서 크기감·굵기·기준선이 맞춰져 있다. 단일 스택은 정당한 선택이다 |
| 라틴·숫자를 별도 서체가 맡는다 | 라틴 서체를 앞에, 한글 서체를 뒤에 | 한글 서체를 앞에 두면 숫자·영문까지 한글 서체의 라틴 글리프로 그려진다 |
/* 라틴 서체가 따로 있을 때. 크기감이 다르면 라틴 쪽 @font-face 에 size-adjust 를 준다.
"Display Latin"·"Body KR"은 고른 서체 이름 자리다 — 후보는 §6-6 */
font-family: "Display Latin", "Body KR", sans-serif;
두 서체의 크기감이 다르면 size-adjust 를 쓴 별도 @font-face 로 맞춘다. §4 와 같은 기법이다. size-adjust는 그 face 전체에 걸리므로 라틴 face에만 준다. 같은 줄에서 라틴 대문자·숫자 높이가 한글 글자 높이보다 눈에 띄게 작거나 크면, 그리고 기준선이 떠 보이면 실제 문장(예: "2026년 iPhone 17 Pro 출시")을 크게 렌더해 비교한다.
6-4. 한글 서체를 지정할지
웹폰트를 쓰지 않고 한글 서체 이름도 적지 않으면 OS마다 다른 기본 한글 서체(맑은 고딕, Apple SD 산돌고딕 Neo, Noto Sans CJK KR 등)로 떨어진다. 서로 크기감·폭이 달라 화면마다 다른 디자인이 된다. 그래서 기본은 한글 서체를 명시하는 것이다.
시스템 서체를 의도적으로 고르는 것(브리프가 "네이티브 느낌"을 요구하거나 전송량 제약이 클 때)은 정당하다. 그때는 OS별로 어느 서체가 그려지는지 스택에 명시하고, 그 결정과 이유를 design.md에 적는다. 미결정과 결정의 차이는 기록에 있다.
/* 웹폰트를 쓸 때 마지막 안전망까지 적는다. "Body KR"은 §6-6에서 고른 서체 이름 자리다 */
font-family: "Body KR", -apple-system, BlinkMacSystemFont,
"Apple SD Gothic Neo", "Noto Sans KR", "Malgun Gothic", sans-serif;
6-5. 한글 조판 — 정본
전제: <html lang="ko">. 외국어 구간에는 lang을 따로 단다. 한자 자형 선택(같은 코드포인트가 한·중·일에서 다르게 그려진다), 줄바꿈 규칙, 아래 :lang(ko) 선택자가 모두 이 값을 따른다.
출발 CSS:
:lang(ko) {
word-break: keep-all; /* 어절(띄어쓰기) 단위로만 줄을 바꾼다 */
overflow-wrap: anywhere; /* 공백 없는 긴 토큰(URL·영문 ID·긴 복합어)만 예외로 끊는다. keep-all과 반드시 짝 */
line-break: strict; /* 넣어도 해는 없다. 한국어 본문에서 효과는 작다(아래 표) */
}
:lang(ko) :is(p, li, dd, blockquote, figcaption) { line-height: 1.7; letter-spacing: 0; }
:lang(ko) :is(h1, h2, h3) { line-height: 1.3; text-wrap: balance; }
/* 한글 이탤릭 금지: UA 기본 이탤릭을 풀고 다른 강조로 바꾼다 */
:root { font-synthesis-style: none; }
:lang(ko) :is(em, i, cite, dfn, var, address) { font-style: normal; }
:lang(ko) em { font-weight: var(--weight-strong); }
/* 밑줄이 받침을 긋지 않게 */
:lang(ko) a { text-underline-position: under; }
| 항목 | 규칙·시작값 | 왜 / 예외 |
|---|---|---|
| 어절 줄바꿈 | word-break: keep-all + overflow-wrap: anywhere. keep-all 단독 금지 |
기본값(normal)은 한글 음절 사이 어디서나 줄을 바꿔 "디자/인"처럼 어절이 끊긴다. keep-all만 두면 공백 없는 긴 토큰이 컨테이너를 뚫는다. anywhere는 min-content 계산에도 반영돼 flex·grid 자식이 줄어들 수 있게 한다. 예외: 자동 폭 표 셀·width: fit-content 상자에서 anywhere 때문에 열이 좁아져 글자 단위로 쪼개지면, 그 범위만 overflow-wrap: break-word로 바꾼다(min-content에 영향을 주지 않는다) |
line-break: strict |
넣되 효과를 기대하지 않는다 | strict와 normal의 차이는 일본어 작은 가나·반복 부호·일부 붙임표 앞 줄바꿈 같은 것이라 한국어 본문에서는 거의 같다. 닫는 괄호·마침표·쉼표가 줄 첫머리로 가는 것은 기본 줄바꿈 규칙(UAX #14)이 이미 막는다 |
word-break: auto-phrase |
한국어에 쓰지 않는다 | Chrome에서 lang="ja"(일본어 문절)에만 동작한다 |
| 하이픈 | hyphens를 한글에 쓰지 않는다 |
한글에는 효과가 없다. 라틴 구간에서만 lang과 함께 쓴다 |
| 본문 크기 | 16px 이상. 캡션·보조 라벨은 13px 이상에서 시작 | 획이 많아 같은 px에서 라틴보다 막힌다. KRDS(정부 디자인 시스템)도 본문 최소 16px을 둔다. 12px 한글은 실제 서체로 확대 없이 읽히는지 확인한 뒤에만 쓴다 |
| 본문 행간 | 긴 읽기 1.7에서 시작, 범위 1.6~1.8. 하한 1.5 | 한글 글리프는 글자틀을 거의 채워 라틴처럼 어센더·디센더가 만든 빈 공간이 없다. 줄 사이 숨은 행간이 만든다. KRDS는 줄 간격 최소 150%를 두고, WCAG 1.4.12도 사용자가 1.5로 바꿔도 깨지지 않기를 요구한다. 1.8을 넘기면 줄이 서로 떨어진 목록처럼 읽히기 시작하므로 실제 문단으로 비교한다 |
| 제목·UI 행간 | 제목 1.2 |
라틴 표제의 1.1을 그대로 쓰면 두 줄 제목에서 윗줄 받침과 아랫줄 초성이 닿는다. 확정 전 두 줄 이상 나오는 가장 긴 제목으로 렌더한다. 행 높이는 4px 격자로 반올림한다(tokens.md §3 수직 리듬) |
| 본문 자간 | 0 ~ -0.01em |
한국 웹에서 흔한 범위다. 자간이 넉넉하게 설계된 고딕은 -0.01em 근처, 명조·부리 본문은 0에서 시작한다. 단위는 반드시 em(px 자간은 크기가 바뀌면 비율이 깨진다) |
| 큰 표제 자간 | 대략 32px 이상 표제에서 -0.02em 시작. 더 좁히면 아래 충돌 검사를 통과해야 한다 |
큰 크기에서는 글자 사이가 벌어져 보여 음수 자간이 흔하다. 그러나 한글은 받침·쉼표·마침표·"ㄹ/ㅌ" 같은 넓은 자소가 옆 글자에 닿는 지점이 라틴보다 빨리 온다. 검사: 실제 문구로 1440·390 폭과 200% 확대 캡처를 2배로 잘라 보고 글리프 외곽이 닿거나 겹치는 쌍이 하나라도 있으면 되돌린다 |
| 양수 자간(라벨) | 한글 라벨에 라틴 대문자식 넓은 자간(0.1em 이상)을 그대로 쓰지 않는다 |
한글은 자소가 모여 한 글자를 이뤄 자간을 벌리면 낱글자로 흩어져 "시 험 구 간"처럼 읽힌다. eyebrow를 한글로 쓰면 자간 대신 크기·굵기·색으로 구분한다 |
| 강조 | 한글 이탤릭 금지. 굵기·색·밑줄·드러냄표(text-emphasis: dot, 지원·렌더 확인 후)로 강조 |
한글 서체는 이탤릭 면이 거의 없다. font-style: italic을 걸면 브라우저가 글자를 기울인 가짜 사체를 만든다. font-synthesis-style: none이면 기울임 대신 그냥 정체로 그려져 강조가 조용히 사라지므로, 위 CSS처럼 em·i·cite 등의 UA 기본 이탤릭을 풀고 다른 강조를 준다. 라틴 구간은 그 라틴 서체에 실제 이탤릭 face가 있을 때만 :lang(en) 등으로 기울인다 |
| 모노 서체 | 한글 글리프가 없는 모노(JetBrains Mono·IBM Plex Mono 등)를 한글이 섞이는 텍스트의 첫 서체로 두지 않는다 | 한글이 폴백 서체로 그려지면서 모노 라벨에 준 자간·폭 설정과 섞여 글자가 흩어져 보인다. 모노는 숫자·코드·라틴 약어에만 쓴다. 숫자 정렬이 목적이면 본문 서체 + font-variant-numeric: tabular-nums. 코드에 한글 주석이 필요하면 한글을 담은 모노(D2Coding·나눔고딕코딩, 둘 다 SIL OFL)를 쓴다. 섞어야 하면 아래 unicode-range 합성 서체로 라틴만 모노에 맡긴다 |
| 밑줄 | :lang(ko) a { text-underline-position: under; }. 그래도 붙으면 text-underline-offset을 em 단위로 실측 조정 |
한글은 기준선 아래로 내려오는 글리프가 많아 기본 밑줄이 받침을 긋는다. under는 글자 아래로 밑줄을 내린다. text-decoration-skip-ink는 한글 받침 사이를 지저분하게 끊을 수 있으니 실제 렌더로 본다 |
| measure | 긴 본문 한 줄 32~40자에서 시작, 단위는 em(--measure: 36em 근처에서 출발해 실제 글자 수를 센다). 카드·사이드바 같은 좁은 컬럼은 20자대까지 허용. ch로 한글 measure를 적지 않는다 |
한글 음절의 폭은 대부분 서체에서 1em 안팎이라 Nem이 한글 N자의 시작 근사가 된다(비례 폭 서체는 조금 좁고, 띄어쓰기는 훨씬 좁다). ch는 "0"의 폭(대개 1em의 절반 남짓)이라 40ch는 한글 20자 남짓이 된다. em은 본문 글자 크기 기준이므로 본문 요소나 그 컨테이너에 선언한다. keep-all에서 measure가 너무 좁으면 오른쪽 들쭉날쭉(rag)이 커진다 |
| 제목 마지막 줄 | text-wrap: balance + keep-all. 표제 마지막 줄에 한 어절만 남기지 않는다(특히 2~3글자) |
balance는 keep-all이 허락한 어절 경계 안에서 줄 길이를 고른다. 미지원 브라우저·예외 길이에서는 마지막 두 어절 사이 공백을 로 묶거나 문구를 고친다. 브랜드 워드마크는 white-space: nowrap. 본문 문단의 위도우는 text-wrap: pretty(지원 범위는 §11-1)로 줄인다. 320·390·1440 폭에서 아래 스크립트로 잰다 |
| 한글·라틴 사이 간격 | 수동 공백이나 margin으로 벌리지 않는다. text-autospace: normal은 기본으로 켜지 않고, 켜려면 엔진별 렌더를 나란히 비교한다 |
세 엔진이 속성을 구현했지만(Chrome 140·Firefox 145·Safari 18.4) 초기값은 모두 no-autospace이고 적용 범위가 다르다. 2026-10-06 렌더에서 Chromium은 한글·라틴 경계에 간격을 넣었고 WebKit은 넣지 않았다 — 켜면 엔진 간 줄바꿈이 달라질 수 있다 [WEB-BASELINE] |
| 숫자·단위 | 표·가격·수치 나열에는 tabular-nums(§5). 숫자와 단위 표기 관례는 product-copy.md §11-1 |
한글 서체의 숫자가 라틴 서체 숫자와 크기감이 다를 수 있다. 혼식(§6-3)에서 함께 확인한다 |
| 사용자 글자 간격 재정의 | WCAG 1.4.12(AA): 행간 1.5, 자간 0.12em, 단어 간격 0.16em, 문단 뒤 2em으로 바꿔도 내용이 잘리거나 겹치지 않는다 | keep-all + 넓어진 자간은 어절을 더 길게 만들어 고정 높이 카드·버튼에서 넘치기 쉽다. 고정 높이 대신 min-height, 줄바꿈 허용으로 둔다 |
모노와 한글을 한 스택에 섞어야 할 때 — unicode-range 합성 서체.
/* "Label Mono"라는 이름 아래 라틴·숫자는 모노, 한글은 본문 고딕이 맡는다 */
@font-face { font-family: "Label Mono"; src: url("/fonts/mono-latin.woff2") format("woff2");
unicode-range: U+0000-00FF, U+2000-206F; }
@font-face { font-family: "Label Mono"; src: url("/fonts/body-hangul.woff2") format("woff2");
unicode-range: U+AC00-D7A3, U+1100-11FF, U+3130-318F; }
.label { font-family: "Label Mono", ui-monospace, monospace; letter-spacing: 0; }
표제 마지막 줄의 어절 수를 잰다.
// 1 이면 마지막 줄에 한 어절만 남은 것이다. 320·390·1440 폭에서 h1·h2에 돌린다
const lastLineWords = (el) => {
const tops = [];
const w = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
for (let n; (n = w.nextNode());) {
for (const m of n.data.matchAll(/\S+/g)) {
const r = document.createRange();
r.setStart(n, m.index); r.setEnd(n, m.index + m[0].length);
const rects = r.getClientRects();
if (rects.length) tops.push(rects[rects.length - 1].top);
}
}
const last = Math.max(...tops);
return tops.filter((t) => t > last - 2).length;
};
[...document.querySelectorAll("h1, h2")].map((h) => [h.textContent.trim().slice(0, 20), lastLineWords(h)]);
배포 전에 한글 조판을 렌더로 확인하는 항목 — 납품 판정은 preflight.md P8h가 이 목록을 가리킨다.
- 가장 긴 제목과 대표 본문 문단을 320·390·1440 폭, 200% 확대에서 렌더했다
- 어절이 중간에서 끊긴 곳, 긴 URL·영문 토큰이 넘친 곳이 없다
- 기울어진 한글(가짜 사체), 모노 라벨에서 흩어진 한글이 없다
- 큰 표제에서 글리프가 서로 닿는 쌍이 없다
- 밑줄이 받침을 긋지 않는다
6-6. 한글 서체 후보 — 기본값을 선택지로 바꾼다
Pretendard + 마루 부리 + JetBrains Mono 조합을 기본값처럼 쓰지 않는다. 이 조합은 지금 이 스킬의 결과물에서 가장 자주 반복된 하우스 룩이다. 이 조합(또는 그중 둘)을 쓰려면 아래 표에서 성격이 다른 후보 두 개 이상과 같은 실제 문구로 proof를 만들어 비교하고, 고른 이유를 design.md에 한 줄로 적는다. 직전 프로젝트와 같은 서체를 고르면 그 이유도 적는다. designpaca로 만든 쇼케이스 21종 실측(2026-10-01)에서 본문이 21종 모두 Pretendard였다. 한글이 섞이는 라벨은 라틴 모노(JetBrains Mono 등)로 조판하지 않는다(§6-5).
후보 풀의 정본은 presets/README.md "한글 서체 후보 풀"이다 — 서체별 배포처와 라이선스 확인일(2026-10-02)이 거기 있고, 프리셋 파일도 그 풀을 인용한다. 아래 표는 역할별 이름 요약이다. 둘이 다르면 presets/README.md를 따른다. 배포 전에는 배포처 원문에서 웹 임베딩·수정(서브셋) 허용을 다시 확인한다(§7). 풀에서 "자체 라이선스"로 적힌 것(G마켓 산스·여기어때 잘난체)은 무료라도 조항이 서체마다 달라 반드시 원문을 본다.
각 칸은 라틴 이름 알파벳순, 그다음 한글 이름 가나다순이다. 순서는 우선순위가 아니다 — 첫 이름을 고르지 않는다. 성격·라이선스·배포처는 정본 풀의 표에서 본다.
| 역할 | 후보 |
|---|---|
| 본문 고딕 | Gothic A1 · Gowun Dodum · IBM Plex Sans KR · LINE Seed Sans KR · Noto Sans KR · Pretendard · Spoqa Han Sans Neo · SUIT · Wanted Sans · 나눔바른고딕 |
| 본문·표제 명조(부리) | Gowun Batang · Hahmlet · Noto Serif KR · 나눔명조 · 리디바탕 · 마루 부리 · 이롭게 바탕 |
| 디스플레이 | Bagel Fat One · Black Han Sans · Do Hyeon · Gasoek One · Jua · Orbit · Paperlogy · Song Myung · G마켓 산스 · 여기어때 잘난체 · 제주명조 |
| 모노(한글 포함) | D2Coding · 나눔고딕코딩 |
KoPub World 바탕·돋움은 웹폰트 임베딩에 한국출판인회의의 별도 승인이 필요하므로 승인 기록이 없으면 웹 후보에서 뺀다.
후보를 고르는 질문은 §1-2와 같다 — 이 글자는 무엇처럼 보여야 하는가. 본문 명조(부리)는 금지가 아니다. 16px 이상 긴 읽기에서 실제 서체로 proof를 만들어 획이 막히지 않는지 보고 채택한다.
6-7. 세로쓰기 — 가로쓰기 자간을 그대로 쓰지 마라
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 에 한 줄로 남긴다: 본문 ___(라이선스, 웹 임베딩 가능 여부, 확인일) / 디스플레이 ___(같은 항목).
8. 구현 중 자기 점검 (납품 판정은 preflight.md P8)
서체를 고르고 조판하는 동안 확인한다. 납품 판정은 preflight.md가 한다 — 폰트 패밀리 수·전송량은 P7, font-display·폴백 메트릭은 P8a, preload·crossorigin은 P8b, 합성 웨이트·이탤릭은 P8c, tabular-nums는 P8d, 서브셋·전송량은 P8e, 라이선스는 P8f, 폰트 없이 열기는 P8g, §6-5 한글 조판 렌더 확인은 P8h.
- "왜 이 폰트인가"에 한 문장으로 답할 수 있다
- 필요한 웨이트의 가변/정적 후보를 실제 subset 전송량·지원 범위로 비교했다
- 혼식 순서를 §6-3 조건으로 정했다(라틴을 별도 서체가 맡으면 앞에, 한글 패밀리 단독이면 그 이유를 기록)
- 한글 서체를 명시했다. 시스템 서체를 쓰면 의도적 선택이라는 기록이
design.md에 있다(§6-4) - Pretendard·마루 부리·JetBrains Mono 조합을 썼다면 다른 후보와 비교한 proof 기록이 있다(§6-6)
근거: 저장소 조사 문서 03-trends-2026 §7, 04-ai-slop-signatures §8 (저장소 전용, 설치본 미포함, 조사일 2026-08-20). 한글 조판 §6-5: KRDS 타이포그래피(본문 최소 16px, 줄 간격 최소 150%), CSS Text 3 line-break, Chrome word-break: auto-phrase(확인 2026-10-02)
9. 위계와 자간·행간 보강
9-1. 자간·행간은 크기의 함수다 — 통합 원칙
이 문서와 tokens.md에 이미 흩어져 있는 개별 규칙은 하나의 원리를 공유한다. 글자가 커질수록 자간은 좁아지고 행간은 타이트해지며, 글자가 작아질수록 자간은 넓어지고 행간은 느슨해진다. 큰 디스플레이 텍스트에서 음수 자간을 후보로 두는 이유(tokens.md §1 "함께 정해야 하는 것")도, 작은 대문자 라벨에 양의 자간을 주는 이유(§5의 all-small-caps 예시)도, 헤딩 행간을 좁히고(1.11.2대) 본문 행간을 넓히는(1.51.6, 한글 1.6~1.8 — 한글 정본은 §6-5) 이유도 같은 함수의 다른 지점일 뿐이다.
이 절은 새 강제 수치를 만들지 않는다. 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].
- 헤딩 레벨은 타입 스케일의 내림차순 단계에 매핑한다. h1이 h2보다, h2가 h3보다 시각적으로 작아서는 안 된다. 스케일 단계가 부족해 인접 레벨이 같은 크기를 공유해야 하면, 굵기나 자간으로 구별을 유지한다.
- 헤딩은 본문보다 작아지지 않는다. 예외는 의도적으로 라벨형 오버라인(예: "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(2024-05-13. Widely는 30개월 뒤) | 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; }
한글 제목은 word-break: keep-all과 함께 쓴다. balance는 keep-all이 허락한 어절 경계 안에서만 줄 길이를 고르므로, keep-all 없이 쓰면 균형을 맞추려고 어절 중간에서 끊을 수 있다(§6-5).
balance는 브라우저가 몇 줄을 넘으면 스스로 적용을 포기한다 — 헤딩·소제목처럼 짧은 텍스트에 쓴다. 장문 본문에는 balance도 pretty도 쓰지 않는다. 문단 전체를 고르게 만들려는 시도는 공간을 낭비하고 가독성을 해칠 수 있다 [SKILL-BETTER-TYPE].
11-2. widow · orphan · river
- widow(과부행): 문단이나 헤드라인의 마지막 줄에 단어 하나만 남는 것.
- orphan(고아행): 문단의 첫 줄이 혼자 떨어져 남는 것.
- river(리버): justify 정렬에서 여러 줄에 걸쳐 흰 공백이 세로로 이어져 보이는 것.
세 가지 모두 관찰 후보다. 헤드라인·리드 문단에서는 text-wrap: balance/pretty(§11-1)로 대부분 해소되고, 미지원 브라우저나 예외적인 문구 길이에서는 수동 개행( 로 마지막 두 단어를 묶기)으로 보정한다. river는 justify 정렬(§11-4)에서만 나타나므로 인터페이스 본문에 justify를 쓰지 않으면 대부분 발생하지 않는다 [SKILL-DESIGN-REVIEW]. 실제 콘텐츠 길이와 뷰포트 폭으로 렌더해 확인한다 — 코드만 보고는 판정할 수 없다.
11-3. overflow-wrap과 white-space
overflow-wrap: anywhere— 긴 단어·URL·ID가 컨테이너를 밀어내지 않게 강제로 끊는다. preflight.md W14가 긴 단어·URL 넘침을 판정한다.break-word와 달리 min-content 크기 계산에도 끊을 자리를 반영해 flex·grid 자식이 줄어들 수 있다. 반대로 자동 폭 표 셀이나fit-content상자는 그 때문에 지나치게 좁아질 수 있으므로, 그런 자리만break-word로 둔다.white-space: nowrap— 배지·라벨처럼 줄바꿈되면 의미가 깨지는 짧은 텍스트에 쓴다. 컨테이너보다 라벨이 길어질 가능성이 있으면text-overflow: ellipsis(§13)를 함께 둔다.
한글 문서에서 word-break가 어절 중간을 끊는 문제와 그 대응(keep-all과 overflow-wrap의 짝)은 §6-5가 정본이다 — 여기서는 라틴 문자열·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(숫자·단위·금액·날짜는 §11-1)이 정본이다. 이 문서는 줄바꿈·렌더 규칙만 다루고 문장부호 자체는 다루지 않는다.
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]. 한글 링크는 text-underline-position: under가 출발점이다 — 기본 위치는 받침을 긋는다(§6-5).
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-10-06) — 세 엔진이 모두 출하했다(Chrome 133, Safari 18.2, 마지막인 Firefox 154 — MDN 호환성 데이터) [WEB-BASELINE]. webstatus.dev는 이 날짜에도 Limited로 표시하지만 Firefox 칸이 빈 데이터 지연이다. 아직 Widely가 아니므로 점진적 향상으로 쓴다.
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;
}
금지되는 것은 잘린 정보가 어디서도 완전한 형태로 확인될 수 없는 상태다. 잘려나간 값이 의미를 가지면(전체 파일명, 긴 이메일 제목, 잘린 가격 설명), 다음 중 하나로 전체 값에 도달할 수단을 남긴다.
- 클릭·탭으로 여는 확장 뷰 — 펼치기 버튼, 상세 페이지 링크, 모달, 아코디언. 마우스·키보드·터치에서 모두 동작하므로 첫 선택이다.
- 포커스 가능한 트리거에 붙인 커스텀 툴팁 — hover와 키보드 focus 양쪽에서 열리고 accessibility.md §9의 1.4.13 세 조건(해제 가능·호버 가능·지속)을 지킨다. 터치에서는 탭으로 열리는지 따로 확인한다.
- 네이티브
title속성은 마우스 보조 수단일 뿐 단독으로는 불충분하다. 키보드 포커스와 터치에서 나타나지 않는다. 1·2 중 하나와 함께 둘 때만 쓴다.
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의 조건부 순서 규칙과 §6-4(시스템 서체를 의도적으로 고르면 기록)를 따른다.
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 자기 점검과 preflight P8이 로딩·라이선스를 다룬다면, 이 표는 렌더된 실수를 잡는다. 실제 콘텐츠로 채운 화면에서 확인한다.
| 발견 | 조치 |
|---|---|
| 합성된 굵기·기울임이 디자인과 다르게 보인다 | 실제 페이스를 로드한다. 검증된 모드만 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) |
| 한글 어절이 중간에서 끊기거나, 긴 URL이 넘친다 | word-break: keep-all + overflow-wrap: anywhere 짝(§6-5) |
| 한글이 기울어져 보인다(가짜 사체) | font-synthesis-style: none + em·i 기울임 해제, 굵기·색으로 강조(§6-5) |
| 모노 라벨의 한글이 낱글자로 흩어진다 | 본문 서체 + tabular-nums, 또는 unicode-range 합성 서체(§6-5) |
| 큰 한글 표제에서 글자·쉼표·받침이 닿는다 | 음수 자간을 되돌린다(§6-5 큰 표제 자간) |
| 한글 표제 마지막 줄에 한 어절만 남는다 | text-wrap: balance + keep-all, 묶기, 문구 수정(§6-5) |
| 한글 본문 한 줄이 20자 남짓이다 | ch로 적은 measure를 em으로 바꾼다(§6-5) |
근거: [SKILL-BETTER-TYPE], [SKILL-FRONTEND-DESIGN], [SKILL-DESIGN-REVIEW], [SKILL-APPLE-DESIGN], [WEB-BASELINE](확인 2026-09-24)