mirim-app/ICONS.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

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 기준)