mirim-app/DESIGN.md
AWESOMEDEV 5c9959ce08
All checks were successful
CI / backend-test (push) Successful in 3m25s
CI / frontend-build (push) Successful in 48s
CI / backend-dep-scan (push) Successful in 31s
feat(design): 디자인 시스템 — Pretendard 실적용·토큰·Lucide 아이콘 132개·/design
이모지 아이콘을 SVG로 바꾸다가, 더 큰 원인을 발견해 함께 고쳤다.

■ 폰트: 이름만 부르고 파일은 안 받아오고 있었다
  global.css에 font-family: "Pretendard"가 적혀 있었지만 @font-face도 CDN
  링크도 없었다. 그래서 이 글꼴이 따로 깔린 사람 외에는 전원 맑은 고딕으로
  보고 있었다 — 앱과 학습 문서 19개 전부. 에러가 안 나서 조용히 넘어간 케이스.
  - PretendardStdVariable.woff2(285KB)를 public/fonts에 self-host
    · CDN 의존 없음 → 외부 서비스가 죽어도, 오프라인에서도 안 깨진다
    · variable font → 파일 하나가 굵기 45~920을 전부 담는다
    · font-display: swap → 받는 동안 흰 화면(FOIT) 방지
  - 전체판(2MB) 대신 Std판(상용 한글 2,350자)을 골랐다. 앱이 쓰는 한글
    1,276자가 Std에 전부 있는지 브라우저에서 실측 확인했다(폴백 0자).
  - 학습 문서 19개에도 같은 @font-face 적용

■ 아이콘: 이모지 95종 → Lucide 132개
  이모지는 OS마다 다른 그림으로 렌더링되고, 굵기·색을 통제할 수 없고,
  스크린리더가 "bust in silhouette"처럼 읽는다.
  - 카탈로그의 icon: '📜'(문자) → Icon: ScrollText(컴포넌트)로 전환
  - strokeWidth={1.5}로 굵기 통일, aria-hidden으로 장식임을 명시
  - 개별 import만 사용(트리셰이킹) → gzip +14KB
  - slug: null인 '개발 환경 설치 가이드' 항목이 일괄 변환에서 누락돼
    Icon이 undefined가 될 뻔한 것을 정합성 검사(항목 수 vs Icon 수)로 잡았다

■ 토큰: 여백·글자 크기·굵기를 :root로 승격
  --space-1~7(4의 배수), --text-xs~2xl, --weight-*, --radius-sm/lg/full.
  값을 직접 박으면 다크 모드에서 깨지고 화면마다 미묘하게 어긋난다.
  CSS 프레임워크는 도입하지 않았다 — 손으로 쓴 global.css와 충돌하고,
  교재로서 "왜 이렇게 썼는지"가 사라지기 때문이다.

■ /design: 재료를 고르는 화면
  색·글자·여백·모서리를 실제 CSS 변수로 렌더링(다크 모드 자동 반영) +
  Lucide 아이콘 1,993개 검색·복사. lucide-react는 아이콘 하나를 이름 3개로
  내보내(Rocket/RocketIcon/LucideRocket) 5,981개처럼 보이므로 별칭을 걸렀다.
  아이콘 전체를 훑어야 해 무거운 화면이라 lazy로 분리했다
  → 첫 화면 번들은 1.9KB만 증가(575.57 → 577.44KB).

검증: ESLint 통과, Vitest 30개 통과, 프로덕션 빌드 성공, 콘솔 에러 0.
브라우저에서 수습 7카테고리 + 초급~특급 4과정 = 11개 화면 전수 확인
(각 12카드/12아이콘/이모지 0).

문서: DESIGN.md(폰트·토큰·비용), ICONS.md(규약·대응표 132행)
2026-07-22 21:10:28 +09:00

6.0 KiB

DESIGN.md — 디자인 재료 사용법

매번 찾지 말고 여기서 고르세요. 색·글자·여백·아이콘이 전부 저장소 안에 들어 있습니다. 앱을 켜고 /design 을 열면 이 문서의 내용을 눈으로 보고 클릭해서 복사할 수 있습니다. 아이콘 이름 대응표는 ICONS.md에 있습니다.


1. 세 가지 재료가 어디 있나

재료 어디 매번 받아야 하나
아이콘 1,993개 lucide-react (node_modules) npm install 한 번으로 끝. 오프라인 동작
폰트 Pretendard frontend/public/fonts/ 저장소에 들어 있음. CDN 의존 없음
색·여백·글자 토큰 frontend/src/styles/global.css:root 이미 정의돼 있음

셋 다 이미 받아 놨습니다. 새로 검색하거나 다운로드할 일은 없습니다. 고르기만 하세요.


2. 폰트 — 이름을 부른다고 폰트가 오지 않는다

⚠️ 실제로 있었던 문제입니다. CSS에 이렇게 적혀 있었습니다.

font-family: "Pretendard", "Apple SD Gothic Neo", "Malgun Gothic", system-ui, sans-serif;

그런데 Pretendard 파일을 어디서도 받아오지 않았습니다. @font-face도, CDN 링크도 없었습니다. 그래서 그 폰트가 따로 깔린 사람 외에는 전원 맑은 고딕으로 보고 있었습니다. 앱과 학습 문서 19개 전부요.

