1. 개요
Oaty의 타이포그래피 시스템은 장문 본문과 서비스 UI를 분리한다. 게시글 본문, 표준 고정 페이지 본문, 로컬 미리보기 본문, 관리자 Milkdown/ProseMirror 에디터는 같은 장문 본문 토큰을 공유한다. 반면 헤더, 내비게이션, 카드 목록, 검색, 관리자 사이드바, 폼, 툴바, 글 제목 영역은 별도 UI 타이포그래피를 유지한다.
이 문서는 현재 코드 기준의 동작을 설명한다. 기준 파일은 packages/blog-site-shared/src/styles/tokens.css, packages/blog-site-shared/src/styles/global.css, apps/admin/src/styles/editor-prose.css, apps/admin/src/styles/admin-editor-shell.css, packages/blog-site-shared/src/utils/text-wrap.ts다.
2. 설계 목표
Oaty의 본문 타이포그래피는 다음 목표를 갖는다.
- 공개 렌더링과 관리자 에디터의 문단, 제목, 목록, 인라인 코드, 코드 블럭 리듬을 가능한 한 맞춘다.
- 한국어와 CJK 본문은
word-break: keep-all이나word-break: break-all을 강제하지 않고 브라우저의 기본 CJK 줄바꿈을 따른다. - 긴 URL, 긴 영문 토큰, 코드 조각은 레이아웃을 깨지 않도록 예외적으로 줄바꿈 또는 가로 스크롤을 허용한다.
- HTML의 의미 구조는 유지하고, CSS는 시각 표현만 담당한다.
- rem, em, semantic token, container query를 사용해 브라우저 확대 설정과 다양한 컨테이너 폭에 대응한다.
- 검색엔진과 보조 기술이 문서 구조를 이해할 수 있도록 h1, h2, h3 계층을 보존한다.
3. 타이포그래피 계층
Oaty의 타이포그래피는 다음 계층으로 구분한다.
- 전역 UI 타이포그래피
body, 내비게이션, 카드, 검색, 관리자 패널 UI가 사용하는 인터페이스 글자 체계다. 장문 본문 기준과 분리한다. - 공개 본문
.prose
게시글 본문, 표준 고정 페이지 본문, 로컬 미리보기 본문에 적용되는 장문 독서 기준이다. - 관리자 에디터 본문
글 에디터와 고정 페이지 에디터의.ProseMirror가 사용하는 기준이다. Milkdown/Crepe가 만드는 DOM 구조 때문에 공개 본문과 완전히 같은 선택자를 쓰지는 않지만, 같은 토큰을 우선 사용한다. - 레이아웃별 공개 예외
About 페이지의 markdown 섹션은.prose를 사용하지만,AboutLayout에서 H2 섹션 그리드와 H2 크기를 별도로 조정한다. FAQ의 H2는 본문 H2 토큰을 따르지만, FAQ 카드 내부 질문·답변은 보조 UI 타이포그래피다. - OG/썸네일 텍스트 래핑
SVG/이미지 생성 경로는 DOM CSS가 아니라segmentTextForTitleWrap()유틸로 줄을 나눈다. 공개 본문 CSS와 목적은 다르며, 제목 표면에서는 한국어 어절과 공백을 우선 보존한다.
4. 본문 기준값
장문 본문 토큰은 tokens.css에서 관리한다.
| 항목 | 기본/넓은 컨테이너 | 좁은 컨테이너 |
|---|---|---|
| 본문 | 17px / 1.65 | 17px / 1.65 |
| H2 | 30px / 1.4 | 26px / 1.35 |
| H3 | 23px / 1.45 | 21px / 1.4 |
| 문단 간격 | 1em | 1em |
| H2 섹션 상단 간격 | 48px | 48px |
| H2 하단 간격 | 24px | 20px |
| H3 상단 간격 | 32px | 32px |
| H3 하단 간격 | 16px | 14px |
| 코드 블럭 내부 패딩 | block 24px / inline 24px | block 24px / inline 24px |
본문 크기는 CSS상 1.0625rem이다. 기본 브라우저 글꼴 크기 16px 기준 17px에 해당하며, 사용자의 브라우저 확대 및 기본 글꼴 크기 설정과 함께 확장된다. 줄간격은 단위 없는 1.65를 사용한다.
코드 블럭 패딩은 --prose-code-padding-block과 --prose-code-padding-inline로 관리하며 현재 둘 다 var(--space-6), 즉 24px이다. 최신 코드에는 모바일 전용 코드 패딩 토큰이나 모바일 full-bleed 코드 블럭 규칙이 없다.
5. 적용 범위
다음 영역은 장문 본문 기준을 따른다.
- 게시글 본문:
PostLayout.astro의.post-content.prose - 표준 고정 페이지 본문:
PageLayout.astro의.standard-page-content.prose - 글 로컬 미리보기 본문:
preview-local.astro의.post-content.prose - 페이지 로컬 미리보기 본문:
page-preview-local.astro의.preview-body.prose - 관리자 글 에디터 본문:
.editor-prose-surface .ProseMirror - 관리자 고정 페이지 에디터 본문:
.editor-prose-surface .ProseMirror - 공개 글과 글 로컬 미리보기의 FAQ H2:
.post-faq .faq-heading
다음 영역은 본문 기준으로 강제하지 않는다.
- 전역
body - 헤더와 내비게이션
- 카드 목록과 archive UI
- 검색 다이얼로그
- 관리자 사이드바와 폼 UI
- 글 제목, 페이지 제목, 설명문, 날짜, 태그 칩
- 에디터 제목 입력 영역, 툴바, 버튼, 입력 필드, 상태 표시 UI
- About 페이지의 프로필, 경력, 포트폴리오 카드
전역 body를 장문 본문 기준으로 바꾸면 관리자 UI, 카드 밀도, 검색 UI, 내비게이션의 균형이 함께 변하므로 금지한다.
6. 공개 본문 동작
공개 본문은 .prose에서 다음 기준을 사용한다.
max-width: var(--content-width)로 본문 폭을 680px 기준에 맞춘다.container-type: inline-size를 설정해 H2/H3와 FAQ H2의 compact 전환을 viewport가 아니라 본문 컨테이너 폭 기준으로 처리한다.- 문단과 목록은
17px / 1.65,letter-spacing: 0,font-weight: 400을 사용한다. - H2/H3는 토큰으로 크기와 줄간격을 지정한다.
- markdown의 H2부터 다음 H2 전까지는
rehype-sectionize가.content-section으로 감싼다. .content-section에는content-visibility: auto와contain-intrinsic-size가 적용된다. 콘텐츠가 DOM에서 제거되는 것은 아니며, 의미 구조와 검색 접근성을 유지한다.- 표는
rehype-wrap-tables가 빌드 시점에.table-wrapper로 감싸며, 좁은 화면에서는 wrapper가 가로 스크롤을 처리한다.
About 페이지의 markdown 영역은 .prose를 포함하지만 공개 본문과 완전히 같은 레이아웃은 아니다. AboutLayout.astro는 .content-section을 2열 그리드로 바꾸고, About markdown H2를 더 작은 섹션 라벨처럼 보이도록 별도 스타일을 적용한다. 따라서 “About markdown은 본문 토큰을 공유하지만 H2 섹션 표현은 About 전용”이라고 보는 것이 정확하다.
7. 관리자 에디터 동작
관리자 글 에디터와 고정 페이지 에디터는 /admin-assets/editor/editor.css를 로드한다. 이 번들은 Milkdown/Crepe CSS, editor-prose.css, admin-editor-shell.css를 포함한다.
본문 입력 영역은 .editor-prose-surface .ProseMirror가 담당한다.
- 문단, H2, H3, 목록, 인라인 코드, 이미지, blockquote, 표,
pre는 공개.prose와 같은 본문 토큰을 우선 사용한다. - H2는
margin-block: 48px 24px, compact 컨테이너에서는 하단 간격 20px과 26px 크기로 전환된다. - H3는
margin-block: 32px 16px, compact 컨테이너에서는 하단 간격 14px과 21px 크기로 전환된다. - Milkdown 목록은 일반
<li>외에.milkdown-list-item-block구조를 만들 수 있다. 그래서 번호, 불릿, 체크박스 라벨은 첫 줄 높이와 맞도록 별도 flex 정렬을 둔다. - 관리자 에디터의 코드 블럭은 Milkdown/CodeMirror DOM을 포함할 수 있다.
prefallback은 공개 본문과 같은 코드 패딩 토큰을 사용하고,.milkdown-code-block은 별도 block margin을 갖는다.
에디터 문서 표면의 여백은 본문 토큰이 아니라 admin-editor-shell.css에서 관리한다. 데스크톱에서는 --paper-px: calc(clamp(2rem, 5vw, 4.5rem) + 15px)를 사용하고, 768px 이하에서는 --paper-px: 24px, .ProseMirror padding은 20px 24px이 된다. 이 값은 코드 블럭 내부 패딩과 별개의 문서 표면 패딩이다.
8. 한국어/CJK 줄바꿈 기준
현재 코드의 핵심은 장문 본문과 제목 표면의 줄바꿈 정책을 분리하는 것이다.
공개 본문 .prose, 관리자 .ProseMirror, 전역 body는 다음 방향을 따른다.
word-break: normaloverflow-wrap: break-word.prose와.ProseMirror는line-break: auto
이 조합은 한국어와 CJK 텍스트를 브라우저 기본 줄바꿈 알고리즘에 맡긴다. 한국어 어절을 무조건 붙잡는 word-break: keep-all을 쓰지 않으므로, 좁은 화면에서 긴 한국어 문장이나 본문이 부모 폭을 밀어내는 위험을 줄인다. 동시에 break-all도 쓰지 않아 라틴 텍스트 전체가 무분별하게 쪼개지는 동작을 피한다.
overflow-wrap: break-word는 긴 URL, 긴 영문 문자열, 긴 인라인 코드처럼 공백 없는 토큰이 컨테이너를 넘칠 때만 예외적인 줄바꿈 기회를 제공한다. 본문 전체에 overflow-wrap: anywhere를 쓰지 않는 것도 같은 이유다.
포스트 카드 제목은 제목 표면으로 취급한다. word-break: keep-all, overflow-wrap: break-word, text-wrap: balance를 사용해 한국어 어절 중간 줄바꿈을 줄이고, 짧은 제목 블록의 줄 길이를 더 균형 있게 맞춘다. 긴 영어 토큰이나 URL처럼 공백 없는 문자열은 overflow-wrap: break-word가 최후의 안전장치로 처리한다.
OG 이미지와 관리자 OG 생성 유틸은 CSS가 아니라 text-wrap.ts의 segmentTextForTitleWrap()을 사용한다. 이 유틸은 한국어 제목의 어절과 공백을 우선 보존하고, 실제 SVG/Canvas 래핑 단계에서 한 줄 폭을 넘는 긴 토큰만 grapheme 단위로 나눈다. 기존 segmentTextForCjkWrap()은 CJK grapheme 단위 래핑이 필요한 경로를 위해 남겨둔다.
9. 코드 블럭, 표, 미디어, 목록
코드 블럭은 본문 폭 안에 머물고 긴 줄은 가로 스크롤로 처리하는 것이 원칙이다. 공개 본문 .prose pre는 overflow-x: auto, border-radius: var(--radius-md), margin-block: var(--space-6), padding-block/inline: var(--prose-code-padding-*)를 사용한다.
최근 모바일 코드 블럭 실험에서 사용했던 --prose-code-padding-inline-mobile, margin-inline: calc(-1 * var(--page-gutter)), 모바일 border-radius: 0 규칙은 최신 코드에서 제거됐다. 따라서 모바일 공개 본문 코드 블럭은 더 이상 page gutter까지 full-bleed로 확장하지 않는다.
관리자 에디터의 pre도 같은 코드 패딩 토큰을 사용한다. 다만 Milkdown 코드 블럭은 CodeMirror 기반 DOM을 만들 수 있어 공개 Shiki 코드 블럭과 내부 DOM이 다르다. 문서 리듬은 맞추되, 구현 선택자까지 동일하다고 가정하지 않는다.
표는 .table-wrapper가 가로 스크롤을 처리한다. 테이블 셀은 white-space: nowrap을 사용하므로 작은 화면에서 셀 내부 텍스트를 억지로 줄바꿈하지 않는다.
이미지는 본문 흐름에서 독립된 블록으로 취급한다. 공개 본문 이미지는 content-visibility: auto와 contain-intrinsic-size를 사용하고, 관리자 에디터 이미지는 같은 여백과 radius를 쓰되 편집 DOM에 맞춘 별도 규칙을 사용한다.
목록은 문단과 같은 본문 크기와 줄간격을 사용한다. 중첩 목록은 들여쓰기와 마커 체계를 단계적으로 구분한다. 관리자 Milkdown 목록은 커스텀 라벨 구조를 별도 보정한다.
10. SEO와 접근성 원칙
Oaty의 타이포그래피는 HTML 의미 구조를 변경하지 않는다.
- h1은 글 또는 페이지의 대표 제목에 사용한다.
- 본문 섹션은 h2를 기준으로 나눈다.
- 세부 항목은 h3를 사용한다.
- TOC는 H2/H3를 추적한다.
- CSS는 시각적 크기와 간격을 조정하지만 제목 계층 자체를 바꾸지 않는다.
WHATWG HTML 표준은 heading이 문서 outline과 목차 생성에 사용될 수 있음을 설명한다. Google Search Central도 검색엔진이 페이지를 이해할 수 있도록 semantic HTML과 DOM에 접근 가능한 텍스트를 사용할 것을 권장한다. 따라서 Oaty의 본문 디자인은 시각적으로만 큰 제목보다 구조적으로 올바른 제목을 우선한다.
줄간격은 WCAG의 텍스트 간격 원칙을 고려해 본문 기본값을 1.65로 둔다. 사용자가 브라우저나 보조 도구로 텍스트 간격을 늘려도 콘텐츠가 잘리거나 겹치지 않도록 레이아웃은 유연하게 유지해야 한다.
11. 예외와 금지사항
다음 변경은 금지한다.
- 전역
body를 본문 기준으로 강제 변경하지 않는다. - 본문 작성자가 H1을 반복 사용하지 않는다.
- 공개 본문과 관리자 에디터 본문 토큰을 임의로 갈라놓지 않는다.
- 본문 H2/H3 크기를 개별 페이지에서 임의 px 값으로 덮어쓰지 않는다. 단, About처럼 레이아웃 자체가 다른 경우는 해당 레이아웃 파일에 명시적으로 예외를 둔다.
- 문단 간격을 고정 px로 흩뿌리지 않는다.
- 제목 크기를 키우기 위해 HTML heading level을 바꾸지 않는다.
- 줄바꿈 문제를 해결하기 위해
word-break: break-all을 본문 전체에 적용하지 않는다. - 한국어 본문 전체에
word-break: keep-all을 강제 적용해 좁은 화면 overflow를 되살리지 않는다. - 카드/썸네일 제목의 줄바꿈 정책을 장문 본문 정책과 무조건 같게 만들지 않는다.
- 본문 전체에
overflow-wrap: anywhere를 무분별하게 적용하지 않는다. - 표, 코드, 긴 URL 때문에 본문 컨테이너가 가로로 밀려나게 두지 않는다.
- 모바일 코드 블럭을 full-bleed로 확장하는 규칙을 되살릴 경우 공개 본문과 관리자 에디터의 실제 여백 차이를 먼저 검증한다.
12. 점검 체크리스트
배포 전 다음 항목을 확인한다.
- 본문 문단이 17px / 1.65 기준으로 보이는가?
- 공개 본문과 관리자 에디터의 H2/H3 크기와 간격이 토큰 기준으로 일치하는가?
- H2는 본문 섹션의 시작으로 충분히 구분되는가?
- H3는 H2보다 작지만 세부 제목으로 명확한가?
- 좁은 컨테이너에서 H2는 26px, H3는 21px 기준으로 전환되는가?
- About markdown H2가 일반 본문 H2와 다른 레이아웃 예외임을 의도적으로 유지하고 있는가?
- 본문 안에 불필요한 H1이 없는가?
- TOC가 H2/H3를 정상적으로 추적하는가?
- 한국어/CJK 본문이
keep-all로 인해 모바일에서 부모 폭을 밀어내지 않는가? - 카드 제목이
keep-all과text-wrap: balance적용 후에도 좁은 화면에서 overflow 없이 읽히는가? - 긴 URL, 긴 영문 단어, 인라인 코드 조각이 본문 폭을 깨지 않는가?
- 목록 번호, 불릿, 체크박스가 첫 줄 기준으로 정렬되는가?
- 공개 코드 블럭이 본문 폭 안에서 24px 내부 패딩과 가로 overflow를 안전하게 처리하는가?
- 관리자 코드 블럭이 공개 본문과 같은 코드 패딩 토큰을 사용하는가?
- 표가
.table-wrapper안에서 가로 overflow를 처리하는가? - 이미지 주변 여백이 문단 리듬을 해치지 않는가?
- 관리자 UI, 카드, 검색, 내비게이션 글자 크기가 본문 개편 때문에 변하지 않았는가?
- OG 이미지 텍스트 래핑 변경 시
segmentTextForTitleWrap()의 어절 우선 규칙과 긴 토큰 fallback이 깨지지 않았는가?
출처
- WHATWG HTML Standard: Headings and outlines
- WHATWG HTML Standard: The section element
- CSS Values and Units Module Level 4: Relative lengths
- CSS Text Module Level 4: Word breaking and wrapping
- CSS Containment Module Level 2: content-visibility
- W3C WCAG 2.2: Text Spacing
- MDN: line-height
- MDN: CSS custom properties
- MDN: CSS container queries
- Google Search Central: SEO Starter Guide
- Google Search Central: Get started with Search, developer guide
- Google Search Central: Influencing title links