Box-Drawing Characters와 CJK 모노스페이스 렌더링
TL;DR
- Box-Drawing 문자는 유니코드이며, CJK 폴백 폰트의 폭 비율이 달라지면 웹에서 정렬이 깨진다.
- 터미널은 wcwidth로 셀을 강제하지만, 브라우저는 폰트의 advance width를 그대로 따른다.
- 2:1 폭을 내장한 duospaced 폰트(D2Coding 등)를 쓰면 정렬 문제가 해결된다.
1. 개념
Box-Drawing Characters는 테두리·표를 그리기 위한 유니코드 블록(U+2500~U+257F)이며, CJK 문자를 포함한 모노스페이스 환경에서 2:1 폭 비율이 유지되어야 다이어그램이 깨지지 않는다. 이 비율을 폰트 자체에 내장한 것이 duospaced 폰트다.
2. 배경
전통적인 터미널은 wcwidth 기준으로 셀을 강제 배치해 CJK는 2셀, Box-Drawing은 1셀로 고정한다. 하지만 브라우저는 폰트의 메트릭(advance width)만 보고 렌더링하므로 폰트 폴백이 발생하면 폭 비율이 깨질 수 있다.
3. 이유
웹 코드 블록에서 Box-Drawing과 한글이 섞이면, Latin 폰트와 CJK 폰트의 폭 비율이 2:1이 아닐 때 테두리 정렬이 붕괴한다. 브라우저에는 터미널처럼 셀 강제 메커니즘이 없기 때문이다.
4. 특징
- Box-Drawing의 East Asian Width는 Ambiguous로 분류되며, 렌더링 맥락에 따라 폭이 달라질 수 있다.
- 폴백 폰트의 UPM/advance width 차이가 2:1 비율을 무너뜨린다.
- D2Coding 같은 duospaced 폰트는 Latin/CJK/Box-Drawing을 동일 폰트에 담아 2:1을 보장한다.
5. 상세 내용
Box-Drawing Characters와 CJK 모노스페이스 렌더링
작성일: 2026-02-25 카테고리: Frontend / Typography / Unicode 포함 내용: Box-Drawing Characters, CJK, East Asian Width, Monospace, Duospaced Font, advance width, wcwidth, D2Coding
1. 용어 정의: “ASCII 다이어그램”의 정확한 명칭
┌─────────────────────────────────────────────────────────┐
│ "ASCII Art"는 기술적으로 부정확한 표현이다 │
│ │
│ 이유: │
│ ├── ASCII = 7-bit 인코딩 (U+0000~U+007F, 128자) │
│ ├── 이 범위에 선/테두리 문자는 존재하지 않음 │
│ └── ┌ ─ │ ┐ 등은 모두 유니코드 문자 │
│ │
│ 정확한 명칭: │
│ ┌───────────────────────────────────────────────────┐ │
│ │ Box-Drawing Characters │ │
│ │ (유니코드 블록 U+2500~U+257F, 128자) │ │
│ │ │ │
│ │ "ASCII art"가 아니라 │
│ │ "Box-Drawing diagram"이 정확한 용어 │
│ └───────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
용어 체계
┌─────────────────────────────────────────────────────────┐
│ Semigraphics / Pseudographics │
│ (문자로 그래픽을 흉내내는 모든 기법의 총칭) │
│ │
│ Semigraphics (세미그래픽스) │
│ = Pseudographics (슈도그래픽스) │
│ │ │
│ ├── Box-Drawing Characters (U+2500~U+257F) │
│ │ = Line-Drawing Characters (동의어) │
│ │ 선과 모서리로 테두리/표를 그리는 128개 문자 │
│ │ │
│ └── Block Elements (U+2580~U+259F) │
│ 블록/음영으로 면적을 채우는 32개 문자 │
│ 예: ▀ ▄ █ ▌ ▐ ░ ▒ ▓ │
│ │
│ 두 블록 모두 유니코드 1.1 (1993년)에서 도입 │
│ CP437 등 레거시 코드 페이지에서 유니코드로 통합됨 │
│ │
└─────────────────────────────────────────────────────────┘
공식 명명 규칙
┌─────────────────────────────────────────────────────────┐
│ Box-Drawing 문자의 유니코드 공식 명명 규칙 │
│ │
│ 형식: "BOX DRAWINGS [WEIGHT] [DIRECTION(S)]" │
│ │
│ Weight (선 굵기): │
│ ├── LIGHT: 가는 선 (기본) │
│ ├── HEAVY: 굵은 선 │
│ └── DOUBLE: 이중 선 │
│ │
│ Direction (방향): │
│ ├── HORIZONTAL (수평: ─) │
│ ├── VERTICAL (수직: │) │
│ ├── DOWN AND RIGHT (아래+오른쪽: ┌) │
│ ├── DOWN AND LEFT (아래+왼쪽: ┐) │
│ └── ... 등 방향 조합 │
│ │
│ 예시: │
│ ┌───────────────────────────────────────────────────┐ │
│ │ U+2500: BOX DRAWINGS LIGHT HORIZONTAL → ─ │ │
│ │ U+2501: BOX DRAWINGS HEAVY HORIZONTAL → ━ │ │
│ │ U+2502: BOX DRAWINGS LIGHT VERTICAL → │ │ │
│ │ U+250C: BOX DRAWINGS LIGHT DOWN AND RIGHT → ┌ │ │
│ │ U+2550: BOX DRAWINGS DOUBLE HORIZONTAL → ═ │ │
│ │ U+253C: BOX DRAWINGS LIGHT VERTICAL → ┼ │ │
│ │ AND HORIZONTAL │ │
│ └───────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
2. Unicode Box Drawing 블록 상세
┌─────────────────────────────────────────────────────────┐
│ Unicode Box Drawing 블록 (U+2500~U+257F) │
│ │
│ 도입: Unicode 1.1 (1993년 6월) │
│ 범위: U+2500 ~ U+257F │
│ 문자 수: 128개 │
│ 출처: IBM PC CP437, DEC VT100 등 레거시 코드 페이지 │
│ │
│ Weight별 분류: │
│ ┌───────────────────────────────────────────────────┐ │
│ │ Light (가는 선) │ │
│ │ ─ │ ┌ ┐ └ ┘ ├ ┤ ┬ ┴ ┼ │ │
│ │ │ │
│ │ Heavy (굵은 선) │ │
│ │ ━ ┃ ┏ ┓ ┗ ┛ ┣ ┫ ┳ ┻ ╋ │ │
│ │ │ │
│ │ Double (이중 선) │ │
│ │ ═ ║ ╔ ╗ ╚ ╝ ╠ ╣ ╦ ╩ ╬ │ │
│ │ │ │
│ │ Mixed (혼합: Light+Heavy, Light+Double 등) │ │
│ │ ┍ ┎ ┑ ┒ ┕ ┖ ┙ ┚ ╒ ╓ ╕ ╖ ╘ ╙ ╛ ╜ │ │
│ └───────────────────────────────────────────────────┘ │
│ │
│ Dash/Round 변형: │
│ ┌───────────────────────────────────────────────────┐ │
│ │ Dashed (점선) │ │
│ │ ┄ ┅ ┆ ┇ ┈ ┉ ┊ ┋ │ │
│ │ │
│ │ Arc / Rounded (둥근 모서리, Unicode 6.x 추가) │ │
│ │ ╭ ╮ ╯ ╰ │ │
│ │ │
│ │ Diagonal (대각선) │
│ │ ╱ ╲ ╳ │
│ └───────────────────────────────────────────────────┘ │
│ │
│ 관련 블록: Block Elements (U+2580~U+259F, 32자) │
│ ▀ ▁ ▂ ▃ ▄ ▅ ▆ ▇ █ ▉ ▊ ▋ ▌ ▍ ▎ ▏ │
│ ▐ ░ ▒ ▓ ▔ ▕ ▖ ▗ ▘ ▙ ▚ ▛ ▜ ▝ ▞ ▟ │
│ │
└─────────────────────────────────────────────────────────┘
3. 문제 현상: 웹에서 Box-Drawing 다이어그램이 깨지는 이유
┌─────────────────────────────────────────────────────────┐
│ 핵심 현상: 같은 텍스트, 다른 렌더링 결과 │
│ │
│ 터미널/에디터에서: │
│ ┌───────────────────────────────────────────────────┐ │
│ │ ┌──────────┐ │ │
│ │ │ 한글 │ ← 테두리 완벽 정렬 │ │
│ │ │ English │ │
│ │ └──────────┘ │ │
│ └───────────────────────────────────────────────────┘ │
│ │
│ 웹 브라우저에서 (CJK 폰트 폴백 시): │
│ ┌───────────────────────────────────────────────────┐ │
│ │ ┌──────────┐ │ │
│ │ │ 한글 │ ← 한글이 2칸 폭을 차지 못하거나 │ │
│ │ │ English │ 초과하여 테두리 어긋남 │ │
│ │ └──────────┘ │ │
│ └───────────────────────────────────────────────────┘ │
│ │
│ 원인 요약: │
│ ├── 터미널: wcwidth() 기반 셀 강제 배치 │
│ │ → 한글 = 2셀, ASCII = 1셀, Box-Drawing = 1셀 │
│ │ → 항상 정확한 2:1 비율 │
│ │ │
│ └── 브라우저: 폰트 파일의 advance width 기반 렌더링 │
│ → 폰트마다 글리프 폭이 다름 │
│ → CJK 폴백 폰트의 폭이 Latin 폰트의 정확히 │
│ 2배가 아니면 정렬 무너짐 │
│ │
└─────────────────────────────────────────────────────────┘
문제 시나리오 상세
┌─────────────────────────────────────────────────────────┐
│ 왜 웹에서만 깨지는가? - 구체적 시나리오 │
│ │
│ 상황: │
│ CSS에 font-family: "Fira Code", monospace 지정 │
│ → 코드 블록에서 Box-Drawing 다이어그램 표시 │
│ │
│ 브라우저의 글리프 탐색 순서: │
│ ┌───────────────────────────────────────────────────┐ │
│ │ 1. "─" (U+2500) → Fira Code에 있음 → 사용 │ │
│ │ advance width = 600 units │ │
│ │ │
│ │ 2. "A" (U+0041) → Fira Code에 있음 → 사용 │ │
│ │ advance width = 600 units │ │
│ │ │
│ │ 3. "한" (U+D55C) → Fira Code에 없음! │
│ │ → 시스템 폴백: "Malgun Gothic" 사용 │
│ │ advance width = 1000 units (UPM 2048 기준) │ │
│ │ │
│ │ 문제: 1000/600 = 1.667배 (2.0배가 아님!) │
│ │ → 한글이 ASCII의 정확히 2배가 아니므로 │
│ │ Box-Drawing 선의 수직 정렬이 무너진다 │
│ └───────────────────────────────────────────────────┘ │
│ │
│ 시각적 비교: │
│ ┌───────────────────────────────────────────────────┐ │
│ │ 기대값 (정확한 2:1): │
│ │ [─][─][─][─][─][─] 6셀 = 6 * 600 = 3600 │ │
│ │ [한 ][글 ][!] 3셀 = 2*1200 + 600 = 3000 │ │
│ │ → 불일치 (3600 != 3000) │ │
│ │ │
│ │ 실제 (폴백 폰트): │
│ │ [─][─][─][─][─][─] = 6 * 600 = 3600px │ │
│ │ [한 ][글 ][!] = 2*1000 + 600 = 2600px │ │
│ │ → 큰 불일치! │
│ └───────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
4. East Asian Width (UAX #11): 유니코드 문자 폭 속성
┌─────────────────────────────────────────────────────────┐
│ East Asian Width = 유니코드 기술 보고서 #11 │
│ (Unicode Standard Annex #11) │
│ │
│ 목적: │
│ 유니코드 문자마다 "동아시아 맥락에서의 폭"을 │
│ 속성(property)으로 정의한다 │
│ │
│ 배경: │
│ 동아시아 텍스트 처리에서 "이 문자가 1칸인가 2칸인가" │
│ 를 판단하기 위한 표준 기준이 필요했다 │
│ │
│ 적용 분야: │
│ ├── 터미널 에뮬레이터 (커서 위치 계산) │
│ ├── 고정폭 텍스트 정렬 │
│ ├── 텍스트 줄바꿈 알고리즘 │
│ └── CJK 혼합 텍스트 레이아웃 │
│ │
└─────────────────────────────────────────────────────────┘
6가지 속성값
┌─────────────────────────────────────────────────────────┐
│ East Asian Width의 6가지 속성값 │
│ │
│ ┌────────┬────────────────────────────────────────┐ │
│ │ 속성 │ 설명 │ │
│ ├────────┼────────────────────────────────────────┤ │
│ │ W │ Wide (넓은 문자) │ │
│ │ (Wide) │ 본질적으로 넓은 문자 │ │
│ │ │ 고정폭 환경에서 2셀 차지 │ │
│ │ │ 예: 한자, 한글 음절 (가~힣), │ │
│ │ │ 일본 가나, 일부 기호 │ │
│ ├────────┼────────────────────────────────────────┤ │
│ │ F │ Fullwidth (전각) │ │
│ │ (Full) │ 좁은 문자의 전각(全角) 호환 변환 │ │
│ │ │ U+FF00~U+FFEF 범위 │ │
│ │ │ 예: A (U+FF21), 1 (U+FF11) │ │
│ │ │ 원래 좁은 문자를 인위적으로 2셀로 확장 │ │
│ ├────────┼────────────────────────────────────────┤ │
│ │ H │ Halfwidth (반각) │ │
│ │ (Half) │ 넓은 문자의 반각(半角) 호환 변환 │ │
│ │ │ 예: ヲ (U+FF66), ハ (U+FF8A) │ │
│ │ │ 원래 넓은 문자를 인위적으로 1셀로 축소 │ │
│ ├────────┼────────────────────────────────────────┤ │
│ │ Na │ Narrow (좁은 문자) │ │
│ │(Narrow)│ 본질적으로 좁은 문자 │ │
│ │ │ 고정폭 환경에서 1셀 차지 │ │
│ │ │ 예: Basic Latin (A~Z, 0~9, !"#$...) │ │
│ ├────────┼────────────────────────────────────────┤ │
│ │ N │ Neutral (중립) │ │
│ │(Neutral│ 동아시아 레거시 문자셋에 없는 문자 │ │
│ │) │ 예: 아랍 문자, 데바나가리, 그리스 문자 │ │
│ │ │ CJK 맥락에서는 좁은 문자로 취급 │ │
│ ├────────┼────────────────────────────────────────┤ │
│ │ A │ Ambiguous (모호) │ │
│ │(Ambi- │ 맥락에 따라 폭이 달라지는 문자 │ │
│ │guous) │ 동아시아 맥락: 2셀 (넓게) │ │
│ │ │ 비동아시아 맥락: 1셀 (좁게) │ │
│ │ │ 예: Greek Alpha (A), 원문자 (①②③), │ │
│ │ │ Box-Drawing Characters (─│┌┐) │ │
│ └────────┴────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
Wide vs Fullwidth의 차이
┌─────────────────────────────────────────────────────────┐
│ Wide vs Fullwidth: 중요한 구분 │
│ │
│ 흔한 오해: "Wide = Fullwidth" → 틀렸다 │
│ │
│ Wide (W): │
│ ├── 태생적으로 넓은 문자 │
│ ├── 대응하는 좁은 형태가 없음 (한자에 반각은 없다) │
│ ├── 예: 漢 (U+6F22), 한 (U+D55C), あ (U+3042) │
│ └── "이 문자는 원래부터 2셀이다" │
│ │
│ Fullwidth (F): │
│ ├── 인위적으로 넓힌 문자 (호환 목적) │
│ ├── 대응하는 Narrow 원본이 있음 │
│ ├── 예: A (U+FF21) ← A (U+0041)의 전각 변환 │
│ │ 1 (U+FF11) ← 1 (U+0031)의 전각 변환 │
│ └── "원래 1셀인 문자를 강제로 2셀로 만든 것" │
│ │
│ 실무 영향: │
│ ┌───────────────────────────────────────────────────┐ │
│ │ 폭 계산 시 W와 F 모두 2셀로 처리하지만 │ │
│ │ 정규화(Normalization) 시 Fullwidth 문자는 │
│ │ 대응하는 Narrow 문자로 변환 가능하다 │
│ │ (NFKC/NFKD 정규화) │
│ │ │
│ │ Wide 문자는 정규화해도 좁아지지 않는다 │
│ └───────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
Box-Drawing Characters의 East Asian Width
┌─────────────────────────────────────────────────────────┐
│ Box-Drawing Characters의 East Asian Width = Ambiguous │
│ │
│ 핵심 사실: │
│ ┌───────────────────────────────────────────────────┐ │
│ │ U+2500~U+257F (Box-Drawing)의 EAW 속성: │
│ │ → Ambiguous (A) │
│ │ │
│ │ 의미: │
│ │ ├── 동아시아 레거시 인코딩에서 전각이었던 문자 │
│ │ ├── 비동아시아 맥락에서는 1셀 │
│ │ └── 동아시아 맥락에서는 2셀일 수도 있음 │
│ └───────────────────────────────────────────────────┘ │
│ │
│ 터미널에서의 처리: │
│ ├── 대부분의 터미널은 Box-Drawing을 1셀로 처리 │
│ ├── 한글/한자는 2셀로 처리 │
│ └── 따라서 2:1 정렬이 유지됨 │
│ │
│ 브라우저에서의 문제: │
│ ├── EAW 속성을 참조하지 않음 │
│ └── 오직 폰트의 advance width만 사용 │
│ │
└─────────────────────────────────────────────────────────┘
5. 터미널 렌더링: wcwidth()와 셀 기반 폭
┌─────────────────────────────────────────────────────────┐
│ wcwidth() = POSIX 표준 함수 │
│ 주어진 문자가 터미널에서 차지하는 컬럼(셀) 수를 반환 │
│ │
│ 함수 시그니처: │
│ ┌───────────────────────────────────────────────────┐ │
│ │ #include <wchar.h> │ │
│ │ int wcwidth(wchar_t wc); │ │
│ │ │
│ │ 반환값: │
│ │ ├── 0 : 비출력 문자 (제어 문자 일부) │
│ │ ├── 1 : 좁은 문자 (ASCII, Box-Drawing 등) │
│ │ ├── 2 : 넓은 문자 (한글, 한자, 가나 등) │
│ │ └── -1 : 출력 불가 문자 │
│ └───────────────────────────────────────────────────┘ │
│ │
│ 핵심 동작: │
│ ├── 한글 "한" (U+D55C) → wcwidth() = 2 │
│ ├── ASCII "A" (U+0041) → wcwidth() = 1 │
│ ├── Box-Drawing "─" (U+2500) → wcwidth() = 1 │
│ └── Box-Drawing "┌" (U+250C) → wcwidth() = 1 │
│ │
│ 이것이 터미널에서 정렬이 완벽한 이유: │
│ 폰트의 실제 글리프 폭이 어떻든, │
│ 터미널은 wcwidth()가 정한 셀 수로 강제 배치한다 │
│ │
└─────────────────────────────────────────────────────────┘
셀 기반 렌더링의 핵심
┌─────────────────────────────────────────────────────────┐
│ 터미널의 셀(Cell) 기반 렌더링 원리 │
│ │
│ 터미널 = 고정 크기 셀의 격자(Grid) │
│ │
│ ┌───────────────────────────────────────────────────┐ │
│ │ 셀 격자 (각 셀 = 동일한 폭 W): │ │
│ │ │
│ │ [W][W][W][W][W][W][W][W][W][W] ← 10셀 │ │
│ │ │
│ │ "Hello" 배치: │
│ │ [H][e][l][l][o][ ][ ][ ][ ][ ] 각 1셀 │ │
│ │ │
│ │ "한글AB" 배치: │
│ │ [한 ][글 ][A][B][ ][ ][ ][ ] │
│ │ 2셀 2셀 1셀 1셀 = 총 6셀 │
│ │ │
│ │ "┌──┐" 배치: │
│ │ [┌][─][─][┐][ ][ ][ ][ ][ ][ ] 각 1셀 │ │
│ └───────────────────────────────────────────────────┘ │
│ │
│ 핵심 원리: │
│ ┌───────────────────────────────────────────────────┐ │
│ │ 폭 결정 주체 = 터미널 (외부) │
│ │ 폰트 메트릭(advance width) = 무시됨 │
│ │ │
│ │ 터미널이 wcwidth() 결과에 따라 │
│ │ 셀을 할당하고, 글리프를 그 셀 안에 │
│ │ 강제로 끼워 넣는다 (스케일링/클리핑) │
│ │ │
│ │ → 어떤 폰트를 쓰든 2:1 비율 보장 │
│ └───────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
6. 웹 브라우저 렌더링: advance width와 문제점
┌─────────────────────────────────────────────────────────┐
│ 브라우저의 텍스트 렌더링: wcwidth() 없음 │
│ │
│ advance width (전진 폭): │
│ 폰트 파일 내부에 저장된 글리프별 수평 이동 거리 │
│ → 다음 글리프를 어디에 배치할지 결정하는 값 │
│ → OpenType/TrueType의 hmtx 테이블에 기록 │
│ │
│ UPM (Units Per Em): │
│ 폰트의 디자인 좌표계 단위 │
│ → 폰트마다 UPM이 다름 (보통 1000 또는 2048) │
│ │
│ 브라우저 렌더링 과정: │
│ ┌───────────────────────────────────────────────────┐ │
│ │ 1. 문자 발견 │
│ │ 2. font-family 목록에서 글리프 탐색 │
│ │ → 첫 번째 폰트에 없으면 다음 폰트로 폴백 │
│ │ → 모두 없으면 시스템 폴백 폰트 사용 │
│ │ 3. 해당 폰트의 hmtx에서 advance width 읽기 │
│ │ 4. advance width만큼 수평 이동 후 다음 글리프 │
│ │ │
│ │ → 셀 강제 메커니즘 없음 │
│ │ → 폰트가 말하는 대로 배치 │
│ └───────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
세 가지 핵심 문제
┌─────────────────────────────────────────────────────────┐
│ 문제 1: 폰트 메트릭 불일치 │
│ │
│ 시나리오: │
│ font-family: "Fira Code", monospace; │
│ │
│ ┌───────────────────────────────────────────────────┐ │
│ │ "A" 렌더링: │
│ │ Fira Code에 있음 → advance width = 600 │
│ │ (UPM 1000 기준) │
│ │ │
│ │ "한" 렌더링: │
│ │ Fira Code에 없음 → 폴백: Malgun Gothic 사용 │
│ │ Malgun Gothic의 UPM = 2048 │
│ │ "한"의 advance width = 2048 (전각) │
│ │ │
│ │ 브라우저가 동일 font-size에서 스케일링: │
│ │ Fira Code "A" → 실제 렌더링 폭 X │
│ │ Malgun Gothic "한" → 실제 렌더링 폭 Y │
│ │ │
│ │ Y / X != 2.0 이므로 정렬 깨짐 │
│ └───────────────────────────────────────────────────┘ │
│ │
│ 원인: 두 폰트가 독립적으로 디자인됨 │
│ → UPM 단위가 다르고, 글리프 폭 비율이 다름 │
│ → "정확히 2배"라는 보장이 없다 │
│ │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 문제 2: 셀 강제 메커니즘의 부재 │
│ │
│ 터미널 vs 브라우저 비교: │
│ ┌───────────────────────────────────────────────────┐ │
│ │ │
│ │ 터미널: │
│ │ "한" → wcwidth() = 2 → 셀 2개 할당 → 강제 배치 │
│ │ "A" → wcwidth() = 1 → 셀 1개 할당 → 강제 배치 │
│ │ 비율 = 항상 정확히 2:1 │
│ │ │
│ │ 브라우저: │
│ │ "한" → advance width = 폰트가 결정 → 그대로 │
│ │ "A" → advance width = 폰트가 결정 → 그대로 │
│ │ 비율 = 폰트 조합에 따라 달라짐 │
│ │ │
│ │ 브라우저에는 wcwidth() 같은 │
│ │ "이 문자는 반드시 N칸이어야 한다"는 │
│ │ 강제 메커니즘이 존재하지 않는다 │
│ │ │
│ └───────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 문제 3: 서로 다른 폰트 파일의 UPM 차이 │
│ │
│ UPM (Units Per Em): │
│ 폰트의 디자인 격자 해상도 │
│ │
│ ┌───────────────────────────────────────────────────┐ │
│ │ 폰트별 UPM 예시: │
│ │ ├── Fira Code: UPM = 1000 │
│ │ ├── JetBrains Mono: UPM = 1000 │
│ │ ├── Malgun Gothic: UPM = 2048 │
│ │ ├── Noto Sans CJK: UPM = 1000 │
│ │ └── D2Coding: UPM = 1000 │
│ └───────────────────────────────────────────────────┘ │
│ │
│ UPM이 같더라도 문제: │
│ ┌───────────────────────────────────────────────────┐ │
│ │ Fira Code (UPM=1000): │
│ │ "A" advance width = 600 │
│ │ → 이상적 CJK 폭 = 1200 (2배) │
│ │ │
│ │ Noto Sans CJK (UPM=1000): │
│ │ "한" advance width = 1000 │
│ │ → 1000 / 600 = 1.667배 (2배 아님!) │
│ │ │
│ │ → UPM이 같아도 advance width 비율이 │
│ │ 정확히 2:1이 아니면 정렬이 깨진다 │
│ └───────────────────────────────────────────────────┘ │
│ │
│ 결론: │
│ Latin과 CJK 글리프가 서로 다른 폰트 파일에 있으면 │
│ 2:1 비율을 보장할 수 없다 │
│ → 단일 폰트 파일에 둘 다 포함해야 한다 │
│ │
└─────────────────────────────────────────────────────────┘
7. Duospaced Font: 해결 개념
┌─────────────────────────────────────────────────────────┐
│ Duospaced Font (듀오스페이스드 폰트) │
│ = 정확히 두 가지 글리프 폭만 허용하는 고정폭 폰트 │
│ (1x와 2x, 즉 반각과 전각) │
│ │
│ 기존 폰트 분류: │
│ ┌───────────────────────────────────────────────────┐ │
│ │ Proportional (가변폭): │
│ │ 글자마다 폭이 다름 │
│ │ 예: "i" 좁고 "W" 넓음 │
│ │ → Times New Roman, Arial │
│ │ │
│ │ Monospaced (고정폭): │
│ │ 모든 글자가 동일한 폭 │
│ │ → Courier New, Fira Code │
│ │ 문제: CJK 글자도 1칸? (너무 좁음) │
│ │ 아니면 CJK 기준 전체 2칸? (Latin이 너무 넓음)│
│ │ │
│ │ Duospaced (이중폭): │
│ │ 정확히 2가지 폭만 허용 │
│ │ Latin = N 유닛 (1x) │
│ │ CJK = 2N 유닛 (2x) │
│ │ → 2:1 비율 엄격 보장 │
│ └───────────────────────────────────────────────────┘ │
│ │
│ Duospaced가 해결하는 것: │
│ ┌───────────────────────────────────────────────────┐ │
│ │ 1. 단일 폰트 파일에 Latin + CJK 모두 포함 │
│ │ → 폰트 폴백 불필요 → UPM 불일치 없음 │
│ │ │
│ │ 2. Latin advance width의 정확히 2배로 │
│ │ CJK advance width를 설계 │
│ │ → 2:1 비율 폰트 레벨에서 보장 │
│ │ │
│ │ 3. Box-Drawing Characters도 Latin과 동일 폭(1x) │
│ │ → 터미널과 동일한 정렬 결과 │
│ └───────────────────────────────────────────────────┘ │
│ │
│ 핵심 통찰: │
│ 브라우저에 wcwidth()가 없으므로 │
│ 폰트 자체가 2:1 비율을 내장해야 한다 │
│ Duospaced font = "폰트에 wcwidth()를 내장한 것" │
│ │
└─────────────────────────────────────────────────────────┘
8. D2Coding: 실용적 해결책
┌─────────────────────────────────────────────────────────┐
│ D2Coding 폰트 개요 │
│ │
│ 개발: Naver │
│ 디자인 협력: Sandoll Communications, Fontrix │
│ 기반: Nanum Barun Gothic (나눔바른고딕) │
│ 라이선스: SIL Open Font License 1.1 │
│ │
│ 핵심 특성: │
│ ┌───────────────────────────────────────────────────┐ │
│ │ Duospaced Font │
│ │ │
│ │ Latin (ASCII) 글리프: N 유닛 (advance width) │
│ │ 한글/한자 글리프: 2N 유닛 (advance width) │
│ │ Box-Drawing 글리프: N 유닛 (advance width) │
│ │ │
│ │ → 단일 .ttf 파일 안에서 │
│ │ 모든 글리프의 2:1 비율이 보장됨 │
│ │ → 폰트 폴백 없이 렌더링 가능 │
│ └───────────────────────────────────────────────────┘ │
│ │
│ 변형: │
│ ├── D2Coding: 기본 버전 │
│ └── D2Coding Ligature: 프로그래밍 합자 포함 │
│ 예: != → ≠, => → ⇒, -> → → 등 │
│ (Fira Code 스타일의 합자를 추가한 변형) │
│ │
│ 포함 글리프: │
│ ├── Basic Latin (ASCII) │
│ ├── Korean Syllables (한글 음절) │
│ ├── CJK Unified Ideographs (한자, 부분) │
│ ├── Box-Drawing Characters (전체) │
│ ├── Block Elements (전체) │
│ ├── Arrows, Mathematical Operators │
│ └── 기타 기호 │
│ │
└─────────────────────────────────────────────────────────┘
대안 폰트
┌─────────────────────────────────────────────────────────┐
│ Duospaced CJK 대안 폰트들 │
│ │
│ ┌───────────────────────────────────────────────────┐ │
│ │ Noto Sans Mono CJK KR │ │
│ │ ├── Google + Adobe 공동 개발 │
│ │ ├── Source Han Mono의 Google 배포명 │
│ │ ├── CJK 전체 커버리지 (한중일 통합) │
│ │ ├── SIL Open Font License │
│ │ └── 장점: 글리프 커버리지 최대 │
│ │ │
│ │ Sarasa Mono K │ │
│ │ ├── Iosevka (Latin) + Source Han Sans (CJK) 합성 │
│ │ ├── K = Korean 변형 │
│ │ ├── 프로그래밍 특화 Latin 글리프 │
│ │ ├── SIL Open Font License │
│ │ └── 장점: Iosevka의 좁은 Latin 글리프 선호 시 │ │
│ │ │
│ │ IBM Plex Mono (KR 확장) │ │
│ │ ├── IBM 개발, 한글 확장 버전 존재 │ │
│ │ └── SIL Open Font License │ │
│ └───────────────────────────────────────────────────┘ │
│ │
│ 선택 기준: │
│ ├── 한국어 중심 → D2Coding (최적화된 한글 디자인) │
│ ├── 다국어 CJK → Noto Sans Mono CJK (최대 커버리지) │
│ └── 좁은 Latin 선호 → Sarasa Mono K │
│ │
└─────────────────────────────────────────────────────────┘
9. Jekyll/Minimal Mistakes 블로그 적용 방법
┌─────────────────────────────────────────────────────────┐
│ Jekyll + Minimal Mistakes 테마에서 적용 방법 │
│ │
│ 목표: │
│ 블로그의 코드 블록(``` ... ```)에서 │
│ Box-Drawing + 한글 혼합 다이어그램이 │
│ 정확히 정렬되도록 한다 │
│ │
│ 수정 파일 2개: │
│ ┌───────────────────────────────────────────────────┐ │
│ │ 1. _includes/head/custom.html │ │
│ │ → D2Coding 웹폰트 CDN 로드 │ │
│ │ │
│ │ 2. assets/css/main.scss │ │
│ │ → 코드 블록의 font-family 오버라이드 │
│ └───────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
수정 1: D2Coding 웹폰트 로드
파일: _includes/head/custom.html
<!-- start custom head snippets -->
<!-- insert favicons. use https://realfavicongenerator.net/ -->
<link rel="icon" type="image/png" href="..." sizes="96x96" />
<!-- ... (기존 favicon 설정) ... -->
<!-- D2Coding: CJK-aware monospace font for ASCII diagram alignment -->
<link href="https://cdn.jsdelivr.net/gh/fonts-archive/D2Coding/D2Coding.css"
rel="stylesheet" type="text/css">
<!-- end custom head snippets -->
┌─────────────────────────────────────────────────────────┐
│ 설명: │
│ ├── jsDelivr CDN을 통해 D2Coding 웹폰트를 로드 │
│ ├── fonts-archive/D2Coding: 폰트 아카이브 저장소 │
│ ├── D2Coding.css: @font-face 선언 포함 │
│ └── <head> 영역에 추가하여 페이지 로드 시 즉시 적용 │
│ │
│ 대안 CDN: │
│ ├── Google Fonts (D2Coding 미등록, 대안 폰트 필요) │
│ ├── 셀프 호스팅: .woff2 파일 직접 서빙 │
│ └── npm: @fontsource/d2-coding 패키지 │
│ │
└─────────────────────────────────────────────────────────┘
수정 2: 코드 블록 폰트 오버라이드
파일: assets/css/main.scss
---
# Only the main Sass file needs front matter (the dashes are enough)
---
@import "minimal-mistakes/skins/neon";
@import "minimal-mistakes";
/* ===================================================================
CJK Monospace Font Override
- D2Coding guarantees CJK characters render at exactly 2x ASCII width
- This fixes ASCII box-drawing diagram alignment with Korean text
=================================================================== */
div.highlighter-rouge,
figure.highlight,
pre,
code {
font-family: "D2Coding", "D2Coding ligature",
"Noto Sans Mono CJK KR", monospace !important;
}
Font Fallback Chain 설명
┌─────────────────────────────────────────────────────────┐
│ Font Fallback Chain 설계 │
│ │
│ font-family: "D2Coding", │
│ "D2Coding ligature", │
│ "Noto Sans Mono CJK KR", │
│ monospace; │
│ │
│ 폴백 순서와 이유: │
│ ┌───────────────────────────────────────────────────┐ │
│ │ 1순위: "D2Coding" │ │
│ │ ├── 주 폰트 (CDN 로드) │ │
│ │ ├── Latin + 한글 + Box-Drawing 모두 포함 │ │
│ │ └── 2:1 비율 보장하는 Duospaced 폰트 │ │
│ │ │
│ │ 2순위: "D2Coding ligature" │ │
│ │ ├── 사용자 로컬에 Ligature 버전이 설치된 경우 │ │
│ │ ├── CDN 실패 시 로컬 폰트로 폴백 │ │
│ │ └── 프로그래밍 합자 지원 │ │
│ │ │
│ │ 3순위: "Noto Sans Mono CJK KR" │ │
│ │ ├── D2Coding 완전 실패 시 대안 │ │
│ │ ├── Google/Adobe의 범용 CJK 모노스페이스 │ │
│ │ └── 역시 Duospaced 폰트 (2:1 보장) │ │
│ │ │
│ │ 4순위: monospace │ │
│ │ ├── 최후의 폴백 (시스템 기본 고정폭 폰트) │ │
│ │ ├── CJK 2:1 비율 보장 불가 │ │
│ │ └── Box-Drawing 정렬 깨질 가능성 │ │
│ └───────────────────────────────────────────────────┘ │
│ │
│ !important를 사용하는 이유: │
│ ├── Minimal Mistakes 테마가 자체 font-family를 지정 │
│ ├── 테마의 기본값은 CJK 비대응 모노스페이스 │
│ └── !important로 테마 스타일을 확실히 오버라이드 │
│ │
│ 셀렉터 대상: │
│ ├── div.highlighter-rouge: Jekyll 마크다운 코드 블록 │
│ ├── figure.highlight: Jekyll 코드 하이라이팅 블록 │
│ ├── pre: HTML <pre> 요소 (서식 유지 텍스트) │
│ └── code: 인라인 <code> 요소 │
│ │
└─────────────────────────────────────────────────────────┘
적용 결과 비교
┌─────────────────────────────────────────────────────────┐
│ 적용 전 vs 적용 후 │
│ │
│ 적용 전 (시스템 monospace 폴백): │
│ ┌───────────────────────────────────────────────────┐ │
│ │ font-family: monospace (= Courier New 등) │ │
│ │ │
│ │ ┌──────────┐ ← Latin 폰트 기준 정렬 │ │
│ │ │ 한글 │ ← 한글은 폴백 폰트, 폭 불일치 │ │
│ │ │ English │ ← Latin은 원래 폰트 │ │
│ │ └──────────┘ ← 테두리 어긋남 │ │
│ │ │
│ │ → 수직 테두리(│)가 행마다 다른 위치 │
│ └───────────────────────────────────────────────────┘ │
│ │
│ 적용 후 (D2Coding): │
│ ┌───────────────────────────────────────────────────┐ │
│ │ font-family: "D2Coding", ... monospace │ │
│ │ │
│ │ ┌──────────┐ ← 단일 폰트에서 모든 글리프 │ │
│ │ │ 한글 │ ← 한글 = Latin의 정확히 2배 │ │
│ │ │ English │ ← Box-Drawing = Latin과 동일 │ │
│ │ └──────────┘ ← 테두리 완벽 정렬 │ │
│ │ │
│ │ → 터미널과 동일한 정렬 결과 │
│ └───────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
관련 키워드
Box-Drawing Characters, CJK, East Asian Width, UAX #11, Monospace, Duospaced Font, advance width, hmtx, UPM, wcwidth, POSIX, D2Coding, Noto Sans Mono CJK, Sarasa Mono, font fallback, Unicode, Semigraphics, Pseudographics, Block Elements, Jekyll, Minimal Mistakes, 고정폭 폰트, 전각, 반각, 모노스페이스, 한글 렌더링