학습 포인트: font-family"이 순서대로 시도해 봐"라는 희망 목록일 뿐입니다. 브라우저는 목록의 이름을 (1) 내 컴퓨터에 깔린 폰트와 (2) @font-face로 알려 준 웹폰트에서만 찾습니다. 둘 다 없으면 조용히 다음 후보로 넘어갑니다. 에러가 안 나기 때문에 몇 달 동안 아무도 모를 수 있습니다. "될 것 같은데 왜 안 되지?"의 절반은 이런 조용한 실패입니다.

지금은 이렇게 고쳤습니다.

@font-face {
  font-family: "Pretendard";
  src: url("/fonts/PretendardStdVariable.woff2") format("woff2-variations");
  font-weight: 45 920;      /* 파일 하나가 모든 굵기를 담는다 (variable font) */
  font-display: swap;       /* 받는 동안 기본 글씨로 먼저 보여 준다 */
}

왜 이 선택인가

  • self-host (CDN 아님) — 외부 서비스가 죽어도, 인터넷이 없어도 글씨가 안 깨집니다. 사내 학습 앱엔 이쪽이 맞습니다.
  • variable font — 굵기별 파일 9개 대신 파일 1개. 요청 수와 용량이 함께 줄어듭니다.
  • Std 판(285KB) — 상용 한글 2,350자 판입니다. 전체 한글 11,172자 판은 2MB로 7배입니다. 이 앱이 쓰는 한글 1,276자가 Std에 100% 들어 있는지 실제로 측정해서 확인했습니다(폴백된 글자 0개).
  • font-display: swap — 폰트를 받는 동안 글자를 숨기면 흰 화면이 됩니다(FOIT). 기본 글씨로 먼저 보여 주고 나중에 바꿔 끼웁니다.

3. 토큰 — 값 대신 이름을 쓴다

global.css:root에 정의돼 있습니다. 값을 직접 적지 마세요.

/* ❌ 이렇게 쓰면 */
padding: 10px;  color: #46536A;  font-size: 13.5px;

/* ✅ 이렇게 쓰세요 */
padding: var(--space-3);  color: var(--muted);  font-size: var(--text-sm);
종류 토큰 쓰는 곳
--bg --card --ink --muted --line --primary --teal --amber --rose 전부
여백 --space-1(4px) ~ --space-7(48px) padding·margin·gap
글자 크기 --text-xs(12) --text-sm(13) --text-md(15, 본문) --text-lg --text-xl --text-2xl font-size
굵기 --weight-regular --weight-medium --weight-bold --weight-black font-weight
모서리 --radius-sm --radius --radius-lg --radius-full border-radius

왜 이름을 쓰나 — 이유 두 개

  1. 다크 모드가 공짜로 따라옵니다. --ink는 밝은 모드에서 거의 검정, 어두운 모드에서 거의 흰색입니다. #101A28이라고 직접 박으면 다크 모드에서 검은 배경에 검은 글씨가 됩니다.
  2. 어긋남이 사라집니다. 여백을 그때그때 10px, 14px, 6px로 찍으면 화면이 미묘하게 틀어집니다. 사람 눈은 그 차이를 "다름"이 아니라 "실수"로 읽습니다. 4의 배수 사다리만 밟으면 저절로 맞습니다.

4. 아이콘

규약과 132개 대응표는 ICONS.md에 있습니다. 요약하면:

import { Rocket } from 'lucide-react';          // 개별 import (통째로 import 금지)

<Rocket size={18} strokeWidth={1.5} aria-hidden="true" />
  • 굵기는 항상 strokeWidth={1.5} — 섞이면 통일감이 깨집니다.
  • 아이콘은 장식이므로 aria-hidden="true". 글자 없이 아이콘만 쓸 땐 부모에 aria-label.
  • ⚠️ lucide-react는 아이콘 하나를 이름 3개로 내보냅니다(Rocket, RocketIcon, LucideRocket). 전부 세면 5,981개지만 실제 아이콘은 1,993개입니다. /design은 별칭을 걸러서 보여 줍니다.

이름은 /design에서 검색해 클릭하면 import 구문째로 복사됩니다.


5. 비용은 얼마나 드나 — 재 봤습니다

크기 언제 받나
폰트 285 KB 첫 방문 1회, 이후 캐시
아이콘 132개 (앱에서 실제 사용) gzip +14 KB 첫 화면 번들에 포함
/design 페이지 gzip 164 KB 그 페이지에 들어갈 때만 (lazy)

/design은 아이콘 1,993개를 전부 훑어야 해서 무겁습니다. 그래서 App.jsx에서 lazy()로 분리했습니다. 덕분에 이 페이지를 추가하고도 첫 화면 번들은 1.9KB만 늘었습니다.

학습 포인트: "무거운 기능을 넣으면 앱이 느려진다"는 꼭 그렇지 않습니다. 그 기능을 필요한 순간에만 내려받게 하면 됩니다. 무엇이 첫 화면에 실려야 하고 무엇이 나중에 와도 되는지를 나누는 것이 프론트엔드 성능 설계의 핵심입니다.


AWESOMEDEV · 디자인 재료 v1