- Restore the step-0 interview as a mechanical gate the skill explicitly depends on; add harness.md (per-harness question tools, limits, fallbacks) and brief-interview.md (slots, question cards, rounds). - Add 10 reference docs absorbed from external design skills (accessibility, interaction-feel, elevation, color, icons, product-copy, component-systems, critique, change-review, print-email) and extend existing references. - Add a review-only route and two hard-gate clauses (truncated content reachability, three-flashes limit). - design-gate: split tap targets into WCAG 2.5.8 and 44px contract layers, run axe-core when available, and fix false positives found on a real site (decorative alt="", stacked wordmark line count, url-only pages). - lint-skill: fail if the interview gate section or its links disappear. - Ship agents/openai.yaml and THIRD_PARTY_NOTICES.md.
14 KiB
icons.md — 아이콘
4-1에서 아이콘을 쓸 때 읽는다. 텍스트·레이아웃과 나란히 놓이는 작은 그래픽이라 규칙 하나가 화면 전체의 아이콘 수만큼 반복된다. 여기서 정한 값이 어긋나면 어긋남도 그만큼 반복된다.
라벨은 셋 중 하나다. 하드 게이트는 위반하면 통과시키지 않는다. 프로젝트 계약은 브리프·플랫폼이 그 조건을 선택했을 때만 적용된다. 관찰 후보는 시작값이고, 실제 렌더에서 확인해 조정한다(실측 조정). 출처가 external 스킬 관찰값일 뿐 designpaca 자체 실측이 아닌 항목은 각주에 그대로 적는다.
이 문서를 읽는 법
| 상황 | 읽을 곳 |
|---|---|
| 아이콘 세트를 아직 못 골랐다 | §1 |
| 아이콘이 옆 글자보다 가늘거나 굵어 보인다 | §2 |
| 작은 크기에서 아이콘이 뭉개진다 | §3 |
| hover·selected·disabled 색을 어떻게 관리하나 | §4 |
| 토글·재생 버튼처럼 아이콘이 상태에 따라 바뀐다 | §5 |
| 아이콘+텍스트 버튼의 패딩이 어긋나 보인다 | §6 |
| RTL 로케일을 지원한다 | §7 |
| 아이콘만 있는 버튼의 접근성 이름 | §8 |
| 아이콘을 코드에서 어떻게 참조하나 | §9 |
| 감사 직전 | §10 |
0. 언제
이 문서는 4-1(레이아웃·타이포) 이후, 화면에 아이콘을 실제로 배치할 때 연다. 아이콘이 없는 화면이면 열지 않는다. 아이콘 전용 버튼의 접근성 이름 규칙(§8)만은 아이콘이 하나라도 있으면 예외 없이 적용된다.
1. 세트 일관성 — 관찰 후보
프로젝트에 이미 쓰는 아이콘 세트가 있으면 새 세트를 들이지 않고 그것을 따른다. package.json 의존성과 기존 import 문을 먼저 확인한다. 아직 없다면 한 세트를 고르고 프로젝트 전체에서 유지한다.
- 표면 하나에 두 세트를 섞지 않는다. 같은 툴바·같은 카드 안에서 선(line) 아이콘과 필(fill) 아이콘, 서로 다른 스트로크 관례가 섞이면 시스템이 없다는 증거로 읽힌다.
antipatterns.md의 "아이콘 세트가 섞임" 항목과 같은 판단이다. - 세트가 스트로크 굵기 변형을 지원하지 않으면 원래 굵기를 유지하고 §2의 매칭은 포기한다. 강조는 크기나 색으로 한다.
- 아이콘을 텍스트 옆에 인라인으로 둘 때는 텍스트의 cap height 기준
1em~1.25em을 시작 크기로 검토해 같은 배율로 커지게 한다[SKILL-BETTER-UI].
2. 스트로크와 글자 굵기 맞춤표 — 관찰 후보 (시작값)
인접 텍스트가 가늘면 굵은 아이콘이 소리치고, 인접 텍스트가 굵으면 가는 아이콘이 부러져 보인다. tokens.md의 웨이트 토큰(--weight-body 400 · --weight-medium 500 · --weight-strong 600)에 맞춰 시작값을 잡는다.
| 인접 텍스트 웨이트 | 아이콘 스트로크(24px 그리드 기준) |
|---|---|
--weight-body(400), 14~16px |
1.5px |
--weight-medium(500)~--weight-strong(600) |
2px |
| 700 이상, 또는 단독으로 강조되는 아이콘(로고형·경고) | 2.5px(토큰 밖 값이므로 실측 조정) |
/* 좋음: 라벨 웨이트에 맞춘 스트로크 */
.icon-medium { stroke-width: 2; } /* 옆 라벨이 --weight-medium 이상일 때 */
/* 나쁨: 굵은 라벨 옆에 기본 1.5px 스트로크를 그대로 둠 */
세트당 스트로크 굵기는 하나로 고정한다. 이 표는 24px 네이티브 그리드 기준이며, 다른 그리드를 쓰는 세트는 비례로 환산한 뒤 실제 렌더로 확인한다[SKILL-BETTER-UI].
3. 크기와 렌더 격자 — 관찰 후보
- 아이콘은 세트의 네이티브 그리드(
16·20·24) 중 하나로 렌더한다. 24px 원본을 16px로 임의 축소하면 내부 여백이 분수 픽셀에 걸려 흐리게 보인다. 세트가 16px 전용 심볼을 따로 제공하면 그것을 쓴다. - 실제로 렌더될 가장 작은 크기, 보통 16px에서 확인한다. 스크린샷이나 브라우저에서 실제 픽셀로 렌더한 뒤 아이콘의 내부 카운터(선으로 둘러싸인 열린 공간)가 막히지 않는지 본다. 48px에서 멀쩡해 보이던 아이콘이 16px에서 뭉개지는 일은 흔하다.
- 작은 자리에는 세부가 많은 그림보다 단순화된 심볼을 우선 검토한다.
- 래스터가 아니라 항상 SVG를 쓴다. 밀도가 다른 화면에서도 동일 자산이 선명하다[SKILL-BETTER-UI].
4. currentColor 리컬러링 — 관찰 후보 (엔지니어링 관행)
상태별로 별도 자산을 만들지 않는다. currentColor로 그린 SVG 하나를 두고, hover·selected·disabled는 CSS의 color·opacity로 구동한다.
.icon-button {
color: var(--ink-muted);
}
.icon-button:hover { color: var(--ink); }
.icon-button[aria-pressed="true"] { color: var(--accent); }
.icon-button:disabled { opacity: 0.4; }
<!-- 좋음: 자산 하나, 상태는 CSS가 결정한다 -->
<svg fill="none" stroke="currentColor" stroke-width="2">…</svg>
임포트한 아이콘에 fill="#666" 같은 하드코딩된 색이 박혀 있으면 currentColor로 바꾼다. 그대로 두면 위 상태 전환이 전부 무력화된다[SKILL-BETTER-UI].
5. 상태 쌍(외곽선·채움)과 전환 레시피
5-1. 외곽선 기본, 채움 활성 — 관찰 후보
세트가 outline·filled 두 변형을 제공하면 이것을 상태쌍으로 쓰고 서로 바꿔 쓰지 않는다.
| 변형 | 쓰임 |
|---|---|
| outline | 기본 상태 — 툴바, 목록 행, 텍스트 인라인 |
| filled | 선택/활성 상태 — 활성 탭, 토글된 북마크, 좋아요 표시 |
필 변형만 전체에 쓰면 활성 탭이 나머지와 구분되지 않는다[SKILL-BETTER-UI].
5-2. 전환은 정적 단서를 대신하지 않는다 — 하드 게이트
아이콘 전환(외곽선→채움, 재생→일시정지 같은 상태 전환)이 상태 변화를 전달하는 유일한 단서면 안 된다. 색이나 aria-pressed 같은 상태 속성이 애니메이션 재생 여부와 무관하게 항상 함께 있어야 한다. 이는 interaction-feel.md §1과 motion.md §5 "접근성 — 타협 없음"과 같은 요구이고, prefers-reduced-motion에서는 코드 H와 같은 원칙(정적 단서는 유지)을 따르되 scale 기반 전환이라 transition 자체를 끈다 — 전환이 없어도 상태는 즉시 바뀌어 보여야 한다.
5-3. 전환 레시피 — 관찰 후보 (시작값, 실측 조정)
컨텍스트에 따라 나타나거나 사라지는 아이콘(호버로 드러나는 액션, 토글 전환)은 표시 여부를 토글하는 대신 opacity·scale로 전환한다.
.icon-swap {
transition: opacity var(--dur-quick) var(--ease-out),
scale var(--dur-quick) var(--ease-out);
}
.icon-swap[data-hidden] {
opacity: 0;
scale: 0.25;
}
@media (prefers-reduced-motion: reduce) {
.icon-swap { transition: none; }
}
두 아이콘을 동시에 DOM에 유지하고 겹쳐 크로스페이드하면 마운트·언마운트 없이 매끄럽다. 한쪽은 position: absolute로 겹치고, 나머지 하나가 레이아웃 크기를 정의한다.
값에 대한 근거는 다음과 같다.
scale: 0.25 → 1,opacity: 0 → 1는 시작값이다[SKILL-BETTER-UI]. designpaca 고유 실측은 아니므로 실제 렌더에서 과장되어 보이는지 확인한다.blur()전환은 추가로 넣지 않는 것이 기본이다. 원 출처는blur(4px) → blur(0px)를 함께 쓰지만[SKILL-BETTER-UI],filter애니메이션은motion.md§2의 저비용 기본(transform/opacity)에 들지 않는다. opacity·scale만으로 상태가 충분히 구분되면 blur를 더하지 않는다. 꼭 필요하면 실제 기기에서 프레임 비용을 측정하고 그 근거를design.md에 남긴다.- 새 duration 토큰을 만들지 않는다. 원 출처의 스프링 값(
duration: 0.3s, bounce: 0)은tokens.md§4의--dur-quick(200ms)과--dur-normal(350ms) 사이다[SKILL-BETTER-UI]. 작은 요소 전환이므로--dur-quick에서 시작하고, 실제 렌더에서 너무 급해 보이면--dur-normal로 올린다. - 언제: 프로젝트가 이미 모션 라이브러리(예: Motion, Framer Motion)를 쓰고 있다면 그 라이브러리의 스프링을 대신 써도 된다. 그때도
bounce(또는 동등 파라미터)는 0에서 시작해 통통 튀지 않게 하고, 감소 모션 분기는 라이브러리 코드에도 그대로 넣는다.
5-4. 언제 애니메이션하나 — 관찰 후보
| 애니메이션 후보 | 애니메이션하지 않음 |
|---|---|
| 호버로 나타나는 액션 아이콘 | 항상 보이는 정적 내비 아이콘 |
| 상태 전환 아이콘(재생↔일시정지, 좋아요↔좋아요됨) | 장식용 아이콘 |
| 컨텍스트 툴바 안 아이콘 | 아이콘 옆 라벨 텍스트 |
6. 광학 정렬 — 관찰 후보 (시작값)
layout.md의 hitbox 정렬은 클릭 가능 영역의 기하학적 중심을 다룬다. 여기서 다루는 광학 정렬은 그 중심이 맞아도 눈에는 어긋나 보이는 경우다. 아이콘의 실제 잉크 무게중심이 기하학적 중심과 다르기 때문에 생긴다.
- 아이콘+텍스트 버튼: 아이콘 쪽 패딩을 텍스트 쪽 패딩보다
2px좁게 잡는 것을 시작값으로 검토한다(icon-side = text-side - 2px)[SKILL-BETTER-UI]. - 재생 삼각형: 삼각형은 기하학적으로 중앙이어도 왼쪽에 치우쳐 보인다.
translateX(2px)를 시작값으로 검토한다[SKILL-BETTER-UI]. - 별·화살표 같은 비대칭 아이콘: 세트가 SVG 내부에서 자체 보정을 제공하면 그것을 우선 쓰고, 없을 때만
margin으로 1px 안팎을 보정한다[SKILL-BETTER-UI].
모두 시작값이다. 실제 버튼 크기·서체·세트에서 어긋나 보이는지 확인하고, 어긋나 보이지 않으면 보정을 넣지 않는다.
7. RTL 뒤집기 표 — 프로젝트 계약 (조건부)
언제: 프로젝트가 RTL 로케일(아랍어·히브리어 등)을 지원하기로 했을 때만 연다. RTL을 지원하지 않는 프로젝트에는 이 절이 적용되지 않는다.
dir="rtl"에서 의미가 읽기 방향에 묶인 아이콘만 뒤집고 나머지는 그대로 둔다.
| 뒤집는다 | 뒤집지 않는다 |
|---|---|
| 뒤로/앞으로 화살표, 내비게이션 쉐브론 | 로고·브랜드 마크 |
| 정렬·목록·들여쓰기 같은 텍스트 블록 아이콘 | 체크마크 |
| 스피커/볼륨 파형(읽기 방향으로 퍼짐) | 시계·컵·연필 같은 실제 사물 |
| "보내기" 계열 방향성 아이콘 | 미디어 재생(재생·되감기는 테이프 방향 관례이며 LTR로 고정) |
/* 방향에 의미가 있는 아이콘만 좌우 반전한다 */
[dir="rtl"] .icon-directional {
scale: -1 1;
}
복합 아이콘은 부분별로 판단한다. 배지나 슬래시 오버레이는 바탕 도형이 뒤집혀도 위치를 유지해야 할 수 있다. 공간 배치·논리 속성 전반은 layout.md의 RTL 절을 함께 본다[SKILL-BETTER-UI].
8. 아이콘 전용 버튼의 이름 — 하드 게이트
텍스트 라벨 없이 아이콘만 있는 버튼·링크는 접근 가능한 이름이 있어야 한다. 모든 인터랙티브 컨트롤은 이름·역할·값을 가져야 한다는 요구다[WCAG-NRV]. 장식용으로 딸려 오는 SVG는 aria-hidden="true"로 스크린리더 트리에서 제외한다.
구체 패턴(aria-label vs 시각적으로 숨긴 텍스트, 언제 어느 쪽을 쓰는지, 검증 절차)은 이 문서가 아니라 accessibility.md가 정본이다. 아이콘 버튼을 구현했으면 반드시 그 문서의 접근 가능한 이름 절을 확인한다.
9. 참조 방식 — 관찰 후보 (엔지니어링 관행)
아이콘은 문자열 키로 런타임에 조회하지 않고, 컴포넌트·심볼을 직접 import해 참조한다.
// 나쁨: 문자열 키 조회 — 오타가 조용히 깨지고 번들러가 죽은 아이콘을 못 지운다
const iconMap = { check: CheckIcon, alert: AlertIcon };
function StatusIcon({ name }) { const Icon = iconMap[name]; return <Icon />; }
// 좋음: 컴포넌트를 값으로 직접 전달
function StatusIcon({ icon: Icon }) { return <Icon />; }
<StatusIcon icon={CheckIcon} />
문자열 키 조회는 타입 검사와 트리 셰이킹을 둘 다 깨뜨린다. 세트 전체가 번들에 남거나, 오타 난 키가 런타임까지 조용히 살아남는다[SKILL-SHADCN].
프로젝트에 이미 설치된 아이콘 라이브러리가 있으면 그 설정을 따르고, 특정 라이브러리를 기본값으로 가정하지 않는다. 언제: 프로젝트에 컴포넌트 라이브러리(예: shadcn 계열)가 감지되면 아이콘 참조 규칙이 그 라이브러리의 합성 규칙과 겹칠 수 있다 — component-systems.md를 함께 본다.
10. 체크리스트
- 아이콘 세트가 프로젝트 기존 세트와 같다. 한 표면에 두 세트가 섞이지 않았다
- 스트로크 굵기가 인접 텍스트의 웨이트 토큰과 맞거나, 세트 제약으로 못 맞추면 그 사실을 기록했다
- 가장 작게 렌더되는 크기(보통 16px)에서 실제로 확인했다
- 상태별 색이
currentColor+ CSS로 구동되고, 상태마다 별도 자산이 없다 - outline/filled 상태쌍이 있으면 일관되게 default/active로 쓰인다
- 아이콘 전환이 있으면 색·상태 속성 같은 정적 단서가 함께 있고,
prefers-reduced-motion에서 즉시 전환된다 - 아이콘+텍스트 버튼·비대칭 아이콘의 광학 정렬을 실제 렌더에서 확인했다
- RTL을 지원하는 프로젝트라면 방향성 아이콘만 뒤집었다
- 아이콘 전용 버튼에 접근 가능한 이름이 있다(
accessibility.md) - 아이콘을 문자열 키가 아니라 컴포넌트로 직접 참조했다