이모지 아이콘을 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행)
13 KiB
ICONS.md — 아이콘 규약과 대응표
이 앱의 아이콘은 Lucide(SVG) 하나로 통일합니다. 이모지는 UI에 쓰지 않습니다. 아이콘을 새로 넣거나 바꿀 때 이 문서를 먼저 읽으세요. 색·글자·여백 등 나머지 재료는 DESIGN.md에, 실물은 앱의
/design에 있습니다.
1. 왜 이모지를 걷어냈나
원래 강좌 카드마다 이모지(📜 💻 🌐 …)가 붙어 있었습니다. 편하지만 세 가지가 발목을 잡습니다.
① 기기마다 다른 그림이 나온다. 이모지는 글꼴입니다. 윈도우·맥·안드로이드가 각자 다른 그림을 그려서, 내 화면에서 예쁜 게 남의 화면에선 아닐 수 있습니다. 우리가 통제할 수 없는 디자인이라는 게 핵심 문제입니다.
② 굵기·크기·색을 못 맞춘다. 어떤 이모지는 선이 두껍고 어떤 건 알록달록합니다. 12개를 한 화면에 모으면 통일감이 안 생깁니다. SVG 아이콘은 size·strokeWidth·color를 코드로 지시할 수 있어 132개가 한 세트처럼 보입니다.
③ 스크린리더가 엉뚱하게 읽는다. 👤 하나만 있는 링크를 리더는 "bust in silhouette 링크"라고 읽습니다. (이 문제는 App.jsx 헤더에서 실제로 겪었습니다.)
학습 포인트: "그냥 취향 문제 아닌가?"로 보이는 UI 결정에도 통제 가능성·일관성·접근성이라는 기술적 근거가 있습니다. 디자인 논쟁은 근거를 대는 쪽이 이깁니다.
2. 쓰는 법
카탈로그는 아이콘을 문자가 아니라 컴포넌트로 들고 있습니다.
// courseCatalog.jsx — 데이터
import { ScrollText } from 'lucide-react';
{ slug: 'history', Icon: ScrollText, title: '컴퓨터의 역사', /* … */ }
// LearnHubPage.jsx — 화면
<course.Icon size={26} strokeWidth={1.5} aria-hidden="true" />
지켜야 할 규칙 네 가지:
| 규칙 | 이유 |
|---|---|
필드 이름은 Icon (대문자 I) |
JSX는 대문자로 시작하는 이름만 컴포넌트로 취급합니다. 소문자는 <div> 같은 HTML 태그로 봅니다. |
strokeWidth={1.5} 고정 |
굵기가 섞이면 통일감이 깨집니다. 이 값이 우리 앱의 기본 굵기입니다. |
aria-hidden="true" 필수 |
아이콘은 장식입니다. 의미는 옆의 글자가 전달합니다. |
글자 없이 아이콘만 쓸 땐 aria-label |
예: 헤더의 내 정보·비밀번호 버튼. 안 그러면 스크린리더가 읽을 게 없습니다. |
개별 import만 씁니다. import * as Icons from 'lucide-react'처럼 통째로 가져오면 안 쓰는 아이콘까지 전부 번들에 실립니다. 지금처럼 이름을 하나씩 적으면 번들러가 쓰는 것만 남깁니다(트리셰이킹).
새 아이콘은 https://lucide.dev/icons 에서 고르고, 파일 상단 import 목록에 알파벳 순서로 끼워 넣으세요.
3. 비용
아이콘 132종을 넣고 번들이 얼마나 커졌는지 실제로 재 봤습니다.
| 변경 전 | 변경 후 | 차이 | |
|---|---|---|---|
| index 청크 | 518.36 kB | 575.57 kB | +57.2 kB |
| gzip 전송 크기 | 171.96 kB | 186.01 kB | +14.0 kB |
학습 포인트: "라이브러리 넣으면 무거워진다"는 말은 숫자로 확인하기 전까진 추측입니다. 실제로 재 보면 아이콘 132개에 gzip 14KB — 사진 한 장보다 작습니다. 빌드 결과를 읽는 습관이 이런 판단을 가능하게 합니다.
Lucide를 고른 이유: 아이콘 약 1,600종으로 우리가 필요한 개념(CPU·네트워크·DB·보안·디자인·앱)을 전부 덮고, 굵기가 한 종류라 섞어 써도 통일감이 유지됩니다.
⚠️ 브랜드 로고(Figma 등)는 Lucide v1에서 제거됐습니다. 그래서 「피그마와 디자인 툴」은 로고 대신 도구의 성격을 나타내는 PenTool을 씁니다.
4. 전체 대응표 (132개)
수습 트랙(83강좌 + 설치 가이드) — frontend/src/courseCatalog.jsx
| 강좌 | 이전(이모지) | 이후(Lucide) | 아이콘 |
|---|---|---|---|
| 컴퓨터의 역사 | 📜 | ScrollText |
scroll-text |
| 컴퓨터의 구성 | 💻 | Computer |
computer |
| 2진수와 데이터 표현 | 🔢 | Binary |
binary |
| CPU 깊이 보기 | 🧠 | Cpu |
cpu |
| 메모리와 저장장치 깊이 보기 | 💾 | HardDrive |
hard-drive |
| 그래픽카드와 GPU의 세계 | 🎮 | MemoryStick |
memory-stick |
| 메인보드와 파워 깊이 보기 | 🔋 | CircuitBoard |
circuit-board |
| 데스크탑 조립하기 | 🔧 | Wrench |
wrench |
| 운영체제의 이해 | 🪟 | AppWindow |
app-window |
| 파일과 폴더의 원리 | 📁 | Folder |
folder |
| 노트북과 주변기기 고르기 | 🖥️ | Laptop |
laptop |
| 컴퓨터 문제 해결 | 🩺 | Stethoscope |
stethoscope |
| 네트워크의 이해 | 🌐 | Globe |
globe |
| 네트워크 속도와 대역폭 | 🚗 | Gauge |
gauge |
| IP 주소 깊이 보기 | 🔍 | MapPin |
map-pin |
| 공유기와 홈 네트워크 실전 | 📡 | Router |
router |
| DNS 깊이 보기 | 📖 | BookMarked |
book-marked |
| TCP와 UDP | 🤝 | Handshake |
handshake |
| HTTP와 HTTPS | 📨 | ArrowRightLeft |
arrow-right-left |
| 네트워크 명령어 실전 | 🧰 | SquareTerminal |
square-terminal |
| 무선 네트워크와 보안 | 🔐 | Wifi |
wifi |
| VPN과 프록시 | 🚇 | Route |
route |
| 방화벽과 보안 기초 | 🛡️ | BrickWall |
brick-wall |
| 클라우드 네트워크 입문 | ☁️ | Cloud |
cloud |
| 서버란 무엇인가 | 🏗️ | Server |
server |
| 리눅스 / 터미널 기초 | 🐧 | Terminal |
terminal |
| 리눅스 심화 | ⚙️ | Settings2 |
settings-2 |
| 도커 입문 | 🐳 | Container |
container |
| 클라우드와 AWS 입문 | 🌩️ | CloudLightning |
cloud-lightning |
| SQL 입문 | 🗄️ | Database |
database |
| SQL 중급 | 📊 | ChartColumn |
chart-column |
| 데이터 모델링 기초 | 🗂️ | Table |
table |
| 캐시와 NoSQL 맛보기 | ⚡ | Zap |
zap |
| API의 이해 | 🔌 | Plug |
plug |
| CI/CD와 배포 자동화 입문 | 🔁 | RotateCw |
rotate-cw |
| 운영과 장애 대응 기초 | 🚨 | Siren |
siren |
| 코딩 기초 | ⌨️ | Keyboard |
keyboard |
| HTML / CSS 기초 | 🧱 | FileCode |
file-code |
| 자바스크립트 기초 | ⚡ | Braces |
braces |
| 자료구조 맛보기 | 🧺 | Boxes |
boxes |
| React 입문 | ⚛️ | Atom |
atom |
| Java 기초 | ☕ | Coffee |
coffee |
| Spring Boot 입문 | 🍃 | Leaf |
leaf |
| Git 깊이 보기 | 🌳 | GitBranch |
git-branch |
| 디버깅과 에러 읽는 법 | 🐞 | Bug |
bug |
| 테스트 코드 입문 | 🧪 | FlaskConical |
flask-conical |
| 클린 코드와 코드리뷰 | ✨ | Sparkles |
sparkles |
| AI 도구와 함께 코딩하기 | 🤖 | Bot |
bot |
| UI/UX 기초 | 🎨 | Palette |
palette |
| 색 이론 기초 | 🌈 | SwatchBook |
swatch-book |
| 타이포그래피 | 🔤 | Type |
type |
| 레이아웃과 그리드 | 📐 | LayoutGrid |
layout-grid |
| 아이콘과 이미지 | 🖼️ | Image |
image |
| 와이어프레임 기초 | ✏️ | PencilRuler |
pencil-ruler |
| 피그마와 디자인 툴 | 🖌️ | PenTool |
pen-tool |
| 디자인 시스템 입문 | 🧩 | Component |
component |
| 모바일 디자인 | 📱 | Smartphone |
smartphone |
| 사용자 리서치 기초 | 🔬 | Microscope |
microscope |
| 레퍼런스와 무드보드 | 🗃️ | Images |
images |
| 포트폴리오 만들기 | 💼 | Briefcase |
briefcase |
| 개발 환경 설치 가이드 | 🛠️ | Wrench |
wrench |
| 윈도우 단축키 마스터 | ⌨️ | Command |
command |
| 윈도우 터미널과 PowerShell | 🖤 | SquareChevronRight |
square-chevron-right |
| VS Code 200% 활용 | 📝 | Code |
code |
| IntelliJ 꿀팁 모음 | 💡 | Lightbulb |
lightbulb |
| 크롬 개발자도구 | 🔎 | Inspect |
inspect |
| Gitea 200% 활용 | 🍵 | GitFork |
git-fork |
| Docker Desktop 사용법 | 🐳 | Ship |
ship |
| DBeaver로 DB 들여다보기 | 🦫 | DatabaseZap |
database-zap |
| Postman으로 API 테스트 | 🚀 | Send |
send |
| 캡처와 화면 녹화 | 📸 | Camera |
camera |
| 노션으로 기록하기 | 🗒️ | NotebookPen |
notebook-pen |
| 앱의 지형 | 📱 | LayoutTemplate |
layout-template |
| 안드로이드 앱의 구조 | 🤖 | TabletSmartphone |
tablet-smartphone |
| Kotlin 입문 | 🟣 | Blocks |
blocks |
| iOS 앱의 구조 | 🍎 | SquareDashedBottomCode |
square-dashed-bottom-code |
| Swift 입문 | 🐦 | Bird |
bird |
| React Native | ⚛️ | Orbit |
orbit |
| Flutter | 🦋 | Feather |
feather |
| 앱 화면과 이동 | 🧭 | Compass |
compass |
| 앱의 상태와 데이터 | 🗃️ | Layers |
layers |
| PWA — 웹앱을 앱처럼 | 🌐 | MonitorSmartphone |
monitor-smartphone |
| 앱 보안과 권한 | 🔐 | ShieldCheck |
shield-check |
| 스토어 배포 | 🚀 | Rocket |
rocket |
수준별 과정(초급~특급 48강좌) — frontend/src/levelCatalog.jsx
| 강좌 | 이전(이모지) | 이후(Lucide) | 아이콘 |
|---|---|---|---|
| 프로그램이란 무엇일까? | 💡 | Lightbulb |
lightbulb |
| 컴퓨터처럼 생각하기: 컴퓨팅 사고 | 🧩 | Puzzle |
puzzle |
| 나의 첫 웹페이지: HTML 첫걸음 | 🌐 | Globe |
globe |
| 태그로 페이지에 살 붙이기 | 🏗️ | Blocks |
blocks |
| 브라우저는 내 코드를 어떻게 읽을까 | 🔍 | Search |
search |
| 나의 첫 JavaScript | ✨ | Sparkles |
sparkles |
| 변수와 자료형: 이름 붙인 상자 | 📦 | Package |
package |
| 조건문: 컴퓨터가 판단하게 하기 | 🚦 | GitBranch |
git-branch |
| 반복문: 지루한 일은 컴퓨터에게 | 🔁 | Repeat |
repeat |
| 함수: 나만의 명령어 만들기 | 🧰 | SquareFunction |
square-function |
| 에러와 친해지기: 빨간 글씨 해독법 | 🩹 | Bandage |
bandage |
| 첫 프로젝트: 숫자 맞히기 게임 만들기 | 🚀 | Rocket |
rocket |
| 비동기 JavaScript: fetch와 Promise 정복 | ⏳ | Hourglass |
hourglass |
| 배열 메서드로 데이터 요리하기 | 🔪 | ChefHat |
chef-hat |
| REST API 설계: 주소와 동사의 문법 | 📐 | Ruler |
ruler |
| Express로 CRUD API 만들기 | 🛠️ | Hammer |
hammer |
| DB 연동과 트랜잭션: 데이터가 사는 집 | 🗄️ | Database |
database |
| 로그인 구현: 세션과 비밀번호 해시 | 🔐 | KeyRound |
key-round |
| React 상태와 이펙트 실전 | ⚛️ | Atom |
atom |
| React Router로 멀티 페이지 SPA | 🧭 | Compass |
compass |
| 폼 검증: 사용자를 돕고 서버를 지키기 | ✅ | CircleCheck |
circle-check |
| 에러 처리 패턴: 무너지지 않는 서비스 | 🚨 | TriangleAlert |
triangle-alert |
| 테스트 작성 실전: 자신 있게 고치는 힘 | 🧪 | FlaskConical |
flask-conical |
| 미니 프로젝트: 설계부터 배포까지 | 🚀 | Rocket |
rocket |
| 레이어드 아키텍처와 관심사 분리 | 🏛️ | Layers |
layers |
| 디자인 패턴 실전 | 🧩 | Puzzle |
puzzle |
| DB 인덱스와 실행계획 읽기 | 🔍 | ListTree |
list-tree |
| 트랜잭션과 격리 수준 | 🔒 | Lock |
lock |
| N+1과 쿼리 최적화 | 🐢 | Turtle |
turtle |
| 캐싱 전략 — 로컬과 Redis | ⚡ | Zap |
zap |
| 성능 측정과 프로파일링 | 📈 | TrendingUp |
trending-up |
| 웹 보안 심화 — OWASP Top 10 | 🛡️ | Shield |
shield |
| 인증 아키텍처 — 세션 vs JWT vs OAuth | 🔑 | Fingerprint |
fingerprint |
| 로깅·모니터링·알림 설계 | 🔭 | Telescope |
telescope |
| 무중단 배포와 롤백 | 🚦 | Milestone |
milestone |
| 코드 리뷰와 리팩터링 전략 | 🧭 | GitPullRequest |
git-pull-request |
| 분산 시스템 첫걸음 — 왜 서버 한 대로는 안 될까 | 🌐 | Network |
network |
| 메시지 큐와 이벤트 드리븐 — 시스템을 느슨하게 잇는 법 | 📨 | Inbox |
inbox |
| 모놀리스 vs 마이크로서비스 — 쪼개는 것의 진짜 비용 | 🧩 | Boxes |
boxes |
| DB 레플리케이션과 샤딩 — 데이터베이스를 늘리는 두 가지 축 | 🗄️ | DatabaseBackup |
database-backup |
| 대용량 트래픽 설계 — 밀려오는 요청 앞에서 무너지지 않기 | 🚦 | Gauge |
gauge |
| 쿠버네티스와 클라우드 네이티브 — 선언하면 알아서 굴러가는 인프라 | ☸️ | Hexagon |
hexagon |
| 관측성 — 보이지 않으면 고칠 수 없다 | 🔭 | Activity |
activity |
| SRE와 장애 대응 — 무너져도 빨리 일어나는 팀 | 🚨 | Siren |
siren |
| 시스템 설계 훈련 1 — URL 단축기를 처음부터 끝까지 | 🔗 | Link |
link |
| 시스템 설계 훈련 2 — 실시간 채팅 시스템 | 💬 | MessageSquare |
message-square |
| 시스템 설계 훈련 3 — 뉴스피드와 타임라인 | 📰 | Newspaper |
newspaper |
| 기술 리더십과 ADR — 결정을 설계하고 기록하는 사람 | 🧭 | Signpost |
signpost |
AWESOMEDEV · 아이콘 규약 v1 (Lucide 기준)