feat(skill): absorb external design skills, restore interview gate, add review route
- 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.
This commit is contained in:
parent
79e79c120b
commit
6805fb2be7
37 changed files with 5688 additions and 128 deletions
563
packages/skill/references/accessibility.md
Normal file
563
packages/skill/references/accessibility.md
Normal file
|
|
@ -0,0 +1,563 @@
|
|||
# accessibility.md — 접근성 구현 계약
|
||||
|
||||
4단계 구현부터 참조하고, 5단계 프리플라이트의 접근성 표([preflight.md](preflight.md) §1)가 가리키는 정본이다. `preflight.md`는 체크리스트 행만 두고 여기를 가리킨다 — 이 문서는 체크리스트를 반복하지 않고 **왜 실패하는지, 어떤 코드로 고치는지, 무엇을 참조하는지**를 다룬다.
|
||||
|
||||
라벨은 셋 중 하나다. **하드 게이트**는 위반하면 통과시키지 않는다 — WCAG AA·키보드·포커스·대비·감소 모션·진실성처럼 규범 또는 기능에 근거한다. **프로젝트 계약**은 브리프·`design.md`·플랫폼이 그 조건을 선택했을 때만 적용된다 — AAA 기준, HIG·M3의 44/48px 같은 모바일 앱 수준 목표가 여기 속한다. **관찰 후보**는 스타일 휴리스틱의 시작값이며 실제 렌더에서 확인해 조정한다. 외부 스킬에서 가져온 수치는 "시작값"이라 적고, 근거가 약하면 "실측 조정"을 덧붙인다.
|
||||
|
||||
## 이 문서를 읽는 법
|
||||
|
||||
| 상황 | 읽을 곳 |
|
||||
|---|---|
|
||||
| AA와 AAA, 자동 점수와 실제 준수의 관계가 헷갈린다 | §0 |
|
||||
| `<div onClick>` 대신 뭘 써야 하는지, ARIA를 언제 쓰는지 | §1 |
|
||||
| 아이콘 전용 버튼, 인라인 SVG의 이름을 어떻게 붙이는지 | §2 |
|
||||
| 포커스 링 스타일·대비·가림 방지 | §3 |
|
||||
| 탭·메뉴·콤보박스·리스트박스·다이얼로그의 키보드 계약, 모달, SPA 라우팅 | §4 |
|
||||
| 스킵 링크·랜드마크·링크 텍스트 | §5 |
|
||||
| 라벨·자동완성·입력 타입·비활성화 규칙·오류 처리 | §6 |
|
||||
| 토스트·로딩 상태를 어떻게 알리는지 | §7 |
|
||||
| 터치 타깃 크기와 히트 영역 확장 | §8 |
|
||||
| 깜빡임·자동재생·호버 콘텐츠 | §9 |
|
||||
| 드래그로만 되는 조작의 대안 | §10 |
|
||||
| 비디오·오디오 자막 | §11 |
|
||||
| `prefers-*` 미디어 쿼리 중 뭐가 하드 게이트고 뭐가 점진적 향상인지 | §12 |
|
||||
| 감사를 어떤 순서로 돌리는지, MCP가 없을 때 뭘 쓰는지 | §13 |
|
||||
| 발견한 문제를 어떤 형식으로 보고하는지 | §14 |
|
||||
| 색·모션 단독 전달 금지(다른 문서가 이 문서를 정본으로 지목한 규칙) | 부록 A |
|
||||
|
||||
---
|
||||
|
||||
## 0. 판정 원칙
|
||||
|
||||
**하드 게이트.** WCAG 2.2는 A·AA·AAA 세 적합성 수준을 정의하며, 셋은 서로 다른 요구다 — AAA를 만족한다고 모두에게 접근 가능해지지는 않는다[WCAG-22]. designpaca는 **AA를 하드 게이트**로 삼는다. 이 문서의 모든 절은 별도 표기가 없는 한 AA 이하 기준을 하드 게이트로 취급한다.
|
||||
|
||||
**프로젝트 계약.** AAA 기준([WCAG-FOCUS-APPEAR] 2.4.13 등)과 HIG·M3의 44/48px 같은 플랫폼 권장은 브리프가 그 수준을 선택했을 때만 적용된다. "모바일 앱 수준"이라는 말이 브리프에 있으면 project contract로 올라간다(§8).
|
||||
|
||||
**자동 점수는 준수가 아니다.** Lighthouse 접근성 100점이나 axe 통과는 도구가 검출할 수 있는 하위 집합만 확인한 것이다. 100점이 WCAG 준수와 같지 않고, 낮은 점수도 그 자체로 이슈 증거를 대체하지 않는다[SKILL-WEB-A11Y]. 통과 판정은 점수가 아니라 §13의 감사 루프(키보드 조작·스크린리더 확인 포함)로 내린다.
|
||||
|
||||
**표준 기반 평가는 사용자 평가로 보완한다.** W3C 자신이 표준 기반 적합성 평가와 실제 사용자 평가를 함께 써야 한다고 설명한다 — 적은 표본이나 한 번의 평가를 모든 사용자에게 일반화하지 않는다[W3C-USER-EVAL]. 자동 검사·수동 APG 대조·스크린리더 확인을 전부 통과했더라도, 실사용자 피드백이 오면 그것으로 규칙을 다시 검토한다.
|
||||
|
||||
**APCA는 규범이 아니다.** 대비의 하드 게이트는 WCAG 2 비율이다. APCA는 2026년 기준으로도 WCAG 3 초안에 이름조차 실려 있지 않고 자체 문서가 스스로를 beta로 표시하는 평가 후보일 뿐이다[APCA-STATUS] — 상세는 [color.md](color.md) §6.
|
||||
|
||||
---
|
||||
|
||||
## 1. 시맨틱·네이티브 우선
|
||||
|
||||
**하드 게이트.** ARIA를 쓰기 전에 같은 의미·동작을 가진 네이티브 HTML 요소가 있는지 먼저 확인한다. 다섯 가지 ARIA 원칙이다[SKILL-BETTER-A11Y]:
|
||||
|
||||
1. 필요한 시맨틱·동작을 가진 네이티브 요소가 있으면 다른 요소를 ARIA로 흉내 내지 않고 그것을 쓴다.
|
||||
2. 정말 필요하지 않으면 네이티브 시맨틱을 바꾸지 않는다.
|
||||
3. 인터랙티브 ARIA 컨트롤은 반드시 키보드로 조작할 수 있어야 한다 — role은 그 위젯의 전체 키보드 모델을 지키겠다는 약속이다.
|
||||
4. 포커스를 받는 요소에 `role="presentation"`이나 `aria-hidden="true"`를 걸지 않는다.
|
||||
5. 모든 인터랙티브 요소는 접근 가능한 이름을 가져야 한다(§2).
|
||||
|
||||
나쁜 ARIA보다 ARIA가 없는 편이 낫다. 스크린리더는 role을 그대로 믿으므로, 틀린 role은 아무것도 없는 것보다 더 나쁘다.
|
||||
|
||||
| 요소 | 쓰는 곳 | 이유 |
|
||||
|---|---|---|
|
||||
| `<a href>` | 이동 — 어딘가로 가거나 URL이 바뀌는 모든 것 | Cmd/Ctrl/가운데 클릭, 우클릭으로 링크 복사, Enter로 활성화가 전부 공짜다 |
|
||||
| `<button>` | 액션 — 제출·토글·열기·삭제 | 포커스, Enter와 Space 둘 다로 활성화, 폼 시맨틱이 전부 공짜다 |
|
||||
| `<div onClick>` | 아무 데도 쓰지 않는다 | role도 포커스도 키보드도 없다. 스크린리더에는 그냥 텍스트다 |
|
||||
|
||||
```html
|
||||
<!-- 나쁨: 키보드와 스크린리더에서 보이지 않는다 -->
|
||||
<div onclick="openSettings()">설정</div>
|
||||
|
||||
<!-- 좋음: 포커스·Enter/Space 활성화·시맨틱이 전부 공짜다 -->
|
||||
<button onclick="openSettings()">설정</button>
|
||||
```
|
||||
|
||||
보이는 게 클릭 가능하면 실제로 클릭 가능해야 하고, 클릭 가능하면 실제 인터랙티브 요소여야 한다. 링크를 버튼으로, 버튼을 링크로 다시 만들면 사용자의 기대(가운데 클릭으로 새 탭 열기 등)가 깨진다. 이동하는 "버튼"은 스타일만 버튼인 `<a>`다[SKILL-BETTER-A11Y].
|
||||
|
||||
**네이티브 요소를 정말 쓸 수 없을 때만** `role="button"` + `tabindex="0"`의 완전한 폴리필을 쓴다(코드는 §4). 코드량 자체가 네이티브 요소보다 항상 많다는 사실이 네이티브를 우선해야 하는 실무적 이유이기도 하다.
|
||||
|
||||
**흔한 ARIA 실수**[SKILL-BETTER-A11Y]
|
||||
|
||||
| 실수 | 왜 실패하는가 |
|
||||
|---|---|
|
||||
| `<div>`나 `<span>`에 `aria-label` | 대부분의 스크린리더가 인터랙티브하지 않고 role도 없는 요소의 이름은 무시한다 |
|
||||
| `<button role="button">` | 중복 role. 잡음만 추가하고 이득이 없다 |
|
||||
| 포커스 가능한 요소에 걸린 `aria-hidden="true"` | Tab으로는 갈 수 있지만 스크린리더에는 존재하지 않는 요소가 생긴다 |
|
||||
| 존재하지 않는 id를 가리키는 `aria-labelledby`/`aria-describedby` | 조용히 이름·설명이 없는 것으로 처리된다 |
|
||||
| 내비게이션 목록에 `role="menu"` | `menu`는 앱 스타일 화살표 키 동작을 약속한다. 사이트 내비게이션은 `<nav>` + 리스트다 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 접근 가능한 이름
|
||||
|
||||
**하드 게이트.** 모든 인터랙티브 컨트롤은 접근 가능한 이름이 있어야 한다[WCAG-NRV]. 이름의 우선순위는 `aria-labelledby` > `aria-label` > 네이티브 라벨(`<label>`·텍스트 콘텐츠·`alt`) > `title` 속성이다. 가능하면 보이는 텍스트나 `aria-labelledby`를 쓴다 — `aria-label`은 화면에 보이지 않아 UI와 어긋나기 쉽고 번역 도구가 일관되게 처리하지 못한다[SKILL-BETTER-A11Y].
|
||||
|
||||
**아이콘 전용 버튼**은 텍스트 라벨이 없으므로 항상 이름이 필요하다. 아이콘 자체는 장식이므로 `aria-hidden="true"`로 접근성 트리에서 뺀다.
|
||||
|
||||
```html
|
||||
<!-- 좋음: 보이는 텍스트에서 이름이 나오고 아이콘은 숨긴다 -->
|
||||
<button>
|
||||
<svg aria-hidden="true">…</svg> 삭제
|
||||
</button>
|
||||
|
||||
<!-- 좋음: 아이콘만 있을 때는 명시적 이름 -->
|
||||
<button aria-label="삭제">
|
||||
<svg aria-hidden="true">…</svg>
|
||||
</button>
|
||||
```
|
||||
|
||||
**Label in Name — [WCAG-LABEL-NAME] (2.5.3).** 보이는 라벨과 접근 가능한 이름이 다르면 음성 제어 사용자가 깨진다. 버튼에 "보내기"라고 쓰여 있는데 `aria-label="메시지 전송"`을 걸면, "보내기 클릭"이라고 말하는 사용자의 명령이 인식되지 않는다. 접근 가능한 이름은 보이는 텍스트를 반드시 포함해야 한다[SKILL-BETTER-A11Y].
|
||||
|
||||
**인라인 SVG**는 장식인지 의미가 있는지로 마크업이 갈린다[SKILL-BETTER-A11Y].
|
||||
|
||||
```html
|
||||
<!-- 장식 아이콘: 이름을 가진 버튼 안에 있다 -->
|
||||
<button aria-label="닫기">
|
||||
<svg aria-hidden="true" focusable="false">…</svg>
|
||||
</button>
|
||||
|
||||
<!-- 독립된 의미 있는 아이콘 -->
|
||||
<svg role="img" aria-label="인증된 계정">…</svg>
|
||||
```
|
||||
|
||||
`focusable="false"`는 레거시 IE·Edge가 SVG를 탭 순서에 넣는 것을 막기 위한 것이다. 단순한 경우에는 `<img src="icon.svg" alt="…">`가 가장 안정적으로 전달된다.
|
||||
|
||||
**브랜드명·코드 토큰에는 `translate="no"`를 붙인다.** 한국어 로케일 프로젝트에서 영문 브랜드명·식별자가 브라우저 자동번역 확장에 의해 깨지는 것을 막는다[SKILL-BETTER-A11Y].
|
||||
|
||||
```html
|
||||
<span translate="no">designpaca</span>
|
||||
```
|
||||
|
||||
**모달·시트·드로어의 제목.** 열리는 표면은 항상 접근 가능한 제목을 가져야 한다. 시각적으로 숨기려면 `.sr-only`(정의는 [motion.md](motion.md) 코드 G)로라도 존재해야 하며, 완전히 생략하는 것은 [WCAG-NRV] 위반이다[SKILL-SHADCN]. 컴포넌트 시스템을 쓰는 프로젝트의 합성 규칙은 [component-systems.md](component-systems.md)를 따른다.
|
||||
|
||||
---
|
||||
|
||||
## 3. 포커스
|
||||
|
||||
**하드 게이트.** `:focus-visible`만 스타일링하고 `:focus` 단독을 쓰지 않는다. 브라우저는 키보드·보조기술 포커스에서만 `:focus-visible`을 보여 주고 마우스 클릭에서는 억제한다 — 클릭에서는 이미 포커스가 명백하기 때문이다. 검증된 대체 없이 `outline: none`을 쓰지 않는다. 이는 눈으로 보는 키보드 사용자의 내비게이션 자체를 지운다[SKILL-BETTER-A11Y].
|
||||
|
||||
```css
|
||||
/* 가장 안전: 브라우저 기본 링을 유지하고 여백만 준다 */
|
||||
:focus-visible { outline-offset: 2px; }
|
||||
|
||||
/* 브랜드 링이 필요할 때: 검증된 토큰을 쓴다 */
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--ring, currentColor);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
```
|
||||
|
||||
브라우저의 기본 포커스 링은 플랫폼·고대비 설정에 맞춰 스스로 적응한다. `outline-offset`만 얹으면 그 적응력을 유지한 채 여백만 얻는다. 커스텀 링을 쓰기로 했다면 `currentColor`나 토큰이 **닿는 모든 인접 색**(카드·히어로·다크모드 표면)에서 3:1 이상인지 직접 검증한다 — [WCAG-NONTEXT] (1.4.11)이 요구하는 것은 텍스트 대비가 아니라 인접 배경과의 대비다.
|
||||
|
||||
`:focus-within`은 래퍼가 켜져야 하는 경우에 쓴다 — 테두리 안에 아이콘이 있는 검색창처럼, 내부 입력이 포커스를 받으면 바깥 래퍼도 함께 밝아져야 할 때다[SKILL-BETTER-A11Y].
|
||||
|
||||
**포커스 가림 방지 — [WCAG-FOCUS-OBSCURED] (2.4.11, WCAG 2.2 신규, AA).** 키보드 포커스를 받은 요소가 스티키 헤더·고정 CTA 바에 완전히 가려지면 안 된다. 앵커 타깃과 포커스 가능 요소에 `scroll-margin-top`을 주되, 값을 손으로 맞추지 않고 [tokens.md](tokens.md)의 `--header-h` 토큰과 연결한다.
|
||||
|
||||
```css
|
||||
:focus-visible, [data-anchor-target] { scroll-margin-top: var(--header-h); }
|
||||
```
|
||||
|
||||
**`forced-colors: active` (Windows 고대비 모드).** 이 미디어 쿼리는 Widely available 상태다(확인 2026-09-24)[WEB-BASELINE]. 기본 색 조정을 유지하거나 `Highlight` 같은 시스템 색을 명명한다. `forced-color-adjust: none`은 저작한 색을 그대로 고정하므로, 강제 색 치환 후에도 실제로 지각 가능함을 검증한 곳에서만 쓴다[SKILL-BETTER-A11Y].
|
||||
|
||||
**프로젝트 계약 — [WCAG-FOCUS-APPEAR] (2.4.13, AAA).** 포커스 표시 영역이 비포커스 상태 컴포넌트 둘레 2px 두께 이상이고, 포커스·비포커스 상태 사이 대비가 3:1 이상이어야 한다는 더 엄격한 기준이다. 브리프가 AAA를 명시했을 때만 적용한다.
|
||||
|
||||
---
|
||||
|
||||
## 4. 키보드
|
||||
|
||||
### 4.1 tabindex 규칙
|
||||
|
||||
**하드 게이트.** `tabindex="0"`은 자연 탭 순서에 합류시킬 때만 쓴다 — 네이티브로 포커스되지 않는 커스텀 인터랙티브 요소 전용이다. `tabindex="-1"`은 JS로만 포커스할 대상(헤딩·모달 컨테이너·roving tabindex 멤버)에 쓴다. **양수 tabindex는 절대 쓰지 않는다** — 페이지 전체의 탭 순서를 납치한다. 순서가 틀렸으면 tabindex가 아니라 DOM 순서를 고친다[SKILL-BETTER-A11Y].
|
||||
|
||||
### 4.2 roving tabindex — 복합 위젯
|
||||
|
||||
탭·메뉴·툴바·라디오 그룹처럼 하나로 묶여 Tab 정지점 하나만 차지해야 하는 위젯은 활성 항목만 `tabindex="0"`, 나머지는 `tabindex="-1"`로 두고 화살표 키가 포커스와 `0`을 함께 옮긴다[SKILL-BETTER-A11Y].
|
||||
|
||||
```html
|
||||
<div role="tablist">
|
||||
<button role="tab" id="tab-1" tabindex="0" aria-selected="true">개요</button>
|
||||
<button role="tab" id="tab-2" tabindex="-1" aria-selected="false">가격</button>
|
||||
<button role="tab" id="tab-3" tabindex="-1" aria-selected="false">FAQ</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
```js
|
||||
// ArrowLeft/ArrowRight로 activeIndex를 옮기고, 옮긴 탭에만 tabindex=0을 준다
|
||||
function onTabsKeydown(e, tabs, activeIndex) {
|
||||
const delta = e.key === 'ArrowRight' ? 1 : e.key === 'ArrowLeft' ? -1 : 0;
|
||||
if (!delta) return;
|
||||
const next = (activeIndex + delta + tabs.length) % tabs.length; // 끝에서 wrap
|
||||
tabs[activeIndex].tabIndex = -1;
|
||||
tabs[next].tabIndex = 0;
|
||||
tabs[next].focus();
|
||||
return next;
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 커스텀 인터랙티브 요소의 키보드 핸들러
|
||||
|
||||
네이티브 요소를 정말 쓸 수 없을 때, `role="button"` + `tabindex="0"`의 완전한 폴리필은 클릭 리스너에 더해 `keydown`에서 Enter·Space를 처리한다. **기본 동작을 막지 않으면 Space가 페이지를 스크롤한다.**
|
||||
|
||||
```js
|
||||
el.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
activate();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**함정 — 네이티브 `<button>`에는 이 핸들러를 절대 추가하지 않는다.** 네이티브 버튼은 이미 Enter와 Space에서 `click` 이벤트를 발생시킨다. 같은 자리에 `keydown` 핸들러로 같은 액션을 또 호출하면, 키보드 사용자만 클릭이 두 번 실행되는(이중 트리거) 버그가 생긴다. 이 핸들러는 `role="button"`으로 흉내 낸 커스텀 요소 전용이다[SKILL-WEB-A11Y].
|
||||
|
||||
### 4.4 ARIA APG 키보드 패턴표
|
||||
|
||||
네이티브 요소는 아래 동작을 공짜로 갖는다. 커스텀 위젯에 role을 붙였다면 (선택)으로 표시한 동작을 뺀 나머지는 전부 구현해야 한다 — role은 그 위젯의 전체 키보드 모델을 지키겠다는 약속이다[SKILL-BETTER-A11Y]. 각 행은 [WAI-ARIA APG](https://www.w3.org/WAI/ARIA/apg/)의 해당 패턴 페이지로 1차 확인됐다[ARIA-APG].
|
||||
|
||||
| 위젯 | 키 계약 |
|
||||
|---|---|
|
||||
| Dialog | 여는 순간 다이얼로그 안 요소로 포커스 이동(파괴적 확인은 가장 덜 위험한 액션으로). Tab/Shift+Tab이 안에서만 순환(끝에서 wrap). Escape로 닫힘 |
|
||||
| Tabs | 화살표 키(세로형은 Up/Down)로 탭 간 이동(wrap). Tab은 탭 목록을 벗어나 패널로 간다. Home/End로 처음/끝 탭. 활성화는 자동(포커스 즉시 전환) 또는 수동(Enter/Space로 전환) 중 선택 |
|
||||
| Menu button | Enter/Space가 열고 첫 항목에 포커스. (선택) ArrowDown이 열고 첫 항목, ArrowUp이 열고 마지막 항목. Escape가 닫고 버튼에 포커스 복귀 |
|
||||
| Disclosure/accordion | 헤더는 `<button aria-expanded>`. Enter·Space가 토글 |
|
||||
| Combobox | ArrowDown이 팝업을 열거나 안으로 이동. Enter가 자동완성 제안을 받아들임. Escape가 팝업을 닫고 입력으로 돌아감. 타이핑이 필터링. 팝업이 listbox/grid/tree/dialog인지에 따라 내부 탐색 방식이 달라진다 |
|
||||
| Listbox/radio group | 화살표 키가 선택을 옮긴다(단일 선택 시). Home/End(5개 이상일 때 권장)로 처음/끝. 그룹 전체가 Tab 정지점 하나 |
|
||||
|
||||
보편 규칙: Escape는 가장 최근에 연 것부터 닫는다(툴팁 → 메뉴 → 다이얼로그 순). 화살표 키는 위젯 **내부**를 움직이고, Tab은 위젯 **사이**를 움직인다. `<textarea>`에서 Enter는 줄바꿈이고 ⌘/Ctrl+Enter가 제출이다[SKILL-BETTER-A11Y].
|
||||
|
||||
### 4.5 모달 포커스 트랩·inert·복귀
|
||||
|
||||
**네이티브 `<dialog>` + `showModal()`을 우선 고려한다.** 트랩·배경 `inert`·Escape 처리를 공짜로 얻는다. `<dialog>`는 Widely available 상태다(확인 2026-09-24, baseline 2022-03-14)[WEB-BASELINE]. 기하 계약(위치·중심·scroll owner)은 [audit-gate.md](audit-gate.md)와 [preflight.md](preflight.md) §4-1이 정하므로 여기서 반복하지 않는다. 이 절은 **포커스 관리**만 다룬다.
|
||||
|
||||
네이티브를 쓸 수 없는 커스텀 오버레이는 `role="dialog"` + `aria-modal="true"` + `aria-labelledby`(제목 id)를 갖추고, 아래를 구현한다[SKILL-BETTER-A11Y]:
|
||||
|
||||
```js
|
||||
// 열 때: 배경을 inert로 만들고 첫 포커스 가능 요소로 이동한다
|
||||
document.getElementById('app-content').inert = true; // 배경을 탭 순서·보조기술 양쪽에서 제거한다
|
||||
const dialog = dialogRef;
|
||||
(dialog.querySelector('[autofocus]') ??
|
||||
dialog.querySelector('button, [href], input, select, textarea'))?.focus();
|
||||
|
||||
// 닫을 때: 배경을 복구하고 포커스를 연 요소로 되돌린다
|
||||
document.getElementById('app-content').inert = false;
|
||||
triggerEl?.focus();
|
||||
```
|
||||
|
||||
`inert` 속성은 Widely available 상태다(확인 2026-09-24, baseline 2023-04-11)[WEB-BASELINE]. 파괴적 확인 다이얼로그는 첫 포커스를 가장 덜 위험한 액션(대개 취소)에 둔다. 다이얼로그에 `overscroll-behavior: contain`을 걸어, 안에서 스크롤해도 배경 페이지가 함께 스크롤되지 않게 한다.
|
||||
|
||||
### 4.6 SPA 라우트 변경
|
||||
|
||||
**언제:** 클라이언트 사이드 라우팅(SPA)을 쓸 때. 라우트 전환은 포커스와 알림을 저절로 리셋하지 않는다. 라우트가 바뀌면 `document.title`을 새 맥락에 맞게 갱신하고, 새 뷰의 `<h1 tabindex="-1">` 또는 `<main>`으로 포커스를 옮긴다. 뒤로/앞으로 내비게이션은 스크롤 위치를 복원하고, 앞으로 이동은 맨 위로 스크롤한다[SKILL-BETTER-A11Y].
|
||||
|
||||
---
|
||||
|
||||
## 5. 구조
|
||||
|
||||
**하드 게이트 — 스킵 링크.** 반복되는 내비게이션을 건너뛸 수 있는 링크를 페이지 첫 포커스 가능 요소로 둔다. 포커스 전에는 화면 밖에 숨긴다.
|
||||
|
||||
```css
|
||||
.skip-link { position: absolute; inset-inline-start: -999px; }
|
||||
.skip-link:focus { inset-inline-start: 16px; top: 16px; }
|
||||
```
|
||||
|
||||
```html
|
||||
<body>
|
||||
<a class="skip-link" href="#main-content">본문으로 건너뛰기</a>
|
||||
<header>…</header>
|
||||
<main id="main-content" tabindex="-1">…</main>
|
||||
</body>
|
||||
```
|
||||
|
||||
**하드 게이트 — 랜드마크.** 보이는 주 `<main>` 랜드마크 하나를 노출한다. `<header>`·`<nav>`·`<aside>`·`<footer>`는 스크린리더 사용자가 랜드마크 사이를 건너뛰며 이동할 수 있게 한다. 같은 타입 랜드마크가 여러 개면 구분 레이블을 단다.
|
||||
|
||||
```html
|
||||
<nav aria-label="주 메뉴">…</nav>
|
||||
<nav aria-label="브레드크럼">…</nav>
|
||||
```
|
||||
|
||||
`div`로 짠 레이아웃을 시맨틱 랜드마크로 바꾸는 것 자체가 흔히 빠뜨리는 항목이다[SKILL-WEB-A11Y].
|
||||
|
||||
**헤딩 순서.** h1 하나, 레벨을 건너뛰지 않는다(기준은 [preflight.md](preflight.md) §1). 헤딩은 구조이지 스타일이 아니다 — 크기 때문에 태그를 고르지 말고, 레벨은 문서 구조로 정한 뒤 시각적 크기는 CSS로 별도로 입힌다[SKILL-BETTER-A11Y].
|
||||
|
||||
**링크 텍스트 — [WCAG-LINK] (2.4.4).** 링크의 목적은 링크 텍스트만으로, 또는 텍스트와 프로그램적으로 확인 가능한 맥락을 합쳐서 알 수 있어야 한다. 스크린리더의 링크 목록 모드는 맥락 없이 텍스트만 읽으므로, "더보기"·"여기"·"클릭"을 단독으로 쓰지 않고 목적어를 포함한다("가격표 보기"). 실제 카피 문구·톤은 [product-copy.md](product-copy.md)를 따른다. 새 탭으로 여는 링크는 `rel="noopener"`와 함께 "새 탭에서 열림"을 알리는 숨김 텍스트(`.sr-only`)를 둔다.
|
||||
|
||||
---
|
||||
|
||||
## 6. 폼
|
||||
|
||||
### 6.1 레이블
|
||||
|
||||
**하드 게이트.** 모든 컨트롤은 프로그램적 레이블이 있어야 한다 — `id`를 가리키는 `<label for>` 또는 감싸는 `<label>`. **placeholder는 레이블이 아니다.** 입력하는 순간 사라지고 대개 대비 기준도 통과하지 못한다[SKILL-BETTER-A11Y].
|
||||
|
||||
```html
|
||||
<label for="email">이메일</label>
|
||||
<input id="email" type="email" autocomplete="email" />
|
||||
|
||||
<!-- 감싸는 레이블: 레이블과 컨트롤이 같은 히트 영역을 공유한다 -->
|
||||
<label>
|
||||
<input type="checkbox" /> 소식 받기
|
||||
</label>
|
||||
```
|
||||
|
||||
필수 필드는 네이티브 `required`와 폼당 한 번 설명하는 보이는 표시("* 필수")를 함께 쓴다. 레이블에 더해 입력 형식 예시를 보여주는 것은 placeholder의 정당한 용도다(`placeholder="name@company.com"`).
|
||||
|
||||
### 6.2 자동완성 — [WCAG-INPUT-PURPOSE] (1.3.5, AA)
|
||||
|
||||
**하드 게이트.** 사용자 정보를 묻는 입력은 의미 있는 `autocomplete` 값을 가져야 한다는 WCAG 요구다. 값을 지어내지 않고 아래 표를 따른다[SKILL-BETTER-A11Y].
|
||||
|
||||
| 필드 | `autocomplete` |
|
||||
|---|---|
|
||||
| 이름 | `name`(또는 `given-name`/`family-name`) |
|
||||
| 이메일 | `email` |
|
||||
| 전화번호 | `tel` |
|
||||
| 주소 | `street-address`, `address-line1`, `postal-code`, `country` |
|
||||
| 카드 | `cc-number`, `cc-exp`, `cc-csc`, `cc-name` |
|
||||
| 로그인 | `username`, `current-password` |
|
||||
| 가입/재설정 | `new-password` |
|
||||
| 2단계 인증 코드 | `one-time-code` |
|
||||
|
||||
섹션이 여러 개일 때는 접두어를 붙인다: `autocomplete="shipping street-address"`.
|
||||
|
||||
### 6.3 입력 타입·inputmode
|
||||
|
||||
올바른 `type`·`inputmode`가 맞는 모바일 키보드를 소환한다[SKILL-BETTER-A11Y].
|
||||
|
||||
| 입력 | 쓸 것 |
|
||||
|---|---|
|
||||
| 이메일·URL·전화 | `type="email"`, `type="url"`, `type="tel"` |
|
||||
| OTP/PIN/카드번호 | `type="text" inputmode="numeric"`(텍스트 시맨틱 유지, 스피너 없음) |
|
||||
| 금액·소수 | `type="text" inputmode="decimal"` |
|
||||
| 진짜 수량(정수 스피너가 맞는 값) | `type="number"` |
|
||||
|
||||
이메일·코드·사용자명에는 `spellcheck="false"`를 건다. 값 자체는 사용자가 붙여넣게 두고, 타이핑을 막거나 문자를 필터링하지 않는다 — 비밀번호·OTP는 붙여넣기가 표준 사용 방식이다. 진짜 `<form>`·정확한 `autocomplete`·가짜 입력 금지로 비밀번호 관리자·2FA 자동입력과 계속 호환되게 유지한다[SKILL-BETTER-A11Y].
|
||||
|
||||
### 6.4 접근 가능한 인증 — [WCAG-AUTH] (3.3.8, AA, WCAG 2.2 신규)
|
||||
|
||||
**언제:** 로그인·가입 플로우를 구현할 때. 로그인은 인지 기능 테스트(비밀번호 암기·퍼즐)만으로 막지 않는다. 다음 중 최소 하나가 함께 있어야 한다: (a) 인지 기능 테스트에 의존하지 않는 대안(패스키·SSO·이메일 링크), (b) 테스트를 돕는 메커니즘(붙여넣기 허용, 자동완성), (c) 객체 인식이나 사용자가 제공한 콘텐츠 인식 기반 테스트[WCAG-AUTH]. 최소한 붙여넣기와 정확한 `autocomplete`는 항상 지킨다.
|
||||
|
||||
### 6.5 중복 입력 방지 — [WCAG-REDUNDANT] (3.3.7, A, WCAG 2.2 신규)
|
||||
|
||||
**언제:** 다단계 폼(체크아웃·가입 등)을 구현할 때. 같은 세션에서 이미 받은 정보를 다시 입력하라고 강요하지 않는다 — 배송지=청구지 자동 채움 체크박스처럼 자동으로 채우거나 선택할 수 있게 한다. 예외는 보안 재확인과 만료된 정보뿐이다.
|
||||
|
||||
### 6.6 제출 버튼
|
||||
|
||||
**프로젝트 계약** — WCAG가 직접 요구하지는 않는 강한 기본값이라는 이유로 하드 게이트가 아니라 프로젝트 계약으로 둔다. 제출 버튼은 요청이 시작되기 전까지 활성 상태를 유지한다. **폼이 유효해질 때까지 미리 비활성화하지 않는다** — 사용자가 어떤 필드가 문제인지 알 방법이 없어진다. 요청이 시작된 뒤에만 비활성화하고, 원래 레이블 옆에 스피너를 둔다("저장" + 스피너이지, 스피너만이 아니다. 레이블이 있어야 보조기술이 어떤 버튼이 바쁜지 안다)[SKILL-BETTER-A11Y].
|
||||
|
||||
**하드 게이트** — 비활성화 때문에 오류를 확인할 방법 자체가 사라지면 오류 식별 실패로 하드 게이트다.
|
||||
|
||||
### 6.7 disabled vs aria-disabled — 결정표
|
||||
|
||||
**프로젝트 계약** — WCAG가 직접 요구하지는 않는 강한 기본값이라는 이유로 하드 게이트가 아니라 프로젝트 계약으로 둔다.
|
||||
|
||||
| 상황 | 쓸 것 | 결과 |
|
||||
|---|---|---|
|
||||
| 컨트롤이 진짜로 쓸 수 없는 상태(네이티브 지원 있음) | `disabled` | 탭 순서에서 빠짐, 활성화 억제, `:disabled` 스타일 적용, 폼 제출에서 제외됨 — 전부 자동 |
|
||||
| 이유를 옆에 텍스트로 보여줘야 하거나, 비활성 상태에서도 탭 순서·hover에 남겨야 함(툴팁이 필요할 때) | `aria-disabled="true"` | 상태만 announce된다. 포커스 가능성·동작·스타일은 바뀌지 않으므로 코드·CSS로 직접 막아야 한다 |
|
||||
| 커스텀 컨트롤이라 네이티브 `disabled`가 없음 | `aria-disabled="true"` | 위와 동일 |
|
||||
|
||||
**함정 — 네이티브 `disabled` 위의 툴팁은 절대 열리지 않는다.** `disabled`는 포인터 이벤트를 억제하고 탭 순서에서 빠지므로, 키보드·터치에서는 열릴 방법이 없고 마우스에서도 신뢰할 수 없다. 이유를 설명해야 한다면 `aria-disabled="true"`로 바꾸거나 이유를 옆에 보이는 텍스트로 둔다. **`disabled`와 `aria-disabled`를 같은 요소에 동시에 설정하지 않는다.** `aria-disabled`를 쓸 때는 핸들러에서 포인터·키보드 활성화를 직접 막고, 폼 제출도 필요하면 직접 막고, forced-colors 대응 스타일을 포함한다. 비활성 컨트롤은 대비 최소 기준의 예외지만 그래도 읽을 수 있게 유지한다(역할 정의는 [color.md](color.md) §8 "비활성 텍스트" 역할)[SKILL-BETTER-A11Y].
|
||||
|
||||
### 6.8 오류 처리
|
||||
|
||||
**하드 게이트.** 완전한 오류 패턴이다[SKILL-BETTER-A11Y]:
|
||||
|
||||
```html
|
||||
<label for="email">이메일</label>
|
||||
<input
|
||||
id="email" type="email" autocomplete="email"
|
||||
aria-invalid="true" aria-describedby="email-error"
|
||||
/>
|
||||
<p id="email-error">올바른 이메일 주소를 입력하세요.</p>
|
||||
```
|
||||
|
||||
- `aria-invalid="true"`는 실패한 필드에만, 고치면 제거한다.
|
||||
- `aria-describedby`가 필드와 인라인 오류를 연결해 스크린리더가 필드와 함께 오류를 읽게 한다.
|
||||
- 오류는 필드 옆에 인라인으로, 아이콘이나 텍스트와 함께 렌더한다. **빨간 테두리만으로는 안 된다** — 색상 단독 신호다.
|
||||
- 제출 시 **첫 번째로 실패한 필드에 포커스를 이동한다.** 포커스 이동 자체가 announcement이므로 별도 알림이 필요 없다.
|
||||
- 제출 전 값은 trim한다 — 자동완성과 텍스트 확장이 뒤 공백을 남긴다.
|
||||
- 폼 레벨 오류(특정 필드에 묶이지 않는 실패)에만 `role="alert"`를 예약한다(§7).
|
||||
|
||||
**관찰 후보 — 검증 시점 [SKILL-APPLE-DESIGN].** 필드를 벗어날 때(blur) 또는 타이핑 중 짧은 디바운스 후 검증하고, 제출 버튼을 누른 뒤에야 오류를 보여주지 않는다. 불완전한 상태로도 제출을 허용해 검증이 드러나게 한다 — 타이핑 자체를 막거나 문자를 필터링하지 않는다.
|
||||
|
||||
### 6.9 세션 타이밍
|
||||
|
||||
**언제:** 세션 타임아웃이 있는 웹앱일 때. 시간 제한은 연장할 수 있어야 한다 — 만료 전 경고 모달에 "연장" / "로그아웃" 선택지를 준다.
|
||||
|
||||
---
|
||||
|
||||
## 7. 상태 메시지 — [WCAG-STATUS] (4.1.3, AA)
|
||||
|
||||
**하드 게이트.** 페이지가 새로고침 없이 바뀌는 콘텐츠(토스트·검증 결과·검색 결과 수·로딩 상태)를 어떻게 알릴지는 아래 순서로 판정하고, **첫 번째로 맞는 항목에서 멈춘다**[SKILL-BETTER-A11Y]:
|
||||
|
||||
1. **포커스가 이미 그곳으로 이동한다** — 열린 모달, 첫 실패 필드처럼. 포커스 이동 자체가 announcement다. 추가 조치가 필요 없다.
|
||||
2. **특정 컨트롤에 묶여 있다** — 필드 오류, 글자 수 카운터처럼. 해당 컨트롤의 `aria-describedby`로 연결한다.
|
||||
3. **긴급하지 않고 특정 컨트롤에 묶이지 않는다** — 토스트, "저장됨", 결과 개수, 로딩 상태처럼. `role="status"`(polite, 말이 멈추길 기다림).
|
||||
4. **긴급하고 특정 컨트롤에 묶이지 않는다** — 폼 레벨 실패, 세션 만료처럼. `role="alert"`(assertive, 즉시 인터럽트).
|
||||
|
||||
| 메커니즘 | 결합 | 언제 |
|
||||
|---|---|---|
|
||||
| `role="status"` | `aria-live="polite"` + `aria-atomic="true"` | 토스트, "저장됨", 결과 수, 로딩 갱신 |
|
||||
| `role="alert"` | `aria-live="assertive"` + `aria-atomic="true"` | 긴급한 오류뿐. 과용이 가장 흔한 실수다 — 사용자가 읽던 것을 인터럽트한다 |
|
||||
|
||||
**재알림 트릭.** 반복되는 polite 업데이트는 DOM에 **안정된 빈 영역**을 미리 두고 그 텍스트만 바꾼다. 매번 콘텐츠를 담은 새 polite 영역을 삽입하면 announce가 일관되지 않는다.
|
||||
|
||||
```html
|
||||
<!-- 처음부터 렌더된 영역, 메시지는 나중에 주입 -->
|
||||
<div role="status" class="sr-only" id="status-region"></div>
|
||||
```
|
||||
|
||||
로딩 상태는 갱신되는 영역에 `aria-busy="true"`를 걸고, "불러오는 중…"을 polite로 알린 뒤 결과("12개 결과를 불러왔습니다")를 announce한다.
|
||||
|
||||
**토스트 특화 규칙**[SKILL-BETTER-A11Y]:
|
||||
|
||||
- **토스트로 포커스를 옮기지 않는다.** announce만 하고 포커스는 사용자가 작업하던 곳에 둔다.
|
||||
- hover 또는 focus가 타이머를 일시정지한다.
|
||||
- 자동 소멸은 저위험 확인에만 어울린다. 타임아웃을 쓴다면 **5초가 바닥값**이다.
|
||||
- **액션·오류·놓치면 안 되는 정보를 담은 토스트는 자동으로 사라지지 않는다.** 유일한 undo 경로가 담긴 토스트가 시간이 지나 사라지는 것은 예정된 데이터 손실이다.
|
||||
|
||||
---
|
||||
|
||||
## 8. 히트 영역 — [WCAG-TARGET] (2.5.8, AA)
|
||||
|
||||
**하드 게이트.** 포인터 입력 대상은 최소 **24×24 CSS px**이거나 아래 예외 중 하나에 해당해야 한다.
|
||||
|
||||
**간격 예외의 정확한 정의**: 24px 미만인 대상도, 그 바운딩박스 중심에 24px 지름 원을 그렸을 때 그 원이 다른 대상이나 다른 미달 대상의 원과 겹치지 않으면 통과한다[WCAG-TARGET]. 단순한 경우로는 20px 대상 사이에 4px 간격이 있으면 충분하다. 그 밖의 예외는 동등 기능을 제공하는 다른 컨트롤(Equivalent), 문장 안 인라인 텍스트(Inline), 사용자 에이전트가 정한 크기(User Agent Control), 특정 표현이 필수이거나 법적으로 요구되는 경우(Essential)다.
|
||||
|
||||
**프로젝트 계약.** HIG의 44×44pt[PLATFORM-HIG-TARGET], Material Design의 48×48dp[PLATFORM-M3-TARGET]는 "모바일 앱 수준"을 브리프가 요구할 때의 계약이다 — WCAG AA의 보편 요건으로 확대하지 않는다. 상세는 [mobile-app-ux.md](mobile-app-ux.md).
|
||||
|
||||
**히트 영역 확장.** 보이는 요소가 작을 때(20×20 체크박스 등), 감싸는 `<label>`이나 `<button>`에 의사요소로 히트 영역을 넓힌다. **`<input>` 자체에는 걸지 않는다** — 대체 요소는 `::before`/`::after`를 안정적으로 렌더하지 않는다[SKILL-BETTER-A11Y].
|
||||
|
||||
아래 44px는 모바일 앱 수준 계약일 때의 값이다. 그 밖에는 24px와 간격 예외로 충분하다.
|
||||
|
||||
```css
|
||||
.checkbox-label {
|
||||
position: relative;
|
||||
width: 20px; height: 20px;
|
||||
}
|
||||
.checkbox-label::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%; left: 50%;
|
||||
transform: translate(-50%, -50%); /* 물리적 중앙 정렬 — 방향과 무관 */
|
||||
width: 44px; height: 44px;
|
||||
}
|
||||
```
|
||||
|
||||
실제 박스 크기를 키울 여유가 있으면 의사요소 대신 박스 자체를 키운다 — 브라우저가 스크롤·제스처에 쓸 진짜 geometry를 갖게 된다.
|
||||
|
||||
```css
|
||||
.icon-button {
|
||||
min-width: 44px; min-height: 44px;
|
||||
display: inline-grid; place-items: center;
|
||||
}
|
||||
```
|
||||
|
||||
**충돌 규칙.** 확장된 히트 영역이 다른 인터랙티브 요소와 겹치면, 충돌하지 않는 최대 크기로 의사요소를 줄인다. 두 인터랙티브 요소의 히트 영역은 절대 겹치지 않는다.
|
||||
|
||||
**장식 레이어.** 인터랙티브 콘텐츠 위에 그려진 그라디언트 스크림·글로우·블러 시트 같은 장식 레이어는 자신이 덮은 영역의 포인터 이벤트를 전부 삼킨다 — 아래 컨트롤은 살아 있는 것처럼 보이지만 아무 반응이 없다. `pointer-events: none`으로 이벤트가 아래로 통과하게 하고, `aria-hidden="true"`로 접근성 트리에서도 뺀다[SKILL-BETTER-A11Y]. `antipatterns.md`의 "컬러 글로우" 항목이 시각 판단만 다루는 곳에 이 규칙을 더한다.
|
||||
|
||||
```css
|
||||
.card-glow {
|
||||
position: absolute; inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
```
|
||||
|
||||
클릭으로 닫는 모달 스크림처럼 사용자가 실제로 눌러야 하는 레이어는 예외다 — 그건 장식이 아니라 컨트롤이다.
|
||||
|
||||
**터치 동작.** 인터랙티브 요소에 `touch-action: manipulation`을 걸어 모바일의 더블탭 확대 지연을 없앤다. 자체 팬·줌·드래그를 구현하는 표면에는 `touch-action: none`을 그 표면에만 스코프해서 건다(페이지 레벨에 걸면 스크롤 자체가 사라진다). hover 전용 스타일은 `@media (hover: hover)`로 게이트한다 — 터치에서는 `:hover`가 탭 뒤에 눌러붙어 고정된 선택 상태처럼 보인다[SKILL-BETTER-A11Y].
|
||||
|
||||
---
|
||||
|
||||
## 9. 움직임·깜빡임·호버 콘텐츠
|
||||
|
||||
**하드 게이트 — 깜빡임 한도 [WCAG-FLASH] (2.3.1, A).** 초당 3회를 넘게 깜빡이거나 일반/적색 섬광 임계값을 넘는 콘텐츠는 만들지 않는다. 펄스형 상태 점, 글로우 애니메이션([svg-filters.md](svg-filters.md)), 스트로브형 모션이 이 한도 안에 있는지 확인한다. 5초 넘게 자동으로 움직이는 콘텐츠의 정지 수단(2.2.2)은 [motion.md](motion.md)가 정본이므로 여기서 반복하지 않는다.
|
||||
|
||||
**하드 게이트 — 호버·포커스로 나타나는 콘텐츠 [WCAG-HOVER] (1.4.13, AA).** 툴팁·팝오버처럼 호버나 포커스로 나타나는 부가 콘텐츠는 세 조건을 모두 만족해야 한다:
|
||||
|
||||
- **Dismissible(해제 가능)** — 추가 호버·포커스 없이 사용자가 닫을 수 있다(Esc 등). [preflight.md](preflight.md) §4-1이 이미 다룬다.
|
||||
- **Hoverable(호버 가능)** — 포인터가 트리거를 벗어나 콘텐츠 쪽으로 이동해도 사라지지 않는다.
|
||||
- **Persistent(지속)** — 호버·포커스가 유지되는 한, 시간 제한 없이 유지된다(사용자가 닫거나 정보가 무효해질 때까지).
|
||||
|
||||
```css
|
||||
/* 호버 가능: 트리거와 콘텐츠 사이에 죽은 공간이 없어야 포인터 이동 중 사라지지 않는다 */
|
||||
.tooltip-trigger:hover .tooltip,
|
||||
.tooltip-trigger:focus-within .tooltip,
|
||||
.tooltip:hover { visibility: visible; }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 드래그 대안 — [WCAG-DRAG] (2.5.7, AA, WCAG 2.2 신규)
|
||||
|
||||
**언제:** 정렬 리스트·슬라이더·지도 팬·컬러피커처럼 드래그 제스처로 조작하는 위젯을 만들 때. 드래그 동작을 요구하는 모든 기능은 드래그 없이 단일 포인터(클릭 등)로도 실행할 수 있어야 한다. 예외는 드래그 자체가 본질적으로 필요한 경우(자유 곡선 그리기 등)와, 사용자 에이전트가 정한 동작(브라우저 스크롤·pull-to-refresh 등 저작자가 수정하지 않은 것)이다[WCAG-DRAG].
|
||||
|
||||
```html
|
||||
<!-- 드래그로 순서를 바꾸는 리스트에는 버튼 대안을 함께 둔다 -->
|
||||
<li>
|
||||
<span>항목 A</span>
|
||||
<button aria-label="위로 이동">↑</button>
|
||||
<button aria-label="아래로 이동">↓</button>
|
||||
</li>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. 미디어 자막·트랜스크립트
|
||||
|
||||
**언제:** 비디오·오디오 콘텐츠를 프로젝트에 포함할 때. 사전 녹화된 비디오는 자막이 필요하고, 오디오는 트랜스크립트를 제공한다. 소리가 있는 콘텐츠는 절대 자동재생하지 않고, 컨트롤은 항상 렌더한다[SKILL-BETTER-A11Y].
|
||||
|
||||
```html
|
||||
<video controls>
|
||||
<source src="demo.mp4" type="video/mp4" />
|
||||
<track kind="captions" src="demo.ko.vtt" srclang="ko" label="한국어" default />
|
||||
</video>
|
||||
|
||||
<audio controls src="podcast.mp3"></audio>
|
||||
<details>
|
||||
<summary>트랜스크립트 보기</summary>
|
||||
<p>…전문…</p>
|
||||
</details>
|
||||
```
|
||||
|
||||
이미지의 `alt` 5분류(장식·정보성·기능성·텍스트 이미지·복합)는 [images.md](images.md) §5가 정본이다.
|
||||
|
||||
---
|
||||
|
||||
## 12. 사용자 선호 신호
|
||||
|
||||
브라우저 지원과 WCAG 근거에 따라 하드 게이트·프로젝트 계약·점진적 향상으로 갈린다(확인 2026-09-24)[WEB-BASELINE].
|
||||
|
||||
| 신호 | 지원 상태 | 판정 |
|
||||
|---|---|---|
|
||||
| `prefers-reduced-motion` | Widely available | **하드 게이트.** 무엇을 끄고 무엇을 남기는지는 [motion.md](motion.md)가 정본이다 |
|
||||
| `prefers-contrast` | Widely available(2022-05부터) | **프로젝트 계약.** `more` 값에 반응해 대비를 높이는 것은 요구되면 지킨다 |
|
||||
| `forced-colors` | Widely available(2022-09부터) | **하드 게이트** — §3의 규칙을 따른다 |
|
||||
| `prefers-reduced-transparency` | Limited — Chrome/Edge만 구현하고 Firefox·Safari는 미지원, Apple이 표준화에 반대 의견을 낸 상태 | **점진적 향상** — 유리 재질을 쓰는 프로젝트에서는 프로젝트 계약([elevation.md](elevation.md) §8). 반투명 표면을 불투명하게 낮추는 구체 레시피는 [elevation.md](elevation.md)·[svg-filters.md](svg-filters.md)가 조건부로 다룬다 |
|
||||
|
||||
**색만으로 의미를 전달하지 않는다.** 이 규칙 자체는 부록 A에서 다룬다.
|
||||
|
||||
---
|
||||
|
||||
## 13. 감사 루프
|
||||
|
||||
**증거 우선 1~4단계 루프**를 닫힌 상태로 돈다 — 수정 후 같은 감사를 다시 돌리지 않으면 통과라고 말할 수 없다. 5는 그 루프를 통과한 뒤 추가로 확인하는 항목이다.
|
||||
|
||||
1. **자동 감사.** Chrome DevTools MCP가 있으면 `lighthouse_audit`(accessibility 카테고리)를 쓴다. 없으면 `npx lighthouse <url> --only-categories=accessibility` 또는 `npx @axe-core/cli <url>`로 대체한다. 프로젝트에 `axe-core` 가 있으면 [audit-gate.md](audit-gate.md)의 게이트가 뷰마다 axe 를 함께 돌린다. 도구 목록과 선택 기준은 [harness.md](harness.md) §6을 따른다.
|
||||
2. **실패 노드 국소화.** 감사가 지목한 노드로 대상 컴포넌트를 좁힌다.
|
||||
3. **수동 확인.** Chrome DevTools MCP가 있으면 `take_snapshot`으로 접근성 트리(이름·역할·상태·랜드마크·헤딩)를 검사한다. 영향받는 플로우를 실제로 키보드로 조작한다. 스크린리더를 켤 수 있으면 최소 단축키로 확인한다 — macOS VoiceOver는 `Cmd+F5`(또는 Touch ID 3회 클릭)로 켜고 끄며 `Ctrl+Option+화살표`로 항목을 이동하고 `Ctrl+Option+U`가 로터(헤딩·링크 목록)를 연다. Windows NVDA는 `Ctrl+Alt+N`으로 시작, `Insert+Q`로 종료, `H`가 다음 헤딩, `K`가 다음 링크, `Insert+F7`이 요소 목록을 연다. 스크린리더를 켤 수 없는 환경에서는 [preflight.md](preflight.md)의 대체 절차(읽기 순서 확인)를 따른다.
|
||||
4. **수정 후 동일 감사 재실행.** 1~3단계를 그대로 다시 돌려 재발이 없는지 확인한다. 재실행하지 않은 수정은 미검증이다.
|
||||
5. **고대비 모드 확인.** `forced-colors: active`(Windows 고대비)를 에뮬레이션하거나 실제로 켜서, 색만으로 전달되던 정보가 시스템 색 치환 후에도 남아 있는지 본다(§3).
|
||||
|
||||
**증거 출처를 기록한다.** 어떤 도구·엔진·에뮬레이션으로 확인했는지 보고에 적는다 — "확인함"이라고만 쓰면 재현할 수 없다는 원칙은 [harness.md](harness.md) §6과 동일하다.
|
||||
|
||||
---
|
||||
|
||||
## 14. 보고
|
||||
|
||||
발견한 접근성 문제는 이 문서의 규칙을 근거로 삼되, **보고 형식·심각도 랭킹·톤은 [critique.md](critique.md)를 따른다.** 이 문서에서 다시 정의하지 않는다 — 접근성 하드 게이트 위반은 critique.md의 심각도 매핑에서 항상 Blocking이다.
|
||||
|
||||
---
|
||||
|
||||
## 부록 A. 색·모션 단독 전달 금지
|
||||
|
||||
**하드 게이트.** [color.md](color.md)와 [motion.md](motion.md)가 이 규칙의 정본을 이 문서로 지목했으므로 여기서 정의한다[WCAG-22].
|
||||
|
||||
- **색만으로 상태·의미를 전달하지 않는다.** 성공/실패, 필수/선택, 클릭 가능/불가능 같은 구분은 색만이 아니라 아이콘·텍스트·밑줄 같은 중복 단서를 함께 준다. 비색각 사용자와 회색조로 인쇄되는 화면 모두에 적용된다.
|
||||
- **모션만으로 상태 변화를 전달하지 않는다.** 토글·완료·선택 같은 상태 변화는 애니메이션이 꺼지거나 생략돼도(감소 모션 환경, 저사양 기기) 색·아이콘·라벨 중 하나로 남아 있어야 한다. 모션은 강조일 뿐 유일한 전달 수단이면 안 된다[SKILL-BETTER-INTERFACE].
|
||||
|
||||
두 규칙 모두 구현 위치는 각 도메인 문서다 — 색 역할·토큰은 [color.md](color.md), 모션이 꺼졌을 때 무엇을 남길지는 [motion.md](motion.md) 코드 H를 따른다. 여기서는 **판정**만 내린다.
|
||||
|
|
@ -5,6 +5,8 @@ AI가 만든 티는 **못 만들어서** 나는 게 아니라 결정을 설명·
|
|||
|
||||
프리플라이트에서 이 문서를 훑어 후보를 찾는다. 검출은 실패가 아니다. 브리프·프로젝트 계약·실제 렌더를 대조해 유지·수정·제거를 결정하고 그 근거를 남긴다.
|
||||
|
||||
**Simplicity 는 Minimalism 이 아니다** [SKILL-APPLE-DESIGN]. 요소를 지우는 것 자체가 단순함을 만들지는 않는다. **관찰 후보** — 정보·행동이 사라져 과업이 더 어려워졌다면 그것은 단순화가 아니라 손실이다. 무엇을 남길지는 과업이 정하고, 화면에 무엇이 없다는 사실만으로 단순하다고 주장하지 않는다. 아래 지문들도 같은 기준으로 읽는다 — 비어 보이거나 장식이 없다고 해서 자동으로 통과가 아니고, 요소가 많다고 자동으로 실패도 아니다.
|
||||
|
||||
---
|
||||
|
||||
## 0. 먼저 보는 반복 후보
|
||||
|
|
@ -38,6 +40,9 @@ AI가 만든 티는 **못 만들어서** 나는 게 아니라 결정을 설명·
|
|||
| **크림/베이지(`#FDF8F3`)를 "고급"의 기본값으로** | 보라를 대체한 신종 슬롭 | 크림을 쓸 거면 왜 크림인지 브리프와 연결하고 텍스트·액센트를 그에 맞춰 재설계 |
|
||||
| **여러 hue가 경쟁함** | 행동·상태·정보의 우선순위가 색 때문에 흐려질 수 있다 | 색 역할과 화면 면적을 실제 상태에서 비교한다. hue 수·60/30/10은 출발 가설일 뿐 통과 기준이 아니다 |
|
||||
| **다크에서 본문 대비 미달** | 적용 WCAG 성공 기준을 실제 배경 합성값으로 충족하는지 점검한다 | `#0f172a` 위 `#94a3b8`의 WCAG 대비는 약 **6.96:1**이므로 미달 사례가 아니다. WCAG 2.2 AA 본문 판정에는 contrast ratio를 사용하며 APCA는 이를 대체하지 않는다. 반투명·이미지 배경은 실제 렌더에서 계산한다. 출처: [WCAG 2.2 Contrast](https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html) |
|
||||
| **웜크림 배경 + 하이컨트라스트 세리프 + 테라코타 액센트** (배경 `#F4F1EA` 부근, 세리프 디스플레이, 액센트 `#D97757` 부근) | **관찰 후보** — 지금 AI 생성 디자인이 가장 자주 수렴하는 클러스터 중 하나다. `#D97757` 부근은 Anthropic 자체 상호작용 액센트와 겹쳐, 사용자 브리프에서는 Claude 계열 도구가 만들었다는 티로 먼저 읽힐 수 있다(자기지시적 경고) | 이 조합을 브랜드·업종 근거로 골랐는지 확인한다. 근거가 없으면 다른 웜톤이나 다른 액센트 hue를 브리프에서 다시 끌어온다. 브리프가 정확히 이 룩을 요구하면 그대로 쓴다. 출처: [SKILL-FRONTEND-DESIGN] |
|
||||
| **거의 검정 배경 + 단일 비비드 애시드그린·버미리언 액센트** | **관찰 후보** — 대비되는 두 번째 흔한 클러스터. 다크 배경 자체는 문제가 아니지만 "네온 액센트 하나"라는 조합이 브랜드와 무관하게 반복된다 | 브랜드·상태 역할과 실제 렌더 대비로 액센트 선택의 근거를 남긴다. 출처: [SKILL-FRONTEND-DESIGN] |
|
||||
| **뷰 하나에 여러 요소가 동시에 채색됨** (버튼·배지·아이콘·텍스트 링크가 한 화면에서 전부 강조색) | **관찰 후보** — 강조색이 여러 곳에 흩어지면 "지금 할 행동"을 색만으로 구분하기 어려워진다 | 뷰당 주요 행동 하나에만 채운 강조색을 쓰고, 나머지는 중성 톤이나 다른 상태 표시로 구분한다. 색 체계 상세는 `references/color.md`. 출처: [SKILL-BETTER-COLORS] |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -73,6 +78,9 @@ AI가 만든 티는 **못 만들어서** 나는 게 아니라 결정을 설명·
|
|||
| **벤토 그리드를 기본 선택지로** | 타일 크기 차이가 정보 위계나 과업을 설명하지 못할 수 있다 | 벤토·단순 목록·비교 grid를 콘텐츠와 작은 폭 재배치에서 비교한다. 연도별 트렌드 주장은 근거가 아니다 |
|
||||
| **콘텐츠 여백·본문 폭이 읽기를 방해함** | viewport·글꼴·언어·확대에 따라 줄 길이와 행동 영역이 달라진다 | gutter와 measure 후보를 320px·200% 확대·넓은 폭에서 렌더해 검증한다. 고정 px·ch 범위는 출발값일 뿐이다 |
|
||||
| **푸터가 링크 4열 + 소셜 아이콘 + 카피라이트뿐** | AI가 가장 성의 없이 만드는 곳 | 실제 정보(연락처, 주소, 사업자 정보)를 넣어라. 링크가 4개뿐이면 4열로 만들지 마라 |
|
||||
| **브로드시트 클러스터** (헤어라인 룰 전체, `border-radius: 0` 전면 적용, 신문형 조밀 컬럼) | **관찰 후보** — 세 번째로 흔한 AI 생성 클러스터. 헤어라인·radius 0 자체가 나쁜 것은 아니지만 브랜드·매체 근거 없이 반복되면 에디토리얼 흉내로 읽힌다 | 브랜드·콘텐츠 밀도와 연결해 근거를 남기고, 좁은 컬럼에서 실제 가독성을 검증한다. 출처: [SKILL-FRONTEND-DESIGN] |
|
||||
| **섹션마다 페이드업 리빌 + 모든 카드에 호버 전환** (예외 없이 전부) | **관찰 후보** — 섹션 단위 등장과 카드 호버를 빠짐없이 걸면 "하나의 오케스트레이션된 모먼트"가 아니라 흩어진 장식 모션의 기본값으로 읽힌다 | 모션은 중요한 변화 한둘에만 걸고 나머지는 정적으로 둔다. `references/motion.md`의 저비용 기본·결정 표와 일관되게 선택한다. 출처: [SKILL-FRONTEND-DESIGN] |
|
||||
| **본문 전체를 justify 정렬** | **관찰 후보** — 자동 하이픈 없이 본문 전체를 양쪽 정렬하면 단어 사이 공백이 불규칙해져 읽기가 나빠질 수 있다. 한글은 어절 단위 줄바꿈이라 영향이 다르게 나타난다 | 왼쪽 정렬을 기본으로 하고, justify가 필요하면 하이픈네이션과 좁은 컬럼에서 실제 렌더로 확인한다. 세부는 `references/typography.md`. 출처: [SKILL-BETTER-TYPE] |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -90,6 +98,14 @@ AI가 만든 티는 **못 만들어서** 나는 게 아니라 결정을 설명·
|
|||
| **자동 스크롤 마퀴** (로고·후기·태그가 끝없이 흐름) | 읽기·움직임 민감도·입력과 충돌할 수 있음 | 목적·정지 수단·감소 모션·성능을 검증하고, 정적 그리드 또는 페이지네이션과 비교 |
|
||||
| **hover 상태가 과업을 돕지 않음** | 포인터 반응이 행동 가능성·현재 상태·위험을 설명하지 못할 수 있다 | 링크·카드·버튼의 상태를 역할별로 설계하고, touch/keyboard에서도 같은 핵심 단서를 제공한다 |
|
||||
| **편집 불가 히어로 카피 뒤 깜빡이는 커서 `|`** | 타이핑 흉내. 정보 없음 | 삭제 |
|
||||
| **균일 radius + 흐린 동일 회색 그림자 카드 키트** (모든 카드가 같은 `border-radius`, 같은 `rgba(0,0,0,.1)` 그림자) | **관찰 후보** — SaaS 카드 키트 클러스터. 콘텐츠를 위계 없이 동일 규격 카드로 자르고 장식용 그라디언트 워시를 더하는 조합이 반복된다 | 카드 역할·중요도에 따라 radius·그림자·여백을 다르게 주거나, 카드가 아닌 다른 구조를 검토한다. 깊이 체계는 `references/elevation.md`. 출처: [SKILL-FRONTEND-DESIGN] |
|
||||
| **가운뎃점으로 이은 메타 문자열** (`A · B · C`) | **관찰 후보** — 어떤 주제에도 나타나는 템플릿 장식. 항목이 실제로 같은 범주인지, 구분자가 정보를 돕는지 점검한다 | 항목 성격이 다르면 구분자를 바꾸거나 줄을 나눈다. 실제 같은 범주라면 가운뎃점 자체는 문제가 아니다. 출처: [SKILL-FRONTEND-DESIGN] |
|
||||
| **버튼·링크 텍스트 뒤 `→` 접미사 남용** | **관찰 후보** — 모든 CTA·링크에 화살표를 붙이면 정보가 아니라 장식이 된다 | 실제로 다음 화면·다운로드·외부 이동처럼 방향성이 있는 경우에만 쓰고, 나머지는 텍스트만으로 충분한지 확인한다. 출처: [SKILL-FRONTEND-DESIGN] |
|
||||
| **작은 데이터 라벨에 장식용 모노스페이스** (자릿수 정렬이 의미 없는 라벨에 "테크 느낌"으로만 사용) | **관찰 후보** — §2의 모노스페이스 규칙(본문·제목 맥락)과 달리, 여기서는 실제 tabular 데이터인지부터 먼저 구분한다 | 숫자·코드·ID처럼 자릿수 정렬이 의미 있는 데이터에만 모노스페이스를 쓰고, 장식이면 본문 서체로 되돌린다. 출처: [SKILL-FRONTEND-DESIGN] |
|
||||
| **컨트롤처럼 보이는 정적 요소, 정적처럼 보이는 컨트롤** | **관찰 후보** — 배경·테두리·배치 구역 같은 시각 신호가 없는 상호작용 요소는 컨트롤로 보이지 않는다. 반대로 클릭할 수 없는 배지·라벨이 버튼처럼 생기면 헛클릭을 모은다 | 상호작용 요소마다 일관된 시각 신호(배경·테두리·배치 구역)를 주고, 비활성 정보는 그 신호를 빼서 구분한다. 출처: [SKILL-BETTER-LAYOUT] |
|
||||
| **테마화하지 않은 브라우저 표면** (`::selection`, `caret-color`, 스크롤바 색) | **관찰 후보** — 직접 그리지 않은 부분도 디자인의 일부다. 팔레트로 테마화하지 않으면 브라우저 기본값(파란 선택 영역, 회색 스크롤바)이 그대로 남아 "조립됐다"는 티가 가장 싸게 난다 | `::selection{background;color}`와 `caret-color`로 선택·캐럿을 팔레트에 맞춘다. `scrollbar-color`(2026-09-24 기준 Baseline Newly available — Chrome·Edge·Firefox·Safari 최신 버전은 지원하나 아직 Widely는 아니다)로 스크롤바를 테마화하고, 미지원 브라우저에서는 기본 스크롤바로 자연 열화되는지 확인한다. 출처: [SKILL-IMPECCABLE], [WEB-BASELINE] |
|
||||
| **비밀번호·OTP 필드의 붙여넣기 차단** (`onpaste=`, `addEventListener('paste', ...)`로 `preventDefault`) | **하드 게이트** — 비밀번호 관리자·2FA 자동 입력과 충돌해 WCAG 3.3.8(Accessible Authentication)이 요구하는, 인지 부담 없는 인증 경로를 막는다 | 붙여넣기를 막지 않는다. 진짜 `<form>`과 정확한 `autocomplete`(`current-password`, `one-time-code`)로 비밀번호 관리자·2FA 자동입력과 호환을 유지한다. 출처: [WCAG-AUTH], [SKILL-BETTER-A11Y] |
|
||||
| **스트리밍·채팅 UI의 자동 스크롤 추적을 직접 재구현** (바닥 고정·점프-투-레이턴트를 손으로 구현) | **관찰 후보** — 스크롤 앵커링 로직은 엣지케이스가 많아 직접 짜면 기존 라이브러리가 이미 처리한 문제를 다시 겪기 쉽다 | 라이브러리·플랫폼이 제공하는 기본 동작을 먼저 확인하고, 정말 필요할 때만 직접 구현한다. 출처: [SKILL-SHADCN] |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -110,6 +126,8 @@ AI가 만든 티는 **못 만들어서** 나는 게 아니라 결정을 설명·
|
|||
|
||||
**AI 티는 시각보다 문장에서 먼저 난다.**
|
||||
|
||||
이 절은 브랜드 차별성을 주장하는 마케팅 카피(헤드라인·브랜드 약속)를 다룬다. 버튼·오류·빈 상태·토글 같은 제품 UI 카피의 목소리·용어 규칙은 `references/product-copy.md`.
|
||||
|
||||
### 진단 범위
|
||||
브랜드가 특별하다고 주장하는 헤드라인·브랜드 약속에만 제품·회사 이름을 **경쟁사 이름으로 바꿔 읽는다.** 다른 업체에도 그대로 성립하면 차별성의 근거와 구체성을 다시 본다.
|
||||
|
||||
|
|
@ -180,6 +198,7 @@ B 라면, 반응형에서 매번 바뀌는 것을 지목한 것이다. 지목하
|
|||
| **스타카토 3연타** (`No fluff. No filler. No BS.` / `빠르게. 정확하게. 간단하게.`) | 리듬으로 내용 없음을 감춤 | 한 문장으로 구체적으로 |
|
||||
| **em-dash(—) 한 문단에 2회 이상** | AI 문장 리듬의 대표 지문 | 마침표로 끊거나 쉼표로. 한국어에서 특히 부자연스럽다 |
|
||||
| **`In today's fast-paced digital landscape...`** 도입 | 아무 말도 하지 않는 문단 | 첫 문장부터 본론. 도입 문단 삭제 |
|
||||
| 제목·라벨의 **`WORD — fragment`** 형식 (`Progress — this week`, `설정 — 계정`) | 본문 em-dash 남용(위 행)과는 별개로, 제목·라벨에서도 같은 리듬이 반복되면 템플릿 티가 난다 | 제목은 하나의 명사구나 문장으로 쓰고, 필요하면 콜론이나 줄바꿈으로 나눈다. 출처: [SKILL-FRONTEND-DESIGN] |
|
||||
|
||||
### 어휘 지문
|
||||
|
||||
|
|
@ -194,6 +213,7 @@ B 라면, 반응형에서 매번 바뀌는 것을 지목한 것이다. 지목하
|
|||
| 모든 헤딩이 명사구 (`Powerful Features`, `Simple Pricing`) | 헤딩이 정보를 전달하지 않음 | 헤딩에 주장을 담아라 (`엑셀 없이 정산이 끝난다`) |
|
||||
| 레이블·서브레이블·헬퍼가 같은 말 3번 (`이메일` / `이메일 주소` / `이메일 주소를 입력하세요`) | 화면 소음 | 하나만 남긴다 |
|
||||
| 이모지로 시작하는 불릿 (`✅ 빠른 속도`) | 정보 위계를 이모지로 대체 | 일반 불릿 또는 문장으로 |
|
||||
| **내부 구현 용어가 사용자 라벨로 누출** (`webhook`, `config`, `payload`, `schema`) | 사용자는 시스템 구성요소 이름이 아니라 자신이 하는 일로 화면을 이해한다. "webhook 설정"이 아니라 "알림"을 관리하는 것이다 | 최종 사용자 어휘로 바꾼다(`webhook config` → `알림`, `payload` → `보낸 내용`). grep 후보는 §9. 출처: [SKILL-FRONTEND-DESIGN] |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -242,6 +262,14 @@ shadow-lg|shadow-xl|shadow-2xl
|
|||
border-l-4|border-t-4
|
||||
backdrop-blur
|
||||
|
||||
# 모션 — 프로젝트 계약(motion.md)
|
||||
scale\(0\)
|
||||
transition:\s*all\b
|
||||
|
||||
# 폼 — 비밀번호 관리자 호환(§4 하드 게이트 참고)
|
||||
onpaste\s*=
|
||||
addEventListener\(['"]paste['"]
|
||||
|
||||
# 타이포
|
||||
font-family:.*Inter
|
||||
(Space Grotesk.*Instrument Serif)|(Instrument Serif.*Geist)|(Space Grotesk.*Geist)
|
||||
|
|
@ -252,6 +280,9 @@ Get Started|Learn More|Empower|Streamline|Supercharge|Seamless
|
|||
world-class|cutting-edge|enterprise-grade|best-in-class
|
||||
It's not .* it's
|
||||
|
||||
# 카피 — 내부 구현 용어 누출 (문맥 확인 필요, grep 자체는 후보일 뿐)
|
||||
\bwebhook\b|\bconfig\b|\bpayload\b|\bschema\b
|
||||
|
||||
# 이모지 아이콘 (텍스트 노드)
|
||||
🚀|⚡|✨|🎯|🔥|💡|✅
|
||||
```
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
| 사고 (실제) | 검사 |
|
||||
|---|---|
|
||||
| CSS 닫는 중괄호 누락 — 이후 규칙 절반이 죽은 채 렌더 | stylelint(구문) + 중괄호 균형 |
|
||||
| 브랜드가 모바일에서 1글자 폭으로 수축, 세로로 쌓임 | 수축 탐지(짧은 라벨 n줄 이상) + 브랜드 1줄 |
|
||||
| 브랜드가 모바일에서 1글자 폭으로 수축, 세로로 쌓임 | 수축 탐지(짧은 라벨 n줄 이상) + 브랜드 줄바꿈(텍스트 블록 각각 1줄) |
|
||||
| 기기 글꼴 확대(안드로이드 큰 글꼴 1.3×)에서 제목 부서짐 | 스케일×폭 h1 행렬 |
|
||||
| 본문 보조색이 배경 대비 4.4:1 (AA 미달) | 전 요소 대비 스캔(블렌드 계산) |
|
||||
| CSS `color-mix()`의 computed `color(srgb …)`를 0~255 rgb로 오독해 정상 대비를 1:1로 거짓 실패 | CSS Color 4 `color(srgb 0~1)` 파싱 + 알파 합성 RED fixture |
|
||||
|
|
@ -54,6 +54,21 @@
|
|||
| L5 탐색 | 키보드 완전 통과 + 사용자 여정 차터 | 프로젝트별 작성 — 마우스 금지 |
|
||||
| L6 시나리오 E2E | 실사용자 여정 재현 — 페르소나의 하루·장기 과업 포함 | 프로젝트별 작성. **기대값 단언 필수**(하니스 규칙 8), 백 키·시트·양방향 흐름은 `mobile-app-ux.md` 패턴 기준. 실측: RED 1,742 단언 중 318 실패가 신규 기능에 정확히 집중 — 시나리오를 먼저 쓰면 미구현이 수치로 드러난다 |
|
||||
|
||||
## 접근성 감사 루프
|
||||
|
||||
**하드 게이트.** 접근성 감사는 자동 점수 하나로 끝나지 않는다. Chrome DevTools MCP가 있으면 `lighthouse_audit`(accessibility 카테고리)로 실패 노드를 모으고 `take_snapshot`으로 접근성 트리(이름·역할·상태·랜드마크·헤딩)를 확인한다. MCP가 없으면 `npx lighthouse --only-categories=accessibility` 또는 `npx @axe-core/cli <url>`로 대체한다. 실패 노드로 대상 컴포넌트를 국소화하고, 수정한 뒤 **동일 감사를 재실행**해야 통과다 — 4단계로 돌아가 고치고 5단계에서 재검증하는 이 문서의 기본 루프와 같은 정신이다. 자동 점수 100은 WCAG 준수와 같지 않고, 낮은 점수가 이슈 증거를 대체하지도 않는다. 판정 원칙·시맨틱·키보드·라이브 리전 같은 세부는 [accessibility.md](accessibility.md)를 따른다 [SKILL-WEB-A11Y].
|
||||
|
||||
**선택 axe 훅.** 이 문서 상단(왜 필요한가)의 axe 관련 검사(aria-label을 div/ul에 오용, dl 안 `<p>`·role="grid" 자식 구문 위반)는 axe-core 가 있을 때만 돈다. `tools/design-gate.mjs` 는 `checks.axe: "auto"`(기본값)일 때 프로젝트 cwd 에서 `axe-core` 를 해석할 수 있으면 뷰마다 WCAG 2.x A·AA 태그로 axe 를 실행해 위반을 실패로 보고하고, 해석할 수 없으면 `SKIP — 미검증` 으로 보고한다(`npm i -D axe-core` 로 켠다). 게이트 밖에서는 브라우저 MCP 나 `npx @axe-core/cli <url>` 로 같은 검사를 돌린다. 어느 쪽도 없으면 해당 체크를 실패로 적지 않고 **미검증(Not verified)**으로 보고한다 — 통과로 추정하지 않는다. 상태값의 뜻은 [리포트 양식](#리포트-양식)을 따른다.
|
||||
|
||||
## 터치 타깃 — 두 층 판정
|
||||
|
||||
L2의 터치 타깃 검사는 컨트롤 하나의 실측 `min(width, height)`을 두 문턱과 비교한다. `thresholds.inlineTargetMin`(기본 24px)은 본문 흐름 속 인라인 텍스트 링크에 적용되는 문턱이고, `thresholds.tapTargetMin`(기본 44px)은 버튼·입력·role 컨트롤 등 인라인이 아닌 컨트롤에 적용되는 문턱이다. 두 문턱은 서로 다른 판정 위계를 진다.
|
||||
|
||||
- **하드 게이트 — WCAG 2.5.8 층.** 인라인이 아닌 대상 중 `min(width, height) < 24px` 인 것은 곧바로 실패가 아니다. 게이트가 간격 예외를 계산한다 — 각 대상의 바운딩박스 중심에 24px 지름 원을 두었을 때, 그 원이 다른 대상의 박스나 다른 미달 대상의 원과 겹치지 않으면 예외로 통과한다. 겹치면 `WCAG 2.5.8:` 로 표시된 실패다. 문장 속 인라인 링크는 WCAG 의 Inline 예외라 이 층에서 검사하지 않는다. Equivalent(같은 기능의 다른 컨트롤)·User Agent Control·Essential 예외는 게이트가 자동으로 판정하지 못하므로, 해당한다고 보면 근거를 design.md 에 적고 수동 평가로 남긴다 [WCAG-TARGET].
|
||||
- **프로젝트 계약 층.** 컨트롤 `< thresholds.tapTargetMin`(기본 44px, HIG 44pt·M3 48dp 관례)과 인라인 링크 `< thresholds.inlineTargetMin`(기본 24px)은 WCAG 위반이 아니라 계약 미달이다. `tapTargetPolicy: "contract"`(기본값, 이전 동작과 같다)이면 `계약 44px:` 로 표시해 실패로 보고하고, `"wcag"` 이면 통과 detail 에 "계약 참고"로만 남긴다. 모바일 앱 수준을 요구하지 않는 프로젝트(예: 랜딩 페이지)는 design.md 에 근거를 적고 `"wcag"` 로 바꾸거나 문턱을 조정한다.
|
||||
|
||||
인라인 판정 자체(요소가 `display:inline`인지, `<a href>`인지, `tapTargetsInline` 선택자에 매칭되는지)는 이 두 층 구분과 별개로 그대로 쓴다.
|
||||
|
||||
## 렌더 측정 계약
|
||||
|
||||
자동 측정은 실제 장면을 재현할 때만 의미가 있다. 새 범용 JS 엔진을 만들라는 뜻이 아니다. 프로젝트의 기존 E2E·시각 회귀 도구에 아래 계약을 적고, 페이지와 기능의 위험에 맞는 단언만 구현한다.
|
||||
|
|
@ -64,6 +79,8 @@
|
|||
|
||||
화면 맞춤 hero는 헤더가 overlay인지, 문서 흐름에서 높이를 차지하는지 먼저 구분한다. 그 역할을 포함해 첫 화면의 남은 높이를 재고, 내용이 고정 높이보다 커지면 자연스럽게 확장되어야 한다. 모든 페이지 섹션에 `100vh`를 강요하지 않는다. 컨테이너의 `overflow=0`만으로 피사체·카피·행동의 안전 영역이 보존됐다고 판정하지 말고, 실제 크롭과 인접 콘텐츠의 겹침을 화면에서 확인한다.
|
||||
|
||||
**언제**: 프로젝트가 backdrop-filter를 쓰는 표면(유리·오버레이·스크림 헤더)을 가질 때. 그런 표면은 뒤로 스크롤되는 콘텐츠에 따라 대비가 달라지므로 한 장면만 재고 통과를 선언하지 않는다. 뒤에 올 수 있는 콘텐츠 중 가장 밝은 장면과 가장 어두운 장면 둘 다를 캡처해 최악·최선 대비를 각각 기록한다 [SKILL-BETTER-COLORS].
|
||||
|
||||
### 2. 모션과 상태
|
||||
|
||||
reveal·전환은 실제 UI 스크롤·클릭·키보드 이벤트로 장면을 먼저 활성화한다. 그 장면에 필요한 폰트·이미지가 준비된 뒤 대상과 전환에 관여하는 조상 요소의 유한 전환 완료를 관측하고, 프로젝트가 의도한 최종 가시성·색·대비·위치에 도달했는지 확인한다. 최종 가시성은 `opacity: 1`을 일괄 요구하는 값이 아니다. 의도한 반투명 텍스트도 조상 opacity 합성을 포함한 실제 대비와 상태 계약으로 판정한다. 정한 상한 시간 안에 끝나지 않으면 실패 또는 미검증으로 기록하고 원인을 남긴다.
|
||||
|
|
@ -150,6 +167,8 @@ L1·L5 는 프로젝트 안에 `tools/unit/*.test.mjs`, `tools/exploratory.mjs`
|
|||
32. **캐시 무효화도 사용자 화면에서 단언한다** — static CSS/JS를 덮어쓴 뒤 HTML만 새 URL이면 브라우저는 이전 stylesheet를 계속 쓴다. stylesheet와 `@import` 의 버전을 같이 바꾸고, 공개 URL에서 실제 href·새 토큰/핵심 computed value를 검사한다. HTTP 200과 새 HTML만으로 배포 성공을 선언하지 않는다.
|
||||
33. **썸네일은 안정한 프레임만 저장한다** — 캡처 전 local HTTP 경로에서 `document.fonts.ready`·모든 이미지 `decode()`·bounded settle을 기다리고, `prefers-reduced-motion: reduce`를 적용한다. 그 뒤 video/audio·CSS/WAAPI·rAF canvas를 멈추고, local 4xx·pageerror·깨진 이미지가 하나라도 있으면 실패시킨다. 원리는 `animation:none`으로 초기 상태를 재현하는 것이 아니라 **완성된 한 프레임을 고정**하는 것이다. 이는 실제 모션 검수와 별도다. 카드·OG·핵심 상태가 같은 계약을 공유하는지 파일 치수·0바이트·시각 검수까지 단언한다.
|
||||
34. **초광폭 text-media split은 가족별로 잰다** — hero가 통과했다고 과정·추천·가맹 분할이 통과한 것이 아니다. 서로 다른 DOM/여백 규칙을 가진 각 split root에서 1920·2560의 rail, copy/media rect, heading의 실제 내용 폭과 줄 수를 hard fail로 기록한다. wide override는 양쪽 logical padding을 명시하고, 상위 container의 `max-width`가 specificity 때문에 named stage를 다시 줄이지 않는지 단언한다. grid 바깥 rect가 정상이어도 text content가 한 글자 열로 수축하면 실패다.
|
||||
35. **커스텀 제스처 컨트롤은 완주까지 시험한다** — 언제: 슬라이더·드래그 표면·스크롤형 컨트롤 스트립이 있을 때. 레이아웃 검증과 별개로 (a) 탭 응답과 대상 입력방식으로 드래그를 끝까지(시작만이 아니라) 확인한다 (b) 컨트롤을 가로지르는 스와이프와 컨트롤 축을 따르는 드래그를 구분해 둘 다 시험한다 — 레이아웃 통과가 제스처 통과를 보장하지 않고, 둘 다 조용히 실패할 수 있다 (c) 증거 출처(에뮬레이션·합성 터치·실기기, 엔진명 Chromium≠Safari)를 명시한다. 접근 불가한 하드웨어는 **보고된 갭**으로 정직하게 남긴다 — 차단 사유가 아니다 [SKILL-IMPECCABLE].
|
||||
36. **증거 방향성을 지킨다** — 런타임 동작이 결과를 좌우하는 항목은 시각 외관만 보고 코드-레벨 finding을 내지 않고, 소스코드만 보고 시각 finding을 내지 않는다. 각 finding은 `path/to/file:line`과 그 항목이 실제로 필요로 하는 증거(렌더 확인 또는 소스 대조)를 함께 남긴다 [SKILL-BETTER-INTERFACE].
|
||||
|
||||
## 리포트 양식
|
||||
|
||||
|
|
@ -158,4 +177,8 @@ L1·L5 는 프로젝트 안에 `tools/unit/*.test.mjs`, `tools/exploratory.mjs`
|
|||
| 범주 | 결과 | 항목 | 소요 | 근거 |
|
||||
```
|
||||
|
||||
결과 값은 PASS/FAIL 둘이 아니라 셋이다. 실행할 수 없는 체크(도구·권한·환경 제약으로 확인 못 한 것)는 FAIL로 적지 않고 **미검증(Not verified)**으로 따로 적는다. 미검증은 finding 개수에 들어가지 않고, 통과로 추정하지도 않는다. 통과·실패·미검증 세 줄을 분리해 나열한다 [SKILL-BETTER-INTERFACE].
|
||||
|
||||
개별 발견 사항의 심각도·What/Why/Fix 서식은 [critique.md](critique.md)를 따른다 — 위 표는 자동 게이트 집계용이고 개별 리뷰 보고 형식은 별도다. diff·PR 단위 변경 리뷰(스코프 계산·제거된 쪽 읽기·Introduced/Regression/Pre-existing 분류)는 [change-review.md](change-review.md)를 따른다.
|
||||
|
||||
실패 상세는 원문 테일 포함. 시각 평가는 같은 viewport·상태·자산을 전후로 보존해 위계·브랜드·타입·구도·이미지·리듬을 기록한다. 사용자 성과의 인과나 취향 우승을 검증 없이 주장하지 않는다. 리포트 파일(gate-report.md / verify-report.md)은 커밋 대상이다.
|
||||
|
|
|
|||
247
packages/skill/references/brief-interview.md
Normal file
247
packages/skill/references/brief-interview.md
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
# brief interview — 브리프를 인터뷰로 확정하는 법
|
||||
|
||||
0단계에서 [SKILL.md](../SKILL.md)의 인터뷰 게이트를 실제로 어떻게 수행하는지 정한 문서다. 게이트의 여섯 규칙은 여기서 바뀌지 않는다 — 이 문서는 그 규칙을 슬롯표, 질문 카드, 라운드, 기록 형식으로 구체화한다. 하네스별 질문 도구 이름과 한도는 [harness.md](harness.md)를 따로 연다.
|
||||
|
||||
## 1. 왜 인터뷰가 필수 입력인가
|
||||
|
||||
브리프가 비어 있는 채로 시작하면 그 빈칸을 메우는 것은 모델의 기본값이고, 기본값의 총합이 AI 슬롭이다. "합리적으로 추측했다"는 것은 안전장치가 아니다 — 추측은 카테고리 평균으로 수렴하고, 카테고리 평균은 어떤 브리프에도 들어맞는 대신 이 브리프에만 맞는 이유가 없다.
|
||||
|
||||
실측 사례 — "꽃집 홍보 사이트 하나 만들어보자"를 받고 업종 성격·목표 행동·톤·이름을 전부 혼자 정했다. 실제로 물어보니 **넷 중 넷이 달랐다.**
|
||||
|
||||
| 내가 가정한 것 | 사용자의 실제 답 |
|
||||
|---|---|
|
||||
| 일상 꽃 · 정기구독 | **하이엔드 플로럴 스튜디오** |
|
||||
| 문의 유도 하나 | 문의 · 구독 · 방문 · 인스타 **넷 다** |
|
||||
| (묻지 않음) | **에디토리얼 · 잡지** |
|
||||
| (내가 지어냄) | **목요일의 화원** |
|
||||
|
||||
이 상태로 1단계에 들어갔으면 레퍼런스 세 개를 전부 틀린 방향에서 골랐을 것이다. 가정한 네 축 중 하나도 맞지 않았다는 것은 "대개는 맞는다"는 전제 자체가 틀렸다는 뜻이다. 인터뷰는 판단으로 생략할 수 있는 예의가 아니라, 1단계 이후 전체 작업의 입력이다.
|
||||
|
||||
## 2. 스캔 → 가설 → 질문
|
||||
|
||||
순서는 항상 이렇다.
|
||||
|
||||
1. **스캔한다.** `design.md`, 프로젝트 토큰, 브랜드 자산(로고·색·패키지), README·기존 문서를 먼저 읽는다.
|
||||
2. **가설을 세운다.** 스캔에서 읽은 값은 **가설이지 사용자 승인이 아니다.** 저장소에 "정기구독" 문구가 있다고 해서 그것이 사용자가 확정한 업종 성격은 아니다.
|
||||
3. **질문한다.** 가설은 질문의 추천 선택지로 제시해 확인받는다 — 추천 선택지는 항상 맨 앞에 두고 "(추천)" 표기를 붙인다. 근거(무엇을 읽고 이렇게 추천하는지)를 한 줄로 곁들인다. 스캔 근거가 없는 축에는 추천을 붙이지 않는다 — 근거 없는 추천은 모델의 기본 미학을 선택지 맨 앞에 올리는 일이다. 도구 설명이 영어 표기("(Recommended)")를 예로 들어도 표기는 기본 언어를 따른다.
|
||||
|
||||
스캔 결과를 사용자 확인 없이 바로 확정으로 쓰지 않는다. "코드에 이렇게 되어 있으니 그대로 갑니다"는 인터뷰를 생략하는 것이 아니라 인터뷰의 첫 단계일 뿐이다.
|
||||
|
||||
## 3. 브리프 슬롯
|
||||
|
||||
아래 표가 채워야 할 전부다. "알려진 사실"은 **사용자 발화 또는 사용자가 준 자료에 명시된 것**만 뜻한다. 업종 평균이나 코드에서 추론한 값은 확정이 아니라 2절의 가설이다.
|
||||
|
||||
| 슬롯 | 필수 조건 | 확정 기준 | 모를 때 |
|
||||
|---|---|---|---|
|
||||
| 무엇을 (산출물·범위) | 항상 | 요청 문장 | 묻는다 |
|
||||
| 누구에게·목표 행동 (복수면 우선순위) | 전체·연장 | 사용자 발화 | 묻는다(multiSelect 후 우선순위 확인) |
|
||||
| 업종·성격 | 전체, 연장의 새 화면 | 사용자 발화·제공 문서 | 묻는다(스캔 결과는 추천 선택지로) |
|
||||
| 톤 | 전체. 연장은 design.md에 없을 때 | 사용자 선택 | 묻는다(선택지마다 모션 강도·프리셋 영향을 적는다) |
|
||||
| 고유명사·실제 값 | 화면에 나오면 항상 | 사용자 제공 | 묻고, 아직 없다고 하면 명시적 placeholder |
|
||||
| 기존 브랜드 자산 (색·로고·서체·쓸 수 있는 사진) | 전체 | 파일 또는 사용자의 "없음" | 묻는다 |
|
||||
| 좁은 화면 내비 | 내비가 있는 다중 화면 | 정보 구조·라벨이 정해진 뒤 | 2라운드에서 묻는다 |
|
||||
| 기본 언어 | 항상 | OS 로케일 명령 | 읽지 못할 때만 묻는다 |
|
||||
|
||||
슬롯이 "필수 조건"에 해당하는데 "확정 기준"을 사용자 발화·자료로 채우지 못했으면 **묻는다.** "결과를 크게 바꾸는가"를 스스로 판정해서 생략하지 않는다 — 그 판단은 이 표가 이미 대신 내렸다.
|
||||
|
||||
## 4. 경로별 적용
|
||||
|
||||
| 경로 | 무엇을 묻는가 |
|
||||
|---|---|
|
||||
| **전체** | 슬롯 전부를 이 표의 조건대로 묻는다. 좁은 화면 내비는 정보 구조가 정해진 뒤 2라운드에서 묻는다. |
|
||||
| **연장** | 무엇을·목표 행동·고유명사·기본 언어는 전체와 같다. 업종·성격은 **새 화면**일 때만 묻는다. 톤은 `design.md`에 없을 때만 묻는다. 브랜드 자산은 `design.md`가 이미 답했으면 묻지 않는다. |
|
||||
| **국소** | `design.md`가 슬롯에 답하면 묻지 않는다. 브랜드나 톤을 새로 정해야 하면 그 순간 경로를 전체나 연장으로 올리고, 올린 경로의 규칙대로 묻는다. 화면에 고유명사가 나오면 국소여도 항상 묻는다. |
|
||||
| **리뷰** | 범위와 입력(스크린샷·URL·diff)만 묻는다. 위 슬롯은 리뷰 대상이 아니라 리뷰 자체의 입력이므로 별도로 취급한다. |
|
||||
|
||||
국소 경로에서 조건이 깨지면(토큰을 새로 정의하게 됐거나 손댄 섹션이 늘었거나 방향을 바꿔야 하면) SKILL.md 0단계의 규칙대로 경로를 올렸다고 말하고, 그 순간부터는 이 표의 전체·연장 행을 따른다.
|
||||
|
||||
## 5. 질문 카드 작성법
|
||||
|
||||
- **결과를 크게 바꾸는 축부터 낸다.** 업종·성격과 목표 행동이 톤이나 고유명사보다 앞선다 — 앞의 답이 뒤의 질문 구성을 바꾸기 때문이다.
|
||||
- **선택지마다 "이걸 고르면 무엇이 달라지는지" 한 줄을 붙인다.** 고르는 사람이 결과를 미리 예상할 수 있어야 한다. "A: 발랄한 느낌"처럼 형용사만 나열하지 않는다. 라벨 자체가 결과를 드러내는 행동형 선택지(목표 행동 등)는 "고르면:" 줄을 생략해도 된다.
|
||||
- **multiSelect와 단일 선택을 구분한다.** 목표 행동은 대개 복수이므로 multiSelect로 내고, 답이 여럿이면 다음 질문이나 다음 라운드에서 우선순위를 확인한다. 톤과 성격은 주된 방향 하나를 단일 선택으로 받는다. 브리프가 요구하면 서로 보완하는 속성을 자유 답으로 덧붙일 수 있지만, 그때도 어떤 속성이 위계를 이끄는지(주 방향이 무엇인지)를 기록한다.
|
||||
- **톤 선택지에는 모션 강도와 프리셋 영향을 적는다.** 톤은 2단계 프리셋과 3단계 모션 문법을 여기서 사실상 결정하므로, "이 톤을 고르면 모션이 어느 정도 세지는지·어떤 레이아웃 프리셋으로 가는지"를 선택지 설명에 넣는다.
|
||||
- **구조화 도구가 preview를 지원하면(Claude Code의 AskUserQuestion) 톤·내비 선택지에 ASCII 목업 비교를 붙인다.** 문장으로 "여백이 넓다"고 말하는 것보다 실제 배치 차이를 보여주는 쪽이 오답을 줄인다. 예시:
|
||||
|
||||
```
|
||||
A. 에디토리얼 · 잡지 (editorial) B. 대담 · 캠페인 (anti-grid)
|
||||
+----------------------+ +----------------------+
|
||||
| | | 거대한 [사진] |
|
||||
| 큰 제목 | | 제목 ↘ |
|
||||
| ------------ | | [사진] 짧은 문장 |
|
||||
| 작은 캡션 | | [사진] |
|
||||
+----------------------+ +----------------------+
|
||||
```
|
||||
|
||||
선택지 이름 옆의 괄호는 [presets/README.md](presets/README.md)의 실제 프리셋 이름이다. 톤 선택지는 존재하는 프리셋(editorial · swiss-minimal · anti-grid · dark-instrument · quiet-commerce)이나 "새로 정의"로만 연결한다.
|
||||
|
||||
## 6. 질문 카드 템플릿 3종
|
||||
|
||||
수단은 판단이 아니라 도구 목록으로 기계적으로 정한다. 하네스별 정확한 도구명·한도는 [harness.md](harness.md)에 있다. 아래는 세 가지 대표 형태다.
|
||||
|
||||
### (a) 구조화 도구 4문항판 (Claude Code · Gemini · Copilot 계열)
|
||||
|
||||
```
|
||||
질문 1 — 업종·성격 (단일 선택)
|
||||
A. 하이엔드 플로럴 스튜디오 (추천 — README에 "맞춤 부케·상담 예약" 언급)
|
||||
고르면: 포트폴리오형 갤러리와 예약 상담 CTA가 중심이 된다.
|
||||
B. 데일리 플라워 · 정기구독형
|
||||
고르면: 반복 구매를 위한 가격·주기 섹션이 커진다.
|
||||
C. 동네 꽃집 · 방문 중심
|
||||
고르면: 위치·영업시간이 첫 화면에 노출된다.
|
||||
|
||||
질문 2 — 목표 행동 (다중 선택)
|
||||
A. 상담·문의 신청
|
||||
B. 정기구독 시작
|
||||
C. 매장 방문·예약
|
||||
D. 소셜 채널 팔로우
|
||||
(복수 선택 시 다음 라운드에서 우선순위를 확인합니다.)
|
||||
|
||||
질문 3 — 톤 (단일 선택, 모션·프리셋 영향 표시)
|
||||
A. 에디토리얼 · 잡지
|
||||
고르면: editorial 프리셋, 넓은 여백과 세리프, 모션은 절제(페이드 위주).
|
||||
B. 대담 · 캠페인
|
||||
고르면: anti-grid 프리셋, 깨진 격자와 큰 타입, 모션은 표현적.
|
||||
C. 조용한 커머스
|
||||
고르면: quiet-commerce 프리셋, 상품·사양이 주인공, 모션 최소.
|
||||
|
||||
질문 4 — 기존 브랜드 자산과 실제 값
|
||||
A. 있다 — 자유 답(Other)에 로고·색 파일 위치와 상호명·연락처를 적어 주세요.
|
||||
B. 아직 없다 — 브랜드 색은 레퍼런스에서 정하고, 상호명·연락처는 placeholder로 둡니다.
|
||||
```
|
||||
|
||||
고유명사·실제 값은 선택지로 만들 수 없다. 질문 4처럼 자유 답(Other)으로 받거나, 비어 있으면 8절에 따라 2라운드에서 평문으로 받는다.
|
||||
|
||||
### (b) 3문항판 (Codex `request_user_input`: 질문 ≤3, 선택지 2~3, "Other"는 클라이언트가 자동 추가하므로 넣지 않는다)
|
||||
|
||||
```
|
||||
질문 1 — 업종·성격
|
||||
A. 하이엔드 플로럴 스튜디오 (추천 — README의 "맞춤 부케·상담 예약")
|
||||
B. 데일리 플라워 · 정기구독형
|
||||
C. 동네 꽃집 · 방문 중심
|
||||
|
||||
질문 2 — 가장 중요한 목표 행동 하나
|
||||
A. 상담·문의 신청
|
||||
B. 정기구독 시작
|
||||
C. 매장 방문·예약
|
||||
|
||||
질문 3 — 톤
|
||||
A. 에디토리얼 · 잡지 (editorial)
|
||||
B. 대담 · 캠페인 (anti-grid)
|
||||
C. 조용한 커머스 (quiet-commerce)
|
||||
```
|
||||
|
||||
이 도구의 선택지는 서로 배타적이라 다중 선택을 받을 수 없다. 그래서 목표 행동은 "가장 중요한 하나"로 묻고, 나머지 행동은 아래 평문 블록에서 받는다. 같은 턴에 평문 한 블록을 더 낸다(질문 수 상한에 걸리는 나머지 슬롯):
|
||||
|
||||
```
|
||||
추가로 확인이 필요합니다.
|
||||
- 위에서 고른 것 말고도 원하는 행동(구독·방문·소셜 등)이 있으면 적어 주세요.
|
||||
- 실제 상호명·연락처·영업시간이 있나요? 없으면 placeholder로 표시하겠습니다.
|
||||
- 기존 로고나 브랜드 색이 있나요? 파일이 있으면 알려주세요.
|
||||
- 좁은 화면 메뉴는 정보 구조를 정한 뒤 다음 라운드에서 따로 여쭙겠습니다.
|
||||
```
|
||||
|
||||
### (c) 평문 폴백판 (구조화 질문 도구가 없거나 확인되지 않는 하네스)
|
||||
|
||||
```
|
||||
1. 업종·성격이 어느 쪽에 가깝나요?
|
||||
a) 하이엔드 플로럴 스튜디오
|
||||
b) 데일리 플라워 · 정기구독형
|
||||
c) 동네 꽃집 · 방문 중심
|
||||
d) 위에 없음 — 직접 설명해 주세요
|
||||
|
||||
2. 목표 행동은 무엇인가요? (복수 가능)
|
||||
a) 상담·문의 신청
|
||||
b) 정기구독 시작
|
||||
c) 매장 방문·예약
|
||||
d) 소셜 채널 팔로우
|
||||
|
||||
3. 톤은 어느 쪽인가요?
|
||||
a) 에디토리얼 · 잡지 (여백 넓고 모션 적음)
|
||||
b) 대담 · 캠페인 (깨진 격자, 표현적 모션)
|
||||
c) 조용한 커머스 (상품이 주인공, 모션 최소)
|
||||
|
||||
4. 실제 상호명·연락처·기존 로고나 브랜드 색이 있나요? 없으면 없다고 답해 주세요.
|
||||
|
||||
답을 주시면 1단계(레퍼런스 조사)로 넘어갑니다.
|
||||
```
|
||||
|
||||
## 7. 턴 종료 규칙
|
||||
|
||||
물었으면 그 턴에서 1단계 조사·파일 생성·구현을 시작하지 않는다. 같은 턴에 할 수 있는 것은 스캔 결과 요약뿐이다 — "이런 파일을 읽었고 이렇게 추천한다"까지는 같은 턴에 말해도 되지만, 답을 받기 전에 레퍼런스를 조사하거나 토큰·코드를 만들지 않는다.
|
||||
|
||||
## 8. 라운드
|
||||
|
||||
1라운드가 기본이다. 2라운드는 다음 세 경우에만 연다.
|
||||
|
||||
- 답이 서로 모순될 때(9절)
|
||||
- 고유명사·실제 값이 아직 없을 때
|
||||
- 좁은 화면 내비를 정할 때(정보 구조가 확정된 뒤라야 물을 수 있으므로)
|
||||
|
||||
그 외의 이유로 라운드를 늘리지 않는다.
|
||||
|
||||
## 9. 답이 모순되면
|
||||
|
||||
그 자리에서 정리한다. 1절 사례에서 목표 행동 넷이 다 선택됐는데, 하이엔드 스튜디오에서 "정기구독"과 "문의 상담"은 성격이 다르다. **우선순위를 제안하고 확인받는다** — 넷을 같은 무게로 두면 CTA가 넷이 되고 페이지가 무너진다.
|
||||
|
||||
정리 방법은 다음 순서를 따른다.
|
||||
|
||||
1. 모순된 두 답을 그대로 나열한다(추측으로 하나를 지우지 않는다).
|
||||
2. 먼저 온 답 또는 더 구체적인 답을 우선순위 후보로 제안한다.
|
||||
3. "이렇게 이해했는데 맞나요"로 확인받는다. 확인 없이 임의로 하나를 폐기하지 않는다.
|
||||
4. 확인된 우선순위를 브리프 세 줄과 `design.md`에 그대로 남긴다.
|
||||
|
||||
## 10. 무응답·비대화형·서브에이전트
|
||||
|
||||
- **한 번 실행하고 끝나는 실행(`claude -p`, `codex exec`)도 평문으로 묻고 끝낸다.** 구조화 도구로 답을 기다릴 수 없을 뿐, 최종 응답에 브리프 카드 초안과 번호 질문을 남기고 파일을 만들지 않은 채 끝낼 수는 있다. 답은 세션 재개나 다음 실행으로 온다.
|
||||
- **자동 해제로 들어온 빈 답은 무응답으로 취급한다.** Codex의 autoResolution(60~240초 뒤 빈 답 자동 제출), Gemini CLI의 YOLO 모드에서 빈 답이 자동 통과되는 경우가 여기 해당한다. 이 빈 답을 어떤 선택지를 고른 것으로 해석하지 않는다.
|
||||
- **서브에이전트로 실행 중이면 브리프 카드 초안과 질문을 호출자에게 반환하고 멈춘다.** 서브에이전트는 사용자에게 직접 물을 수 없으므로, 0단계는 반드시 메인 세션에서 끝낸다. 서브에이전트가 이 문서를 참조해 초안을 만들었더라도 그 초안은 확정이 아니다.
|
||||
- **가정으로 진행하는 것은 질문이 오류·시간 초과·빈 답으로 끝났을 때뿐이다**(11절의 명시적 위임은 별도). 그때는 모든 가정에 라벨을 붙여 첫 응답에서 밝히고(마지막 응답이 아니라), 12절의 형식으로 `design.md` 미확정 목록에 올린다.
|
||||
- **오케스트레이터가 워커에게 디자인 구현을 넘길 때는 브리프 카드를 작업 패킷에 포함한다.** 워커가 다시 사용자에게 묻는 일이 없도록, 확정된 슬롯과 미확정 가정을 함께 넘긴다.
|
||||
|
||||
## 11. 명시적 위임 예외
|
||||
|
||||
예외는 사용자의 명시적 위임뿐이다 — "알아서 해줘", "묻지 말고 진행" 같은 발화가 여기 해당한다. **"되돌리기 쉬운 습작"은 예외가 아니다.** 되돌리기 쉬움은 사용자가 판단할 몫이지, 모델이 되돌리기 쉬워 보인다는 이유로 인터뷰를 생략할 근거가 아니다.
|
||||
|
||||
위임을 받았을 때도 가정을 침묵 속에 진행하지 않는다.
|
||||
|
||||
1. 어떤 슬롯을 가정으로 채우는지 목록으로 말한다.
|
||||
2. 12절의 형식으로 `design.md` 미확정 목록에 올린다.
|
||||
3. 이후 사용자가 특정 슬롯을 정정하면 그 슬롯만 다시 확정 상태로 바꾼다.
|
||||
|
||||
## 12. 가정 기록 형식
|
||||
|
||||
`design.md`의 미확정 목록에는 아래 네 항목을 슬롯마다 한 행씩 적는다.
|
||||
|
||||
| 슬롯 | 가정한 값 | 근거 | 확인 필요 여부 |
|
||||
|---|---|---|---|
|
||||
| (3절 슬롯 이름) | (실제로 채택한 값) | (스캔한 파일·업종 평균·모델의 판단 중 무엇에서 나왔는지) | (예/아니오 — 다음 상호작용에서 반드시 확인해야 하면 예) |
|
||||
|
||||
근거 칸에 "모델의 판단"이라고 쓸 수밖에 없는 슬롯은 확인 필요를 항상 "예"로 둔다. 스캔한 파일이 근거면 파일 경로를 적는다.
|
||||
|
||||
## 13. 색 자산 확인
|
||||
|
||||
브랜드 자산은 3절 표에서 전체 경로의 필수 슬롯이다. 로고·간판·패키지·기존 토큰에 브랜드 색이 있으면 그것부터 확인한다. **파일로 확인되면 묻지 않는다.** 파일에 색이 없거나 사용자가 이미 "브랜드 색 없음"이라고 말했다면 그것도 확정으로 취급하고 다시 묻지 않는다. 그 외에는(파일도 없고 사용자 발화도 없으면) 묻는다.
|
||||
|
||||
- **기존 색이 있다** → 역할·대비·면적을 검토해 토큰에 반영한다. 반드시 강조색일 필요는 없다.
|
||||
- **없다·상관없다(사용자가 확인함)** → 레퍼런스와 과업에서 색 역할을 정한다.
|
||||
- **아직 확인되지 않았다** → 묻는다.
|
||||
|
||||
> 실측 사례: 꽃집 작업에서 색을 묻지 않고 레퍼런스 세 곳의 배경 평균(`#F8F6F0`)으로 정했다. 결과는 좋았지만 **운이 좋았던 것**이다. 브랜드 색이 있었다면 그걸 무시한 작업이 된다.
|
||||
|
||||
사진 자산의 유무와 조달은 [images.md](images.md) 4-0을 따른다.
|
||||
|
||||
## 14. 좁은 화면 내비
|
||||
|
||||
내비 구조는 항목 수만으로 정하지 않는다. 실제 라벨과 번역 길이, 글꼴 확대, 최소 화면 폭, 가장 중요한 이동 경로, 메뉴 깊이, 키보드 포커스 순서를 함께 본다. 한 줄·가로 스크롤·하단 바·여는 메뉴 중에서 그 조건에서 과업을 가장 덜 방해하는 방식을 고르고, 실제 좁은 화면과 확대 상태에서 검사한다.
|
||||
|
||||
여는 메뉴가 필요하다고 판단되면 [layout.md](layout.md)의 모바일 내비 절을 본다. 항목이 적어도 긴 번역·깊은 계층이면 여는 메뉴가 맞을 수 있고, 항목이 많아도 우선순위가 뚜렷하면 일부를 분리할 수 있다.
|
||||
|
||||
이 슬롯은 정보 구조·라벨이 정해진 뒤에만 물을 수 있으므로 8절에 따라 2라운드에서 묻는다. 판단 기준(라벨 길이·확대·폭·우선순위·깊이·포커스 순서)은 여기서 바뀌지 않는다.
|
||||
|
||||
## 15. 묻는 것과 묻지 않는 것
|
||||
|
||||
- **묻는다**: 결과를 바꾸는 결정(3절 슬롯), 사용자만 아는 사실(실제 수치·이름·재고·연락처 같은 것).
|
||||
- **묻지 않는다**: 코드를 읽거나 검색하면 확인되는 사실.
|
||||
|
||||
이 스킬에는 "관례적으로 명확해서 묻지 않아도 되는" 디자인 결정이 없다는 것이 전제다. 무엇을 만들지, 누구에게 무엇을 시킬지, 어떤 인상을 줄지는 업종이 같아도 브랜드마다 다르다 — 관례를 근거로 질문을 생략하면 1절의 실측 사례처럼 넷 중 넷이 틀린다. 질문을 생략할 수 있는 유일한 근거는 "이미 코드·검색으로 확인했다"뿐이다.
|
||||
303
packages/skill/references/change-review.md
Normal file
303
packages/skill/references/change-review.md
Normal file
|
|
@ -0,0 +1,303 @@
|
|||
# change-review — diff·PR·커밋 범위 리뷰
|
||||
|
||||
**리뷰 경로**에서 연다. 화면 전체가 아니라 **git diff·PR·커밋 범위로 정의된 변경**을 스코프로 잡는 요청("이 PR 봐줘", "이 브랜치 확인해줘", "방금 커밋 리뷰해줘", "이거 고친 거 회귀 있는지 봐줘")이 트리거다. 화면 전체를 감사할 때는 이 문서가 아니라 [preflight.md](preflight.md)·[audit-gate.md](audit-gate.md)를 쓴다. 이 문서가 소유하는 것은 **스코프를 어떻게 잡는가**와 **finding에 어떤 상태를 매기는가** 두 가지뿐이다. 무엇이 얼마나 나쁜가(심각도·에스컬레이션·저비용 수정 사다리)는 [critique.md](critique.md) 소관이며, 이 문서는 그것을 다시 정의하지 않는다. 정확성·테스트·보안·성능은 프로젝트의 일반 코드 리뷰 소관이다 — 한 번만 짚고 넘어간다[SKILL-INTERFACE-REVIEW].
|
||||
|
||||
이 문서의 **필수 절차**는 리뷰를 진행하는 과정의 규칙이다. 하드 게이트·프로젝트 계약·관찰 후보라는 판정 위계는 finding 자체에만 붙인다 — 절차 규칙을 하드 게이트라 부르면 심각도 매핑(하드 게이트 = Blocking)과 섞인다.
|
||||
|
||||
## 이 문서를 읽는 법
|
||||
|
||||
| 상황 | 읽을 곳 |
|
||||
|---|---|
|
||||
| 리뷰 대상 자체가 뭔지부터 정해야 한다 | §2 |
|
||||
| 락파일·스냅샷 같은 게 스코프에 잡혀 노이즈가 커진다 | §3 |
|
||||
| 토큰 파일 하나 고쳤는데 몇 화면에 퍼지는지 모른다 | §4 |
|
||||
| "고치다가 실수로 뭔가 지운 것 같다"를 확인해야 한다 | §5 |
|
||||
| "이 버그 우리가 만들었나 원래 있었나"를 가려야 한다 | §6 |
|
||||
| PR이 주장한 걸 다 했는지 확인해야 한다 | §7 |
|
||||
| detached HEAD·shallow clone·리베이스 도중이다 | §8 |
|
||||
| 파일이 이동+편집됐다 | §9 |
|
||||
| GitHub·Forgejo·Gitea의 PR을 가져와야 한다 | §10 |
|
||||
| 리포트를 어떻게 쓰는지 | §11 |
|
||||
| 심각도를 어디서 가져오는지 | §12 |
|
||||
|
||||
---
|
||||
|
||||
## 0. 언제 이 문서를 연다
|
||||
|
||||
트리거: 사용자가 브랜치·PR·커밋 범위·미커밋 변경을 구체적으로 지목한다. "이번에 뭐가 바뀌었나"를 묻는 요청이지 "이 화면이 지금 맞나"를 묻는 요청이 아니다. 두 질문은 다르다 — 전자는 diff의 `-`쪽과 `+`쪽을 함께 읽고 회귀를 찾는 것이고, 후자는 렌더된 최종 상태만 본다. 리뷰 경로는 0(스코프 확정) → 이 문서의 절차 → [critique.md](critique.md) 형식의 보고로 끝나며, 구현이 필요하면 사용자 요청에 따라 국소·연장 경로로 올라간다.
|
||||
|
||||
0단계 브리프 인터뷰(무엇을 만들지 확인)와 이 문서의 "스코프가 불분명하면 묻고 기다린다"(§2, 무엇을 검토할지 확인)는 목적이 다르다. 둘을 섞지 않는다. 검증 못 한 항목을 "미검증"으로 표시하고 진행하는 것(§11)도 0단계 인터뷰와는 별개 축이다 — 인터뷰는 시작 전에, 미검증 표시는 끝난 뒤에 쓴다[SKILL-INTERFACE-REVIEW].
|
||||
|
||||
---
|
||||
|
||||
## 1. 읽기 전용 계약
|
||||
|
||||
**필수 절차.** 리뷰 대상 브랜치·PR·커밋을 체크아웃·switch·stash·reset하지 않는다. 작업자가 열어 둔 파일을 리뷰 스킬이 덮어쓰거나 버리게 만드는 사고를 막는 안전 계약이며, 예외 없이 지킨다.
|
||||
|
||||
- **허용**: `git fetch`(원격 ref를 `.git` 안에만 쓴다), `git show <ref>:<path>`로 파일 읽기, `git blame`, `git diff`, `git log -L`, `git grep <ref>`.
|
||||
- **금지**: `git checkout <ref>`, `git switch <ref>`, `git stash`, `git reset --hard`, 포지 CLI의 `pr checkout`류 명령.
|
||||
- **파일은 워킹트리 사본이 아니라 `git show <ref>:<path>`로 읽는다.** 특히 포크에서 온 PR은 워킹트리와 다른 파일일 수 있다.
|
||||
- **렌더 검증이 필요하면**: 옵트인이다. 프로젝트가 저렴한 프리뷰(스테이징 배포 등)를 이미 제공하거나 사용자가 명시적으로 요청하지 않는 한, 시각·런타임 주장은 **"미검증"**으로 남긴다(§11). 격리가 필요하면 저장소 안 디렉터리에 `git worktree add`로 임시 워크트리를 만들고 끝나면 `git worktree remove`로 정리한다 — `rm -rf`로 지우지 않는다. 리뷰 대상 저장소가 무엇이든 같은 위생을 지킨다[SKILL-INTERFACE-REVIEW].
|
||||
|
||||
---
|
||||
|
||||
## 2. 스코프 계산
|
||||
|
||||
**필수 절차.** 리뷰 대상을 지어내지 않는다 — 무엇을 봤다고 주장하는지 먼저 확정하고, 그 확정 근거를 스코프 블록(§11)에 남긴다.
|
||||
|
||||
### 대상 미지정일 때의 우선순위
|
||||
|
||||
사용자가 타깃을 지목하지 않으면 아래 순서로 시도하고 **첫 매치에서 멈춘다**.
|
||||
|
||||
1. `HEAD`가 `git merge-base origin/<default-branch> HEAD` 대비 앞서 있다 → 그 범위 **+** 미커밋 변경. 커밋 수와 미커밋 파일 수를 **따로** 보고한다.
|
||||
2. 워킹트리가 dirty(추적되는 파일이든 아니든)하다 → 미커밋 변경만.
|
||||
3. 둘 다 아니다 → "리뷰할 변경이 없음"(아래 함정 참고).
|
||||
|
||||
**작업 트리를 먼저 확인해야 하는 이유**: 순서를 뒤집으면 한 줄짜리 포매팅 수정이 열두 커밋짜리 브랜치를 가리면서도 "전체를 봤다"고 보고하는 사고가 난다. 추적되지 않은 새 파일도 포함한다 — 새로 추가한 컴포넌트가 "전체 커버"라 주장하는 스코프에서 조용히 빠지는 것을 막는다[SKILL-INTERFACE-REVIEW].
|
||||
|
||||
### 대상이 명시됐을 때
|
||||
|
||||
| 타깃 | 의미 |
|
||||
|---|---|
|
||||
| `working` | 워킹트리 변경(추적+미추적) |
|
||||
| `staged` | 스테이지된 변경만 |
|
||||
| `branch` | 현재 브랜치 vs 기본 브랜치, merge-base 기준 |
|
||||
| `pr <n>` / `mr <n>` | §10 |
|
||||
| 단독 `<ref>` | 그 커밋 하나 |
|
||||
| `<a>..<b>` | 두 점 — 끝점끼리 직접 비교 |
|
||||
| `<a>...<b>` | 세 점 — `merge-base(<a>,<b>)`와 `<b>` 비교 |
|
||||
|
||||
**두 점과 세 점을 임의로 바꾸지 않는다.** 사용자가 쓴 점 개수를 그대로 존중한다. `release..feature`를 세 점으로 바꾸면 `release`와 merge-base 사이의 변경분을 통째로 놓칠 수 있고, 그게 바로 사용자가 물어본 것일 수 있다.
|
||||
|
||||
**브랜치 diff는 merge-base(세 점) 기준이다.** 두 점 diff는 기본 브랜치에 이미 올라간 업스트림 커밋까지 변경으로 잡아버린다.
|
||||
|
||||
### 함정 — `git diff HEAD`가 놓치는 것
|
||||
|
||||
`git diff HEAD`는 **추적된 변경만** 본다. 미커밋 작업이 포함될 수 있는 타깃(`working`, `branch` + 워킹트리 dirty)에서는 반드시 `git ls-files --others --exclude-standard`와 짝지어 새로 추가된 파일을 잡는다. 안 그러면 새 컴포넌트가 "전체 커버"라 주장하는 스코프에서 빠진다.
|
||||
|
||||
### 리뷰할 게 없을 때 — 임의로 대체하지 않는다
|
||||
|
||||
**필수 절차.** 클린 트리 + 기본 브랜치 대비 앞선 커밋이 없으면, 이것은 "존재하지 않는 변경을 리뷰해 달라는 요청"이다. **마지막 커밋(`HEAD~1..HEAD`)으로 임의 대체하지 않는다.** 마지막 커밋은 우연히 거기 있게 된 무엇(머지 커밋이거나 남의 작업일 수 있다)이고, 그것에 대한 보고는 사용자가 실제로 의미한 것과 구분되지 않는다[SKILL-INTERFACE-REVIEW].
|
||||
|
||||
발명하는 대신 사실을 먼저 모으고 제시한 뒤 **대기**한다.
|
||||
|
||||
1. 현재 브랜치, 클린 여부, 기본 브랜치 대비 커밋 수, 마지막 커밋의 짧은 SHA와 제목을 모은다.
|
||||
2. 현재 브랜치에 열린 PR/MR이 있는지 확인하고, 있으면 **가장 먼저** 제시한다 — 커밋이 이미 머지돼 "변경 없음"으로 보여도 그 PR은 여전히 사용자가 의미한 것일 수 있다.
|
||||
3. 정확히 3가지 경로만 제시한다: (a) 마지막 커밋 — SHA와 제목을 구체적으로 밝혀 뭘 받을지 보이게 함, (b) 사용자가 지정하는 타깃, (c) 저장소 전체 인터페이스 감사(이건 change review가 아니라 [critique.md](critique.md)의 화면 감사로 그대로 핸드오프 — 스코프 블록·상태·Pre-existing 섹션 없이).
|
||||
|
||||
제외 규칙(§3) 적용 후 스코프가 비었을 때도 같은 방식으로 묻는다. **"아무것도 볼 게 없는 상태"를 통과로 보고하지 않는다.**
|
||||
|
||||
---
|
||||
|
||||
## 3. 제외 경로와 예외
|
||||
|
||||
**프로젝트 계약.** 아래는 리뷰 스코프에서 제외해 노이즈를 줄이는 기본값이다 — 프로젝트가 다른 생성물 경로를 쓰면 맞춰 조정한다.
|
||||
|
||||
| 카테고리 | 패턴 |
|
||||
|---|---|
|
||||
| 락파일 | `package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`, `bun.lock`, `bun.lockb`, `Cargo.lock`, `composer.lock`, `Gemfile.lock`, `poetry.lock`, `uv.lock` |
|
||||
| 스냅샷·픽스처 | `__snapshots__/`, `*.snap`, `*.approved.*`, `test-results/`, `playwright-report/` |
|
||||
| 생성 산출물 | `dist/`, `build/`, `out/`, `.next/`, `.turbo/`, `coverage/`, `*.min.js`, `*.min.css`, `*.map` |
|
||||
| 생성 소스 | `*.gen.ts`, `*.generated.*`, 빌드가 뱉은 `*.d.ts` |
|
||||
| 벤더 코드 | `vendor/`, `third_party/`, `node_modules/` |
|
||||
| 바이너리·미디어 | 원본 자산 바이트 자체(폰트·이미지 예외는 아래 참고) |
|
||||
|
||||
**무엇을 제외했는지 리포트에 이름으로 밝힌다.** 제외 후 스코프가 비면 §2의 "리뷰할 게 없음" 처리와 같다.
|
||||
|
||||
**예외 2가지는 스코프에 남는다** — 바이트 자체가 아니라 그것을 참조하는 코드를 본다.
|
||||
|
||||
- 추가·교체된 **폰트 파일**: [typography.md](typography.md)가 다루는 로딩·서브셋 계약이 바뀌었을 수 있다.
|
||||
- 컴포넌트에 추가된 **이미지**: `alt` 텍스트 분류와 파일 조달·크롭 모두 [images.md](images.md) 소관.
|
||||
|
||||
**언더익스클루전 함정 2가지**
|
||||
|
||||
1. `*.lock`은 `yarn.lock`·`Cargo.lock`은 잡지만 `package-lock.json`·`pnpm-lock.yaml`은 못 잡는다 — 표의 확장자를 각각 커버해야 한다.
|
||||
2. `**`는 glob 매직이 필요하다. 없으면 `*`가 `/`를 못 건너뛰어 `**/dist/**`가 루트 레벨 `dist/`를 놓친다.
|
||||
|
||||
**검증**: 제외 pathspec 유무로 diff를 두 번 돌려, 파일 수가 정확히 이름 붙인 만큼 줄었는지 확인한다[SKILL-INTERFACE-REVIEW].
|
||||
|
||||
---
|
||||
|
||||
## 4. 파급 범위 — diff는 표면이 아니다
|
||||
|
||||
**프로젝트 계약.** 변경 파일은 증거일 뿐 리뷰 대상 전부가 아니다. 실제로 리뷰할 것은 그 파일이 렌더되는 **표면 전체(blast radius)**다.
|
||||
|
||||
- **기본은 1홉 확장**: 변경된 컴포넌트·함수를 직접 import·호출하는 것까지.
|
||||
- **2홉 확장은 디자인 토큰·테마 값·공유 프리미티브에 한정**한다 — 한 줄이 제품 전체에 닿을 수 있는 파일이라서다. [tokens.md](tokens.md)가 유일 원본인 토큰 파일이 여기 해당한다.
|
||||
- **고정 개수 상한을 두지 않는다.** 대신 위험 기준으로 고르고 선언한다: 사용자에게 노출되는 경로(라우트·레이아웃 진입점) 먼저, 그다음 importer 수가 많은 순, 동률이면 같은 패키지·기능 디렉터리 근접성. 프로젝트에 라우트 개념 자체가 없으면(컴포넌트 라이브러리 등) 1순위를 건너뛰고 바로 importer 수로 정렬한다. **몇 개를 확장했고 몇 개를 확장하지 않았는지 리포트에 명시한다** — 상한 없는 스윕은 주장할 수 있는 커버리지를 지지하지 못하고, 컷오프를 밝히지 않으면 완전한 것처럼 읽힌다.
|
||||
|
||||
### 컨슈머 검색 명령
|
||||
|
||||
```bash
|
||||
# 리뷰 중인 ref를 대상으로 검색한다 — 워킹트리를 검색하면
|
||||
# 이 변경 자체가 추가한 importer를 놓친다(특히 PR에서)
|
||||
git grep -n "ComponentName" "$REVIEWED_REF" -- '*.tsx' '*.astro'
|
||||
|
||||
# 패턴이 -로 시작하면(예: 토큰명 --color-*) -e로 옵션 파싱을 막는다
|
||||
git grep -n -e "--color-brand" "$REVIEWED_REF"
|
||||
```
|
||||
|
||||
결과는 `<rev>:path/to/file` 형태로 오며 `git show`로 읽는다(워킹트리 사본 금지, §1). **토큰·테마 값이 바뀐 경우 파일명이 아니라 토큰 이름으로 검색한다** — 컨슈머는 토큰을 import하지 않고 이름으로 참조하기 때문이다[SKILL-INTERFACE-REVIEW].
|
||||
|
||||
---
|
||||
|
||||
## 5. 제거된 쪽 읽기
|
||||
|
||||
**필수 절차다.** 회귀는 변경 후 상태에서는 보이지 않는다. `+`쪽만 읽고 끝내지 않는다 — 모든 헝크의 `-`쪽을 아래 표와 대조한다. 이 절차 자체(제거된 쪽을 읽는다)는 건너뛰지 않는 필수 단계다. 다만 **표에 걸린 각 항목의 실제 심각도**는 그 항목을 소유하는 문서(accessibility.md·typography.md 등)의 판정 위계를 따른다 — 여기서 다시 정의하지 않는다.
|
||||
|
||||
| `-`쪽에서 제거된 것 | 소유 문서 | 확인할 것 |
|
||||
|---|---|---|
|
||||
| `aria-label`, `aria-labelledby`, `aria-describedby`, `aria-live`, `role=` | [accessibility.md](accessibility.md) | 컨트롤·영역이 접근 가능한 이름·설명·알림을 잃었는가 |
|
||||
| `<label`, `for=`, `scope=` | [accessibility.md](accessibility.md) | 필드·표 셀이 프로그램적 연결을 잃었는가 |
|
||||
| `alt=` | [images.md](images.md) | 이미지가 대체 텍스트를 잃었는가 — 분류는 images.md §5 |
|
||||
| `<button>`, `<a>`, `<nav>`, `<main>`, `<ul>`이 `div`·`span`으로 교체 | [accessibility.md](accessibility.md) | 키보드·보조기술 동작이 스타일과 맞바뀌었는가 |
|
||||
| `:focus-visible`, `:focus`, `outline`, `tabindex` | [accessibility.md](accessibility.md) | 포커스 표시자를 잃었거나 탭 순서에서 빠졌는가 |
|
||||
| `prefers-reduced-motion`, `prefers-contrast` | [accessibility.md](accessibility.md) | 모션·대비가 시스템 선호를 더는 존중하지 않는가 |
|
||||
| 논리 속성이 `left`·`right`로 교체 | [layout.md](layout.md) | 방향 인식 레이아웃이 빠졌는가 |
|
||||
| `lang=`, `dir=` | [typography.md](typography.md) | 언어 메타·텍스트 방향이 빠졌는가 |
|
||||
| `text-wrap`, `line-clamp`, `overflow-wrap`, `tabular-nums` | [typography.md](typography.md) | 텍스트 렌더링·줄바꿈·숫자 정렬이 조용히 바뀌었는가 |
|
||||
| 색 토큰이 리터럴로, 또는 더 옅은 토큰으로 교체 | [color.md](color.md) | 렌더된 대비 쌍이 실패할 수 있다 — 실측한다 |
|
||||
| 사용자 노출 문자열이 삭제·축약 | [product-copy.md](product-copy.md) | 라벨·오류·빈 상태가 담던 정보를 잃었는가 |
|
||||
|
||||
**등가 대체 — 아래 7가지는 회귀가 아니다.** 먼저 확인하지 않으면 정당한 리팩터를 회귀로 오보한다.
|
||||
|
||||
1. `aria-label`이 화면에 보이는 텍스트를 가리키는 `aria-labelledby`로 바뀜.
|
||||
2. 명시적 `role`이 사라졌지만 요소 자체가 네이티브 동급이 됨(`role="button"`인 `div` → `<button>`).
|
||||
3. `outline`이 여전히 포커스 표시 규칙을 충족하는 `box-shadow` 포커스 링으로 교체.
|
||||
4. 네이티브로 focusable해진 요소에서 `tabindex="0"`이 빠짐.
|
||||
5. 색 리터럴이 같은 렌더 페어를 측정하는 토큰으로 교체.
|
||||
6. 물리적 속성이 논리적 대응으로 교체(이건 회귀가 아니라 수정 그 자체).
|
||||
7. 문자열이 삭제가 아니라 번역 카탈로그·상수 파일로 이동(다국어 프로젝트에 한정 — designpaca가 다루는 단일 로케일 랜딩페이지는 대부분 카탈로그 자체가 없으므로 이 항목이 적용되지 않는다).
|
||||
|
||||
### 검색 명령
|
||||
|
||||
```bash
|
||||
git diff -U0 "$BASE"...HEAD -- '*.tsx' '*.css' \
|
||||
| grep -E '^-[^-]' \
|
||||
| grep -E 'aria-|role=|alt=|focus|tabindex|prefers-'
|
||||
```
|
||||
|
||||
**컨텍스트 없이 결론 내지 않는다.** `-U0`는 의도적으로 컨텍스트를 숨기며, 제거된 속성은 그것이 붙어 있던 요소 없이는 의미가 없다. 헝크 컨텍스트를 넓혀(`-U3` 이상) 실제로 무엇이 없어졌는지 확인한 뒤 표로 가져간다[SKILL-INTERFACE-REVIEW].
|
||||
|
||||
---
|
||||
|
||||
## 6. 상태 분류
|
||||
|
||||
**필수 절차.** 모든 finding에 상태 하나를 매긴다 — 생략하지 않는다.
|
||||
|
||||
| 상태 | 의미 |
|
||||
|---|---|
|
||||
| `Introduced` | 이 변경이 새로 만들었다 |
|
||||
| `Regression` | 이 변경이 이전에 정상이던 것을 약화시켰다 |
|
||||
| `Pre-existing` | 손댄 코드 안에 있지만 이 변경이 원인이 아니다 |
|
||||
|
||||
**파일이 아니라 diff가 실제로 건드린 지점으로 상태를 매긴다.** 헝크에서 몇 줄 떨어진, 이번 변경이 손대지 않은 줄은 `Pre-existing`이다. 필요하면 base ref와 대조한다:
|
||||
|
||||
```bash
|
||||
git blame -L <line>,<line> "$BASE" -- path/to/file
|
||||
```
|
||||
|
||||
**`Pre-existing`은 §12의 심각도 집계와 별도로 나열한다.** 레거시 파일 하나를 건드렸다고 전체 감사 범위가 되는 것을 막는다. 심각도 높은 순으로 정렬한다. 고정 개수 상한은 두지 않는다 — [critique.md](critique.md) §7이 이미 정한 원칙("숫자 상한은 근거 없는 수치이고, 스코프를 좁히는 판단이 상한보다 먼저다")을 여기서도 그대로 따른다. 목록이 감당하기 어렵게 길면(레거시 파일을 광범위하게 건드린 변경) 전부 나열하는 대신 "Pre-existing N건, 이 변경 책임 아님"으로 요약하고 근거를 밝힌다.
|
||||
|
||||
---
|
||||
|
||||
## 7. 의도 대비 완성도
|
||||
|
||||
**프로젝트 계약.** PR 제목·본문, 연결된 이슈, 커밋 메시지를 읽고 인터페이스가 그 주장을 실제로 이행하는지 본다. 표면 리뷰만으로는 안 보이는 "미완성 변경"을 드러내는 항목들이다.
|
||||
|
||||
- **변형 상태 매트릭스**: 새 variant·size·theme가 hover·focus·active·disabled·loading·selected 중 일부에만 적용됐는가. 새 컴포넌트에 empty·loading·error·disabled·narrow-width 상태가 빠졌는가.
|
||||
- **형제 표면 대칭**: 한 표면에만 추가된 컨트롤이, 이미 그 동료 컨트롤을 가진 형제 표면들에는 없는가.
|
||||
- **번역 카탈로그**(다국어 프로젝트에 한정. 언제: 프로젝트가 실제로 번역 카탈로그를 쓸 때만 — 단일 로케일 하드코드 카피가 표준인 프로젝트에는 적용하지 않는다): 새 사용자 노출 문자열에 카탈로그 항목이 없는가.
|
||||
|
||||
**scope creep(변경이 너무 많은 일을 하는가)은 이 문서가 보고하지 않는다.** 그건 인터페이스 문제가 아니라 프로세스 문제다[SKILL-INTERFACE-REVIEW].
|
||||
|
||||
---
|
||||
|
||||
## 8. 까다로운 저장소 상태
|
||||
|
||||
- **Detached HEAD**: 기본 브랜치 대비 merge-base를 그대로 쓰되, 스코프 블록의 "브랜치" 필드에는 브랜치명 대신 **SHA**를 적는다.
|
||||
- **Shallow clone**(CI 기본값 — `merge-base`가 아무것도 못 돌려준다): `git fetch --deepen=50` 후 재시도, 안 되면 `--deepen=200`, 그래도 안 되면 "스코프 해석 불가"로 보고한다. `--deepen`은 `.git` 아래에만 쓰므로 §1의 읽기 전용 계약을 어기지 않는다.
|
||||
- **리베이스·머지 진행 중**(조용히 실패하는 유일한 케이스 — `git diff`가 성공해서 뭔가를 돌려주지만 그게 실제 변경이 아니다): `git rev-parse --git-path rebase-merge`(및 `rebase-apply`, `MERGE_HEAD`, `CHERRY_PICK_HEAD`)로 감지한다. **`.git/` 경로를 직접 테스트하지 않는다** — 링크된 워크트리 안에서는 디렉터리가 아닐 수 있다. 감지되면 "트리가 작업 도중"이라고 말하고 멈춘다.
|
||||
- **그 외 모든 실패**(원격 없음, unrelated histories, 커밋 없는 저장소)는 `merge-base`에서 실패로 나타난다 — base를 못 정했다고 말하고 멈춘다. **이름 붙일 수 없는 범위는 리뷰하지 않는다**[SKILL-INTERFACE-REVIEW].
|
||||
|
||||
---
|
||||
|
||||
## 9. 이름 변경 감지
|
||||
|
||||
리네임 감지는 git에서 기본 켜져 있다(`--name-status`에서 `R100 old/path new/path`). 같은 변경에서 이동과 편집이 동시에 일어났으면 감지 윈도를 넓힌다:
|
||||
|
||||
```bash
|
||||
git diff --find-renames=40% --find-copies-harder "$BASE"...HEAD
|
||||
```
|
||||
|
||||
**리네임은 삭제+추가가 아니라 이동으로 리뷰한다.** 이동에서 살아남은 코드는 손대지 않은 코드다 — 진짜 편집분만 스코프에 넣는다. 이렇게 하지 않으면 파일 이동이 잦은 리팩터에서 안 바뀐 코드가 새 결함처럼 보고된다.
|
||||
|
||||
---
|
||||
|
||||
## 10. PR·MR 가져오기 (포지 중립)
|
||||
|
||||
**언제**: 사용자가 PR·MR 번호나 링크를 지목했을 때.
|
||||
|
||||
GitHub·Forgejo·Gitea·GitLab은 CLI와 PR ref 노출 방식이 서로 다르고, 이 스킬은 특정 포지를 전제하지 않는다. 아래는 어떤 포지에서든 통하는 git 기반 절차이며, 포지 CLI(`gh`, `tea`, `glab` 등)가 있으면 메타데이터(제목·본문·상태) 조회를 더 짧게 해 준다.
|
||||
|
||||
1. **head를 로컬 ref로 받는다.** 많은 포지가 PR/MR head를 가리키는 서버측 ref를 노출한다(예: GitHub의 `refs/pull/<n>/head`). 정확한 경로는 프로젝트가 쓰는 포지마다 다를 수 있으므로 확실하지 않으면 지어내지 말고, 원격 ref 목록을 직접 조회해 확인한다(`git ls-remote origin | grep -i pull`류). 확인이 안 되면 기여자 브랜치를 직접 지정해 fetch하는 경로로 대체한다:
|
||||
```bash
|
||||
git fetch origin <contributor-branch>:refs/remotes/pr/<n>
|
||||
```
|
||||
서버측 PR ref가 확인되면 그것을 쓴다:
|
||||
```bash
|
||||
git fetch origin "refs/pull/<n>/head:refs/remotes/pr/<n>"
|
||||
```
|
||||
두 방식 모두 포크에서 온 기여에도 동작한다(`origin/<branch>`만으로는 포크 브랜치에 닿지 않는다).
|
||||
2. **파일은 `git show refs/remotes/pr/<n>:path/to/file`로 읽는다.** 워킹트리 사본을 열지 않는다(§1) — 포크 PR에서는 다른 파일일 수 있다.
|
||||
3. **포지 CLI의 diff/patch 보기는 지름길이지만 한계가 있다** — 변경 안 된 컨텍스트를 못 읽고, 컨슈머로 확장할 방법이 없다. 항상 ref도 같이 fetch한다.
|
||||
4. **의도(stated intent)**: PR·MR의 제목·본문이 §7의 "의도 대비 완성도"에서 대조할 주장이다. 본문이 비어 있으면 커밋 제목들을 대신 쓴다.
|
||||
5. **인용 줄 번호**: 리뷰는 fetch된 ref 기준이고, 그 줄 번호는 워킹트리와 다를 수 있다. §11의 스코프 블록에 head ref와 그 SHA를 선언해 줄 번호가 무엇 기준인지 풀리게 한다.
|
||||
|
||||
포지 CLI가 없거나 인증이 안 돼 있어도 오류가 아니라 "PR 메타데이터 없음"으로 취급하고, §2의 3경로(마지막 커밋 / 사용자 지정 타깃 / 저장소 전체 감사)를 제시한다[SKILL-INTERFACE-REVIEW].
|
||||
|
||||
---
|
||||
|
||||
## 11. 리포트 형식
|
||||
|
||||
### 스코프 블록 — 리뷰를 시작하기 전에 무엇을 봤다고 주장하는지 먼저 밝힌다
|
||||
|
||||
| 필드 | 값 |
|
||||
|---|---|
|
||||
| Target | `branch`, `working`, `staged`, `pr 482`, 또는 사용자가 입력한 범위 그대로 |
|
||||
| Base ref | `origin/main` at `a1b2c3d` |
|
||||
| Head ref | `refs/remotes/pr/482` at `e4f5g6h` |
|
||||
| Commits | 7 커밋 + 미커밋 파일 2개(따로 표기) |
|
||||
| Files in scope | 제외 적용 후 12개 |
|
||||
| Excluded | `pnpm-lock.yaml`, `src/__snapshots__/` — 락파일과 스냅샷 |
|
||||
| Surfaces expanded | `CheckoutPage`, `SettingsPanel`; `Button` 컨슈머 3개는 더 확장하지 않음 |
|
||||
|
||||
### findings 표 — [critique.md](critique.md) §3 형식에 상태 열을 더한다
|
||||
|
||||
finding 형식·표 컬럼(심각도·위치·무엇·왜·고침)은 새로 만들지 않는다 — critique.md §3을 그대로 쓰고, 앞에 **상태** 열 하나만 더한다.
|
||||
|
||||
| 상태 | 심각도 | 위치 | 무엇 | 왜 | 고침 |
|
||||
|---|---|---|---|---|---|
|
||||
| Regression | Blocking | `src/Dialog.tsx:42` | `aria-label="닫기"`가 이 변경에서 제거됨 | 하드 게이트 — 닫기 컨트롤이 이 변경 전에는 접근 가능한 이름을 갖고 있었고 지금은 없다 [WCAG-NRV] | `aria-label="닫기"` 복원 |
|
||||
|
||||
`Introduced`·`Regression`이 하나도 없으면 표를 생략하고 critique.md §13 템플릿을 그대로 쓴다: **"이 흐름에서 Blocking·Important 발견 없음. Polish 관찰 후보는 \<있으면 나열, 없으면 없음\>."**
|
||||
|
||||
### Pre-existing 섹션
|
||||
|
||||
§6에서 분리한 목록을 심각도 높은 순으로 따로 둔다. "이 변경의 책임이 아니다"라고 평이하게 적는다. 없으면 섹션 자체를 생략한다. 이 섹션은 critique.md §4의 심각도 집계에 들어가지 않는다(§12).
|
||||
|
||||
### 미검증 · 미점검
|
||||
|
||||
렌더 검증을 하지 않았거나(§1, 옵트인) 확인할 수단이 없는 주장은 [critique.md](critique.md) §8의 두 상태를 그대로 구분해 쓴다 — **미검증**(시도했지만 도구·권한·환경 제약으로 확인 못 함, 통과로 추정하지 않는다)과 **미점검**(§4의 파급 범위 확장에서 의도적으로 뺀 컨슈머 등, "확인 안 함"이라고 명시). 둘 다 finding 개수·심각도 집계에 넣지 않는다.
|
||||
|
||||
**빈 스코프를 "문제 없음"으로 보고하지 않는다.** §2에서 이미 처리된 "리뷰할 게 없음"과, 스코프는 있지만 findings가 없는 "실행 가능한 결함 없음"을 혼동하지 않는다.
|
||||
|
||||
---
|
||||
|
||||
## 12. 판정 — critique.md에 맡긴다
|
||||
|
||||
**심각도 스케일·에스컬레이션 트리거·finding 상한·저비용 수정 우선순위는 이 문서가 소유하지 않는다.** 심각도·에스컬레이션·저비용 수정은 [critique.md](critique.md) §4~§5, finding에 상한을 두지 않는 원칙은 §7을 따른다. 이 문서가 하는 일은 findings에 상태(§6)를 매겨 그 심각도 체계에 넘기는 것뿐이다.
|
||||
|
||||
- `Pre-existing`은 critique.md §4의 심각도 집계에서 제외한다 — 레거시 결함만 있는 변경은 Blocking·Important 집계가 비게 된다.
|
||||
- `Introduced`·`Regression`만 심각도 집계에 들어간다.
|
||||
- critique.md가 아직 없거나 심각도 매핑을 못 찾으면, 스코프와 파일 인벤토리만 보고하고 무엇이 누락됐는지 밝힌 뒤 멈춘다 — 심각도 스케일을 이 문서에서 임의로 지어내지 않는다[SKILL-INTERFACE-REVIEW].
|
||||
259
packages/skill/references/color.md
Normal file
259
packages/skill/references/color.md
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
# color — 색 체계 상세
|
||||
|
||||
3단계에서 [tokens.md](tokens.md) §2 의 역할 토큰(`--surface`·`--surface-raised`·`--ink`·`--ink-muted`·`--line`·`--accent`·`--accent-ink`)을 실제 값으로 채울 때 필요한 절만 연다. **역할 토큰이 정본이고 이 문서는 방법이다** — 여기서 새 역할을 만들거나 `tokens.md`의 규칙을 바꾸지 않는다. 이 문서는 그 역할에 어떤 값을 어떻게 채우는지, 다크모드·그라디언트·대비를 어떻게 다루는지를 다룬다.
|
||||
|
||||
## 이 문서를 읽는 법
|
||||
|
||||
| 상황 | 읽을 곳 |
|
||||
|---|---|
|
||||
| 역할 토큰에 채울 실제 색이 없다(새 시스템) | §1~§3 |
|
||||
| 여러 단계(램프)가 필요하다 | §2 |
|
||||
| 그라디언트를 쓴다 | §4 |
|
||||
| 다크모드를 만들거나 다듬는다 | §5 |
|
||||
| 대비가 실패했거나 대비 근거가 필요하다 | §6 |
|
||||
| 상태색·강조색의 의미와 로케일을 정한다 | §7 |
|
||||
| 표준 7개 역할 밖의 색(비활성·scrim 등)이 필요하다 | §8 |
|
||||
| 고채도 브랜드 색이 화면에서 탁하게 보인다 | §9 |
|
||||
| 리디자인에서 기존 팔레트를 정리한다 | §10 |
|
||||
| 값을 어떤 표기법(hex·oklch 등)으로 쓸지 모르겠다 | §11 |
|
||||
| 마무리 전 확인 | §12 |
|
||||
|
||||
---
|
||||
|
||||
## §0. 언제
|
||||
|
||||
색을 다루는 모든 순간이 아니라, **역할 토큰에 값을 채우는 3단계**와 **다크모드·그라디언트·리디자인처럼 별도 결정이 필요한 순간**에만 연다. 브리프·기존 코드에 이미 정해진 색이 있으면 그것을 재사용하고 이 문서는 건너뛴다.
|
||||
|
||||
판정 위계는 이 문서 전체에서 세 층으로 나뉜다. **하드 게이트**는 WCAG 2 대비처럼 협상 대상이 아닌 규범이다. **프로젝트 계약**은 브리프·다크모드 지원 여부·특정 프레임워크 채택처럼 이 프로젝트가 선택하면 지키는 약속이다. **관찰 후보**는 스타일 휴리스틱이며 시작값일 뿐 통과 기준이 아니다.
|
||||
|
||||
---
|
||||
|
||||
## §1. 역할 우선과 선택적 원시값 계층
|
||||
|
||||
> 언제: 다중 hue 브랜드(강조색이 여러 개 필요)이거나 정밀한 다크모드 튜닝이 필요할 때만 연다.
|
||||
|
||||
[tokens.md](tokens.md) §2의 7개 역할 토큰은 대부분의 프로젝트에 충분한 **시맨틱 계층**이다. 시맨틱 토큰은 직무를 이름 짓고(`--accent`, `--ink-muted`) 컴포넌트가 참조하는 유일한 계층이다.
|
||||
|
||||
그 아래에 **원시값(primitive) 계층**을 둘지는 선택이다. 원시값은 값을 이름 짓고(`--blue-500`, `--neutral-200`) 컴포넌트에 절대 직접 쓰이지 않으며, 시맨틱 토큰이 그것을 가리킨다. 이 2계층 구조가 다크모드·화이트라벨 테마·증가-대비 변형을 가능하게 하는 이음매다 — 경계가 없으면 다크 모드를 만들 때마다 "이 파랑이 강조를 뜻했는지 그냥 파랑을 원했는지"를 모든 사용처에서 가려내야 한다 [SKILL-BETTER-COLORS].
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* 원시값(1계층) — 값을 이름 짓는다. 컴포넌트에 직접 쓰지 않는다 */
|
||||
--blue-500: #3b82f6;
|
||||
--neutral-700: #374151;
|
||||
|
||||
/* 시맨틱(2계층) — 직무를 이름 짓는다. tokens.md §2의 역할과 일치시킨다 */
|
||||
--accent: var(--blue-500);
|
||||
--ink-muted: var(--neutral-700);
|
||||
}
|
||||
```
|
||||
|
||||
**단순 프로젝트에 원시값 계층을 강제하지 않는다.** 다중 hue나 정밀 다크모드 튜닝이 필요 없으면 `tokens.md`의 7개 역할 토큰에 값을 직접 채우는 것으로 끝낸다 — 계층을 늘리는 것 자체가 목적이 아니다.
|
||||
|
||||
---
|
||||
|
||||
## §2. 램프 생성
|
||||
|
||||
> 언제: 하나의 hue에서 여러 단계(호버·활성·옅은 배경·솔리드 필 등)가 동시에 필요할 때만 연다. 역할 토큰 하나에 값 하나면 이 절은 필요 없다.
|
||||
|
||||
잘 만든 램프는 네 가지 속성을 갖는다 [SKILL-BETTER-COLORS].
|
||||
|
||||
1. **지각 명도 간격이 고르다.** 포맷이 부르는 명도가 아니라 사람이 실제로 느끼는 명도로 계단진다 — HSL의 명도는 비지각적이라 고르게 벌린 HSL 값이 한쪽으로 뭉친다.
|
||||
2. **hue가 끝까지 일정하다.** 방황하는 hue는 두 색이 섞인 것처럼 읽힌다.
|
||||
3. **생생함(채도·명도의 지각적 강도)은 중간에서 정점을 찍고 양끝에서 떨어진다.** 양끝까지 완전 채도를 유지하면 빛나는 밝은 끝과 잉크를 쏟은 듯한 어두운 끝이 된다.
|
||||
4. **밝은 끝일수록 스텝이 촘촘하다.** 밝은 배경은 더 세밀한 구분이 필요하다 — 전 범위를 고르게 배치하면 옅은 끝의 두 표면(예: 페이지 배경과 카드 배경)이 구별되지 않는다.
|
||||
|
||||
**관찰 후보 — 브랜드 색을 고정하는 두 방식.** 계약상 정확히 지켜야 하는 브랜드 색은 **핀(pin)** — 그 값을 정확히 유지하고 램프가 그 주변에서 바깥으로 지어지며, 그 스텝만 약간 고르지 않게 배치된다. 그 외에는 **스냅(snap)** — 램프에 맞춰 모든 스텝을 고르게 배치한다. 스냅이 대개 더 고르게 보이고, 스와치를 나란히 대보지 않으면 차이가 잘 드러나지 않는다 [SKILL-BETTER-COLORS].
|
||||
|
||||
**절대 손이나 눈으로 계산하지 않는다.** 색 라이브러리(`culori`, `colorjs.io`, `chroma.js` 등)로 지각 공간에서 보간하고 프로젝트 표기법으로 출력한다 [SKILL-BETTER-COLORS].
|
||||
|
||||
```js
|
||||
import { formatHex, interpolate, samples } from 'culori'
|
||||
|
||||
// 지각적 보간. hex로 받아 hex로 낸다
|
||||
const ramp = interpolate(['#eff6ff', '#3b82f6', '#172554'], 'lab')
|
||||
const steps = samples(11).map((t) => formatHex(ramp(t)))
|
||||
```
|
||||
|
||||
**Tailwind `50`-`950` 11스텝, Radix `1`-`12` 12스텝 매핑은 그 체계를 이미 쓰는 프로젝트에서만 참조한다.** 이 매핑을 새 프로젝트의 기본값으로 강제하지 않는다. 두 컨벤션은 번호가 아니라 종류가 다르다 — Radix는 스텝을 역할로 정의해 다크 스케일이 같은 번호를 재사용하는 별도 램프라 `--accent-9`가 라이트·다크 양쪽에서 "솔리드 필"을 뜻하지만, Tailwind는 스텝을 명도로 정의해(`50`=밝음, `950`=어두움) 다크모드에서 매핑이 반전된다(페이지 배경이 `950`이 됨) [SKILL-BETTER-COLORS]. 양끝 모두 순수 검정·흰색에는 못 미치게 둔다 — 거기 닿는 순간 색이 페이지 배경이 사는 지점에서 정체성을 잃는다.
|
||||
|
||||
---
|
||||
|
||||
## §3. OKLCH 파생
|
||||
|
||||
`oklch(L C H)` — 명도(L) 0~1, 채도(C) 0~약 0.4, hue(H) 0~360, 알파는 `/`로 분리한다. 지각적으로 균일한 명도·안정적 hue·예측 가능한 램프가 장점이며, 2023년 5월부터 주요 브라우저에서 널리 지원된다(Baseline Widely) [WEB-BASELINE].
|
||||
|
||||
**hue와 chroma를 고정하고 lightness만 움직이면 라이트·다크 쌍을 체계적으로 파생할 수 있다.**
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* hue(259)·chroma(0.19)는 고정, lightness만 움직인다 — 어두운 배경 위에서는 강조색을 밝게 올린다 */
|
||||
--accent-light: oklch(0.55 0.19 259);
|
||||
--accent-dark: oklch(0.75 0.19 259);
|
||||
}
|
||||
```
|
||||
|
||||
**진짜 새 색 시스템이면 `oklch()`가 최선의 기본값이다.** 그 외에는 색 라이브러리로 프로젝트 고유 표기법에 맞춰 같은 연산을 한다 — 표기법 선택 자체는 §11을 따른다.
|
||||
|
||||
`color-mix()`는 상태용 색 파생(호버·비활성 등)에 유용하며 Baseline Widely다(2023년 5월부터) [WEB-BASELINE]. 다만 생성값이 토큰 계층 밖에 놓여 디자인 툴에서 검사할 수 없다는 한계가 있으므로, 반복해서 쓰이는 값이면 토큰으로 승격할지 판단한다. 상대 색 문법(`oklch(from var(--x) calc(l - 0.1) c h)`)도 같은 종류로 강력하지만, 3중 이상 연쇄 파생은 읽을 수 없어진다 — **관찰 후보**로 두고 체인이 길어지면 고정 값으로 되돌린다 [SKILL-BETTER-COLORS].
|
||||
|
||||
---
|
||||
|
||||
## §4. 그라디언트 보간
|
||||
|
||||
> 언제: 실제로 그라디언트를 쓸 때만 연다.
|
||||
|
||||
기본값(아무것도 지정하지 않았을 때)인 sRGB 보간은 직선(rectangular) 공간이다. hue 휠에서 반대편에 있는 두 색 사이를 직선으로 잇기 때문에 그 직선이 중립축 근처를 지나며 **중간 지점이 탁하고 어두운 회색으로 꺼진다(회색 데드존)**. `oklab`도 직선 보간이지만 지각적으로 균일한 밝기를 유지해 sRGB보다 낫다. `oklch`는 극좌표(polar) 공간이라 hue 각도를 돌아가며 보간해 생생함이 끝까지 유지되지만, 대신 두 스톱 사이의 모든 hue를 훑고 지나간다(예: 파랑→핑크가 보라를 거쳐 간다 — 의도한 룩일 수도, 서프라이즈일 수도 있다) [SKILL-BETTER-COLORS].
|
||||
|
||||
```css
|
||||
/* 기본값(sRGB): 지정 안 하면 이것 — 중간이 탁해진다 */
|
||||
.a { background: linear-gradient(to right, #eff6ff, #3b82f6); }
|
||||
|
||||
/* 최선 기본값: 직선 보간이면서 균일한 밝기 */
|
||||
.b { background: linear-gradient(in oklab, #eff6ff, #3b82f6); }
|
||||
|
||||
/* hue 휠을 도는 룩이 필요할 때 */
|
||||
.c { background: linear-gradient(in oklch longer hue, #eff6ff, #3b82f6); }
|
||||
```
|
||||
|
||||
그라디언트 보간 공간 지정 문법은 Baseline Newly available이다(2024년 6월 기준, Firefox가 마지막으로 지원을 완료) — 아직 Widely에는 이르지 않았으므로 지정하지 않았을 때의 기본 sRGB 결과가 허용 가능한지 확인하거나 `@supports` 폴백을 둔다 [WEB-BASELINE].
|
||||
|
||||
**밴딩**은 스톱 간 대비가 낮은 넓은 영역에서 8비트 디스플레이의 계단이 드러나는 현상이다. 대비를 넓히거나 영역을 줄이거나 미세한 노이즈 텍스처를 얹는다. `shorter hue`/`longer hue` 키워드로 `oklch` 보간이 도는 방향을 고를 수 있다.
|
||||
|
||||
---
|
||||
|
||||
## §5. 다크모드 재조정과 전환 메커니즘
|
||||
|
||||
> 언제: 프로젝트가 다크모드를 지원하기로 했을 때 연다. `tokens.md` §2는 "다크는 반전이 아니라 역할별 재정의"라고 이미 선언한다 — 이 절은 그 재정의를 실행하는 구체 체크리스트다.
|
||||
|
||||
**반전은 출발점이지 결과물이 아니다.** 라이트 팔레트를 기계적으로 뒤집은 뒤, 거의 항상 아래 세 가지를 수동으로 다듬어야 한다 [SKILL-BETTER-COLORS]. 이는 **프로젝트 계약** — 다크모드를 지원하기로 했다면 지킨다.
|
||||
|
||||
1. **생생함을 낮춘다.** 흰 배경 위에서 자신감 있게 읽히던 색이 거의 검정 위에서는 네온으로 읽힌다. 다크 외형에서는 강조색을 1~2스텝 덜 생생하게 조정한다.
|
||||
2. **어두운 끝을 분리한다.** 옅은 배경에서 구분되던 스텝이 어두운 표면에서는 서로 뭉개진다. 어두운 끝에 스텝을 더 둔다.
|
||||
3. **대비 비대칭을 재검사한다.** 대비는 거울상이 아니다 — 라이트에서 통과한 쌍이 반전되면 실패할 수 있다. 두 외형 모두 실제 렌더 배경에 대해 전경을 다시 검사한다.
|
||||
|
||||
**전환 메커니즘은 하나를 고르고 전체에 쓴다.** 메커니즘을 섞는 것이 흔한 실패다 — 일부 토큰은 미디어쿼리로, 다른 토큰은 클래스로 설정하면 사용자가 시스템 선호를 오버라이드하는 순간 반쯤만 테마된 인터페이스가 된다 [SKILL-BETTER-COLORS].
|
||||
|
||||
| 메커니즘 | 맞는 상황 | 주의 |
|
||||
|---|---|---|
|
||||
| `prefers-color-scheme` 단독 | 테마 토글 UI가 없을 때 | persist·hydrate할 상태가 없어야 성립한다 |
|
||||
| `.dark` 클래스 | 사용자가 시스템 설정을 오버라이드할 수 있을 때 | 미디어쿼리는 초깃값만 정하고, 클래스가 최종 결정이다 |
|
||||
| `light-dark()` | `color-scheme`도 함께 선언하는 프로젝트 | 클래스 기반 토글이라도 `color-scheme` 속성을 함께 갱신해야 값이 반영된다. Baseline Newly available(2024년 5월 기준, 아직 Widely 아님)이므로 지원 범위를 확인한다 [WEB-BASELINE] |
|
||||
|
||||
```css
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
--surface: light-dark(#fbfaf8, #14130f);
|
||||
--ink: light-dark(#1a1917, #e8e4dc);
|
||||
}
|
||||
```
|
||||
|
||||
**테마 전환 순간 색·배경·테두리·그림자가 동시에 바뀌어 뭉개지는(smear) 것을 막는 전환 억제 레시피**는 구현 기법이므로 [motion.md](motion.md)를 따른다 — 이 문서에서는 값만 다룬다.
|
||||
|
||||
---
|
||||
|
||||
## §6. 대비
|
||||
|
||||
**하드 게이트: WCAG 2 대비.** 일반 텍스트(24px 미만, bold 18.5px 미만) AA 4.5:1, 큰 텍스트(24px 이상, bold 18.5px 이상) AA 3:1 [WCAG-READ]. UI 컴포넌트와 그래픽 객체는 인접 색과 3:1 — 비활성 컴포넌트, 필수 로고·브랜드 마크, 장식용 그래픽, 텍스트 대체물이 있는 그래픽은 예외다 [WCAG-NONTEXT]. 이 수치는 협상 대상이 아니다.
|
||||
|
||||
**수정 절차: 명도를 먼저 바꾼다.** 대비가 실제로 반응하는 채널은 명도다 — hue와 채도는 측정값을 훨씬 덜 움직이므로, hue를 바꿔 대비를 고치려는 시도는 대체로 헛수고다. hue·채도를 고정한 채 전경을 배경에서 지각적으로 더 멀리 옮기고 재측정한다. hue를 고정하는 이유는 "대비 수정"이 조용히 "팔레트 변경"이 되는 것을 막기 위해서다 [SKILL-BETTER-COLORS]. APCA 기준으로는 지각 명도 약 75% 안팎 배경에서 순수 검정도 Lc 60 안팎에 그친다[SKILL-BETTER-COLORS] — WCAG 2 비율로는 이 배경에서 검정 텍스트가 여유 있게 통과하므로 두 척도를 섞지 않는다. WCAG 2 기준으로 흑·백 어느 전경도 여유가 적은 구간은 중간 회색(CIELAB L* 약 50 부근, 최대 약 4.6:1)이며, 그런 배경에서는 전경보다 배경을 바꾸는 편이 낫다. 명도를 밀면 갬멀을 벗어날 수 있으니 렌더 가능하도록 채도를 함께 낮춘다. **값을 바꾼 뒤에는 항상 재측정한다 — 고쳐졌다고 가정하지 않는다.**
|
||||
|
||||
**APCA는 규범이 아니라 보조 진단이다.** 2026년 9월 확인 기준 WCAG 3 초안 본문은 대비 알고리즘 자체를 아직 정하지 않았고(APCA라는 이름조차 명시되어 있지 않다), APCA 자체 문서도 스스로를 "beta"로 표시하며 미래 표준의 평가 후보일 뿐이라고 밝힌다 [APCA-STATUS]. 그래서 designpaca는 WCAG 2 비율을 대비의 하드 게이트로 유지한다. APCA는 "라이트 모드에서 통과한 쌍이 다크 모드에서 실패하는가" 같은 추가 신호가 필요할 때만 보조로 참고한다. 쓴다면 본문 텍스트는 Lc 75(선호 90), 라벨·헤드라인 같은 비본문은 Lc 60(선호 75)을 시작값으로 둔다 — 실측 조정 [SKILL-BETTER-COLORS].
|
||||
|
||||
**렌더 타임 계산값은 렌더된 결과를 측정한다.** `color-mix()`, 상대 색 문법, `light-dark()`는 모두 렌더 타임에 해석되므로 선언값이 아니라 실제 렌더 결과를 측정해야 한다. `backdrop-filter`가 걸린 표면은 뒤로 스크롤되는 콘텐츠에 따라 실효 색이 바뀌므로, 가장 밝은·가장 어두운 콘텐츠에서 테스트하거나 표면을 충분히 불투명하게 만든다 [SKILL-BETTER-COLORS].
|
||||
|
||||
---
|
||||
|
||||
## §7. 의미와 상태색
|
||||
|
||||
**시맨틱 색을 비파괴 액션에 쓰지 않는다.** 상태색(`danger`, `success` 등)은 그 상태를 실제로 표시하는 곳에만 쓴다. 삭제 같은 파괴적 행동이 아닌 일반 강조에 `danger` 색을 빌려 쓰면 사용자가 실제로 위험한 것으로 오독한다. **프로젝트 계약**: 하나의 색은 하나의 의미만 진다 — hue 15도 이내는 같은 색으로 취급하므로, 강조색이 "인터랙티브"를 뜻하면 그 hue가 정적 텍스트에 쓰이는 순간 클릭 불가능한 것을 클릭하라고 말하는 셈이다 [SKILL-BETTER-COLORS].
|
||||
|
||||
**뷰당 채색된 주요 액션은 하나다.** 채운 색이 주요 강조를 인코딩할 때는 뷰당 주 행동 하나만 색을 채우고 나머지는 중성으로 둔다. 색은 배경에 올리지 라벨에 올리지 않는다 — 채운 버튼은 방 건너편에서도 주요 행동으로 읽히지만, 중성 버튼 위 강조색 텍스트는 링크로 읽힌다. 서로 다른 상태·카테고리를 인코딩하는 경우라면 여러 색 배경도 괜찮다 [SKILL-BETTER-COLORS]. **프로젝트 계약**.
|
||||
|
||||
**색만으로 의미를 전달하지 않는다.** 이 규칙 자체(비색각 사용자·회색조 인쇄를 위한 아이콘·라벨 병기)는 하드 게이트이며 정본은 [accessibility.md](accessibility.md)다.
|
||||
|
||||
**문화적 의미.** 색은 문화마다 다르게 읽힌다. 서구 관행에서 빨강=위험·손실, 초록=성공·이익이라는 연상이 흔히 쓰이지만, 중국어권 금융 UI는 반대로 상승을 빨강, 하락을 초록으로 표시한다 [SKILL-BETTER-COLORS]. 한국 증권 화면의 "빨강=상승" 관행도 이번 조사에서 한국거래소 등의 1차 공식 규정을 찾지 못했다 — 국제 표준이 아니라 시장 관행일 가능성이 높다. 그러므로 손익·상태처럼 문화적으로 뒤집힐 수 있는 색은 하드코딩하지 않고, 데이터 제공자(증권사·거래소 API)와 브리프가 실제로 쓰는 관례를 확인해 로케일별 토큰으로 둔다. 확인하지 못한 관행을 확인된 사실처럼 쓰지 않는다.
|
||||
|
||||
---
|
||||
|
||||
## §8. 역할 확장 목록
|
||||
|
||||
> 언제: 필요할 때만 연다. [tokens.md](tokens.md) §2의 7개 역할이 최소 집합이며 기본값이다. 컴포넌트가 요구할 때마다 무분별하게 역할을 추가하면 팔레트가 맨 처음 만든 화면의 모양으로 굳는다 — 실제로 그 역할을 렌더하는 화면이 생겼을 때만 아래에서 골라 추가한다.
|
||||
|
||||
| 확장 역할 | 용도 | 위계 |
|
||||
|---|---|---|
|
||||
| 비활성 텍스트(disabled) | 조작 불가 상태의 라벨·값 | 프로젝트 계약 — 대비는 충분히 유지하되 활성 상태처럼 읽혀서는 안 된다 |
|
||||
| 반전(inverse) | 어두운/밝은 표면 위에 반대 톤으로 놓이는 텍스트 | 프로젝트 계약 |
|
||||
| on-accent | 강조 배경 위 텍스트·아이콘(`--accent-ink`와 별도 구분이 필요할 때) | 프로젝트 계약 |
|
||||
| sunken | 입력창·웰처럼 표면보다 가라앉아 보이는 배경 | 관찰 후보 |
|
||||
| scrim | 모달·오버레이 뒤에 어둡게 깔리는 층 | 프로젝트 계약 |
|
||||
| 포커스 링 | 키보드 포커스 표시 | 인접 색과 3:1 이상 — 하드 게이트, 정본은 [accessibility.md](accessibility.md) |
|
||||
| separator vs border | 구분선(콘텐츠를 나눔) vs 테두리(컨트롤을 둘러쌈) | 관찰 후보 — 오늘 값이 같아도 역할은 분리해 둔다. 입력창을 재스타일링하는 순간 둘이 갈라진다 [SKILL-BETTER-COLORS] |
|
||||
|
||||
이 목록은 역할 재고이지 강제 목록이 아니다. `tokens.md` §2의 "역할에 토큰이 없으면 토큰을 추가한다"는 원칙을 그대로 따르되, 추가할 이름을 고를 때 여기서 고른다.
|
||||
|
||||
---
|
||||
|
||||
## §9. 넓은 색역 — P3
|
||||
|
||||
> 언제: 고채도 브랜드 색이 화면에서 탁하거나 밋밋해 보일 때만 연다(점진적 향상).
|
||||
|
||||
모든 sRGB 색은 Display P3 색역 안에 있지만 역은 성립하지 않는다. P3는 sRGB보다 약 50% 더 많은 색을 커버하며, 그 차이는 가장 채도 높은 값에서만 의미가 있다 — 최대 생생함의 60% 이하인 색은 양쪽 색역에서 똑같아 보인다 [SKILL-BETTER-COLORS].
|
||||
|
||||
**순서가 중요하다.** sRGB 값을 먼저 선언하고, `@media (color-gamut: p3)` 안에서 더 채도 높은 P3 값으로 오버라이드한다.
|
||||
|
||||
```css
|
||||
:root {
|
||||
--accent: #3b82f6; /* sRGB 먼저 */
|
||||
}
|
||||
@media (color-gamut: p3) {
|
||||
:root {
|
||||
--accent: color(display-p3 0.28 0.52 0.95);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**클리핑은 우아하지 않다.** sRGB로 표시 못 하는 P3 값을 지원 안 하는 디스플레이에 그대로 보내면 이웃 스텝들을 하나의 렌더 색으로 뭉갠다 — 최대 생생함은 hue마다 달라서(시안이 빨강·보라보다 훨씬 낮다) 일부 스텝에서만 클리핑이 일어나 램프가 고르지 않게 무너진다. **프로젝트 계약 — P3 값을 쓰면 sRGB 폴백을 먼저 선언한다.** 폴백 없이는 우아하게 저하되지 않고 램프가 무너진다 [SKILL-BETTER-COLORS].
|
||||
|
||||
---
|
||||
|
||||
## §10. 기존 팔레트 감사 5단계
|
||||
|
||||
> 언제: 리디자인에서 팔레트를 재구조화하기 전에 연다.
|
||||
|
||||
1. **모든 리터럴을 수집한다.** `hex`, `rgb(`, `hsl(`, `oklch(`, 프로젝트 유틸리티 클래스 접두어를 grep한다. 스타일시트뿐 아니라 SVG `fill`/`stroke`, 차트 설정, 이메일 템플릿까지 포함한다 — 색은 스타일시트 밖에도 숨는다.
|
||||
2. **각 hue 계열 안에서 지각된 명도로 정렬한다.** 근접 중복이 이웃으로 즉시 드러난다.
|
||||
3. **근접 중복을 합친다.** 대략 1램프 스텝보다 가까운 두 색은 드리프트한 하나의 색이다 — 가장 많이 쓰인 값을 남기고 나머지는 폐기한다. **절대 평균 내지 않는다.**
|
||||
4. **생존자마다 §8의 역할을 배정한다.** 어떤 역할과도 맞지 않는 색은 누락된 토큰이거나 실수다 — 어느 쪽인지 정해 finding에 적는다.
|
||||
5. **역할당 램프가 1개를 넘는지 센다.** 넘는다면 팔레트가 구조를 벗어난 것이지 제품에 색이 더 필요한 게 아니다.
|
||||
|
||||
[SKILL-BETTER-COLORS]
|
||||
|
||||
**아무것도 바꾸기 전에 인벤토리부터 보고한다.** 팔레트 통합은 아무도 건드리라 하지 않은 화면의 렌더 결과까지 바꾸므로, 사용자가 수락할 때까지는 제안으로 남긴다.
|
||||
|
||||
---
|
||||
|
||||
## §11. 표기법은 프로젝트를 따른다
|
||||
|
||||
**프로젝트가 이미 쓰는 표기법을 재사용한다.** hex를 쓰는 프로젝트가 잘못하고 있는 게 아니다 — 값 하나 고치려고 두 번째 표기법을 더하지 않는다. hex와 `oklch()`가 섞인 것보다 일관된 hex 체계가 낫다 [SKILL-BETTER-COLORS].
|
||||
|
||||
**변환은 다음 세 경우에만 한다.** 사용자가 요청했을 때, 합의된 마이그레이션이 스코프일 때, 프로젝트가 표기법을 표준화하는 중이고 이 값이 낙오자일 때. 이 문서가 열렸다는 이유만으로 변환하지 않는다.
|
||||
|
||||
변환 스코프 안에서도 CSS 키워드(`currentColor`, `inherit`, `transparent`, `initial`, `unset`)는 그대로 두고, 그라디언트 함수는 색 스톱만 바꾸고 보간 방식은 건드리지 않는다. **대량 변환은 정리(cleanup)가 아니라 마이그레이션이다** — 렌더된 모든 색을 반올림 오차만큼 바꾸고 아무도 요청하지 않은 파일까지 건드리므로, 부수효과가 아니라 그 자체가 별도 작업이어야 한다 [SKILL-BETTER-COLORS].
|
||||
|
||||
---
|
||||
|
||||
## §12. 체크리스트
|
||||
|
||||
- [ ] `tokens.md` §2의 역할 토큰이 먼저 채워져 있다. 원시값 계층은 다중 hue·정밀 다크모드가 실제로 필요할 때만 추가했다.
|
||||
- [ ] 라이트·다크 두 외형 모두 실제 렌더 배경에 대해 대비를 측정했다(WCAG 2, 하드 게이트).
|
||||
- [ ] 대비를 고쳤다면 명도를 먼저 바꾸고 hue를 고정했으며, 값을 바꾼 뒤 재측정했다.
|
||||
- [ ] 그라디언트를 썼다면 보간 공간을 의도적으로 골랐고, Newly available 문법에는 결과가 허용 가능한 기본 sRGB 폴백이 있다.
|
||||
- [ ] 다크모드를 반전이 아니라 재조정했다(생생함 낮추기·어두운 끝 분리·대비 비대칭 재검사 3항목).
|
||||
- [ ] 전환 메커니즘을 하나만 골라 전체에 썼다(미디어쿼리·클래스·`light-dark()` 혼용 없음).
|
||||
- [ ] 상태색·강조색이 하나의 색=하나의 의미를 지킨다. 뷰당 채색된 주요 액션이 하나다.
|
||||
- [ ] 색만으로 의미를 전달하는 곳이 없다([accessibility.md](accessibility.md)).
|
||||
- [ ] 손익·상태처럼 문화적으로 뒤집힐 수 있는 색은 데이터 제공자·브리프의 실제 관례를 확인했다(지어내지 않았다).
|
||||
- [ ] P3를 썼다면 sRGB 폴백이 먼저 선언돼 있다.
|
||||
- [ ] 리디자인이면 기존 팔레트를 5단계로 감사해 인벤토리를 먼저 보고했다.
|
||||
- [ ] 표기법은 프로젝트 것을 따랐고, 이 문서를 열었다는 이유만으로 값을 변환하지 않았다.
|
||||
282
packages/skill/references/component-systems.md
Normal file
282
packages/skill/references/component-systems.md
Normal file
|
|
@ -0,0 +1,282 @@
|
|||
# component-systems — 기존 컴포넌트 라이브러리 위에서 일하기
|
||||
|
||||
기존 컴포넌트 라이브러리 위에서 일할 때 4단계 전에 읽는다. `components.json`, `@radix-ui/*`, Base UI 계열 패키지, `cva`·`tailwind-merge` 같은 컴포넌트 기반이 감지되면 여는 조건부 문서다. 감지되지 않으면 이 문서는 읽지 않는다 — designpaca의 기본 파이프라인(SVG 필터·three.js·순수 CSS)은 이 문서 없이도 완결된다.
|
||||
|
||||
## 이 문서를 읽는 법
|
||||
|
||||
| 상황 | 읽을 곳 |
|
||||
|---|---|
|
||||
| 지금 컴포넌트 기반이 있는지부터 확인해야 한다 | §0 |
|
||||
| 화면을 만들기 전에 뭘 먼저 검색해야 하는지 | §1 |
|
||||
| "이건 브랜드 디자인 시스템 작업인가?"가 헷갈린다 | §2 |
|
||||
| 토큰 이름을 라이브러리 변수로 옮겨야 한다 | §3 |
|
||||
| Dialog·Avatar·Group 같은 합성 실수를 피해야 한다 | §4 |
|
||||
| Base UI와 Radix 중 뭘 쓰는 프로젝트인지 API가 다르다 | §5 |
|
||||
| CLI로 컴포넌트를 추가·갱신해야 한다 | §6 |
|
||||
| Tailwind 프로젝트다 | §7(조건부) |
|
||||
| 설정·대시보드 같은 화면 유형의 조합을 참고하고 싶다 | §8(관찰 후보) |
|
||||
| 채팅·스트리밍 UI를 만든다 | §9(조건부) |
|
||||
| 작업 직전 마지막 확인 | §10 |
|
||||
|
||||
---
|
||||
|
||||
## 0. 언제·감지 방법
|
||||
|
||||
**하드 게이트는 없다.** 이 절은 "이 문서를 열어야 하는가"만 판정한다.
|
||||
|
||||
다음 중 하나라도 있으면 컴포넌트 기반이 있는 프로젝트다.
|
||||
|
||||
- 저장소 루트 또는 앱 루트에 `components.json`이 있다(shadcn/ui 계열의 표준 설정 파일)
|
||||
- `package.json` 의존성에 `@radix-ui/*`, Base UI 계열 패키지(`@base-ui/react`, 구 `@base-ui-components/react` 등), `cva`(class-variance-authority), `tailwind-merge`/`clsx` 중 하나 이상이 있다
|
||||
- `src/components/ui/`(또는 동등한 디렉터리)에 `button.tsx`, `dialog.tsx` 같은 생성된 프리미티브 파일이 이미 있다
|
||||
|
||||
`components.json`이 있으면 그 안의 `aliases`, `style`, `iconLibrary`, `tailwind` 필드를 먼저 읽는다. CLI가 있으면(`npx shadcn@latest info` 등) 실행해 프로젝트가 실제로 어떤 `base`(`radix` 또는 `base`)·경로 별칭·아이콘 라이브러리를 쓰는지 확정하고, 이후 모든 결정에서 그 값을 그대로 따른다. 설정을 읽지 못했는데 라이브러리 종류·경로를 추측해서 설치·수정을 진행하지 않는다 — 불확실하면 사용자에게 묻는다([brief-interview.md](brief-interview.md)) [SKILL-SHADCN].
|
||||
|
||||
---
|
||||
|
||||
## 1. 기존 컴포넌트·반복 패턴 우선
|
||||
|
||||
**프로젝트 계약.** 커스텀 마크업(스타일링한 `div`)을 쓰기 전에 같은 역할의 컴포넌트가 이미 있는지 먼저 검색한다. 콜아웃·빈 상태·로딩 placeholder·상태 배지처럼 화면마다 반복되는 UI는 손으로 다시 짜지 않고 기존 컴포넌트를 확장하거나 그대로 쓴다.
|
||||
|
||||
| 대신 손으로 짜지 않는다 | 이미 있는지 검색한다 |
|
||||
|---|---|
|
||||
| 색칠한 `div` + 아이콘 + 텍스트 | Alert/Callout류 컴포넌트 |
|
||||
| `animate-pulse` 커스텀 div | Skeleton류 컴포넌트 |
|
||||
| `rounded-full bg-*` span | Badge류 컴포넌트 |
|
||||
| `<hr>` 또는 `border-t` div | Separator류 컴포넌트 |
|
||||
| 빈 목록을 위한 임의 레이아웃 | Empty state류 컴포넌트 |
|
||||
|
||||
검색 순서는 (1) 프로젝트에 이미 설치된 컴포넌트 디렉터리, (2) 그 라이브러리의 공식 카탈로그, (3) 그래도 없을 때만 새로 합성이다. 새 컴포넌트를 만들기로 했다면 §2의 역할 분담을 먼저 확인한다 [SKILL-SHADCN].
|
||||
|
||||
---
|
||||
|
||||
## 2. 역할 분담 — 컴포넌트 기반은 브랜드 디자인 시스템이 아니다
|
||||
|
||||
**프로젝트 계약.** shadcn/ui, Base UI, Radix 같은 컴포넌트 기반은 "이 프로젝트가 어떻게 보여야 하는가"에 답하지 않는다. 이들은 조립 규칙(합성·상태·접근성 배선)을 제공할 뿐, 브랜드·타입 위계·간격 리듬·모션 언어는 여전히 designpaca가 3~4단계에서 정한 값이다.
|
||||
|
||||
역할을 나누면 다음과 같다.
|
||||
|
||||
- **designpaca가 맡는 것**: 방향 결정(1~2단계), 토큰 값(3단계: 타입 스케일·색·간격·radius·모션), 브랜드 표면(히어로·섹션 구성·카피 목소리), 검증(5단계: 접근성·성능·시각 감사)
|
||||
- **컴포넌트 라이브러리가 맡는 것**: 인터랙션 프리미티브의 조립(Dialog의 포커스 트랩, Select의 키보드 탐색, Tabs의 ARIA 배선), 상태 관리, 합성 규칙
|
||||
|
||||
두 역할이 섞이면 실패한다. 라이브러리의 기본 variant·기본 그림자·기본 radius를 그대로 두고 "이게 이 프로젝트의 디자인"이라고 부르면, `tokens.md`가 정의한 역할 토큰이 무의미해진다. 역으로 라이브러리가 이미 처리하는 포커스 트랩·키보드 탐색을 손으로 재구현하면 §4의 합성 규칙을 깨고 접근성 배선이 두 벌로 충돌한다.
|
||||
|
||||
**컴포넌트 라이브러리를 새로 설치할지 여부는 사용자 결정이다.** 브리프에 없는데 컴포넌트 기반을 새로 도입하지 않는다. 이미 있는 프로젝트에서는 그 기반을 존중하고, 그 위에 designpaca의 토큰·브랜드 표면·검증을 얹는다 [SKILL-SHADCN].
|
||||
|
||||
---
|
||||
|
||||
## 3. 토큰 매핑표 — designpaca 역할 토큰 → 라이브러리 CSS 변수
|
||||
|
||||
**프로젝트 계약.** 라이브러리가 이미 CSS 변수 기반 테마 시스템을 갖추고 있으면(shadcn/ui 계열이 대표적이다) `tokens.md` §2에서 정한 역할 토큰 값을 새 변수 체계로 중복 정의하지 말고, 라이브러리가 읽는 변수에 그대로 대입한다. 변수 이름은 라이브러리마다 다를 수 있으므로 실제 설치된 테마 파일(보통 `globals.css`)에서 확인하고, 아래는 shadcn/ui 계열에서 관찰되는 이름의 매핑 시작값이다.
|
||||
|
||||
| designpaca 역할 토큰(`tokens.md` §2) | 라이브러리 CSS 변수(관찰값) | 비고 |
|
||||
|---|---|---|
|
||||
| `--surface` | `--background` | 페이지 바탕 |
|
||||
| `--surface-raised` | `--card` (또는 `--popover`) | 카드·패널·오버레이 표면 |
|
||||
| `--ink` | `--foreground` | 본문 텍스트 |
|
||||
| `--ink-muted` | `--muted-foreground` | 보조 텍스트. 대비 4.5:1은 여전히 검증한다 |
|
||||
| `--line` | `--border` | 경계선. 폼 입력 테두리는 `--input`으로 분리되어 있을 수 있다 |
|
||||
| `--accent` | `--primary` | 강조 역할의 기본 토큰 |
|
||||
| `--accent-ink` | `--primary-foreground` | 강조 위 글자색 |
|
||||
| (역할 확장) | `--destructive` / `--destructive-foreground` | 위험·삭제 상태. designpaca 쪽 상태색 확장 자리(`tokens.md` §2)에 대응 |
|
||||
| (역할 확장) | `--ring` | 포커스 링. `accessibility.md`의 포커스 하드 게이트와 연결 |
|
||||
| `--radius-*` | `--radius` | 라이브러리는 보통 단일 `--radius`에서 `calc()`로 나머지 반경을 파생시킨다. designpaca가 3종 미만으로 제한한 radius 어휘와 정신이 같다(`tokens.md` §3-b) |
|
||||
|
||||
이 표는 라이브러리 변수명을 designpaca가 강제하는 것이 아니라, 이미 있는 값을 designpaca 쪽 역할 토큰과 대응시켜 이중 정의를 막는 목적이다. 색상 값 자체(라이트/다크 쌍)를 어떻게 정하는지는 `tokens.md` §2와 [color.md](color.md)를 따른다. `--dur-*`·`--ease-*` 모션 토큰은 라이브러리가 별도로 정의하지 않는 한 `tokens.md` §4와 [motion.md](motion.md)의 값을 그대로 쓴다 — 라이브러리 컴포넌트의 트랜지션 duration이 프로젝트 모션 토큰과 다르면 그 불일치를 기록하고 맞출지 결정한다 [SKILL-SHADCN].
|
||||
|
||||
---
|
||||
|
||||
## 4. 합성 규칙
|
||||
|
||||
**하드 게이트 하나, 나머지는 프로젝트 계약.** 컴포넌트 기반을 쓰기로 한 이상 아래는 그 라이브러리의 계약이며, 어기면 스타일이 아니라 동작이 깨진다.
|
||||
|
||||
### 4-1. Dialog·Sheet·Drawer는 접근 가능한 제목이 필수다 — 하드 게이트
|
||||
|
||||
시각적으로 숨기더라도 `sr-only` 텍스트로는 존재해야 한다. 완전 생략은 WCAG 4.1.2(Name, Role, Value) 위반이다 [WCAG-NRV, SKILL-SHADCN]. 상세 판정 기준과 검사 방법은 [accessibility.md](accessibility.md)를 따른다.
|
||||
|
||||
```tsx
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>프로필 수정</DialogTitle>
|
||||
<DialogDescription>정보를 업데이트합니다.</DialogDescription>
|
||||
</DialogHeader>
|
||||
...
|
||||
</DialogContent>
|
||||
```
|
||||
|
||||
시각적으로 제목을 숨겨야 한다면:
|
||||
|
||||
```tsx
|
||||
<DialogTitle className="sr-only">프로필 수정</DialogTitle>
|
||||
```
|
||||
|
||||
### 4-2. Avatar는 Fallback이 필수다 — 프로젝트 계약
|
||||
|
||||
이미지 로드가 실패했을 때의 대체 표시가 없으면 빈 원이 남는다.
|
||||
|
||||
```tsx
|
||||
<Avatar>
|
||||
<AvatarImage src="/avatar.png" alt="사용자" />
|
||||
<AvatarFallback>홍</AvatarFallback>
|
||||
</Avatar>
|
||||
```
|
||||
|
||||
### 4-3. Group 안에 Item — 프로젝트 계약
|
||||
|
||||
리스트형 컴포넌트는 콘텐츠 컨테이너에 Item을 직접 렌더링하지 않고 Group으로 감싼다. 감싸지 않으면 스크린리더가 그룹 경계를 읽지 못하거나 레이아웃이 깨진다.
|
||||
|
||||
| Item | Group |
|
||||
|---|---|
|
||||
| `SelectItem`, `SelectLabel` | `SelectGroup` |
|
||||
| `DropdownMenuItem`, `DropdownMenuLabel` | `DropdownMenuGroup` |
|
||||
| `CommandItem` | `CommandGroup` |
|
||||
| `MenubarItem` | `MenubarGroup` |
|
||||
|
||||
```tsx
|
||||
// 틀림 — Item이 콘텐츠 컨테이너에 직접
|
||||
<SelectContent>
|
||||
<SelectItem value="apple">사과</SelectItem>
|
||||
</SelectContent>
|
||||
|
||||
// 맞음
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="apple">사과</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
```
|
||||
|
||||
[SKILL-SHADCN]
|
||||
|
||||
### 4-4. 폼 필드 묶음 — 프로젝트 계약
|
||||
|
||||
라이브러리가 필드 묶음 컴포넌트(FieldGroup/Field류)를 제공하면 raw `div` + `space-y-*`/`grid gap-*`로 폼 레이아웃을 다시 짜지 않는다. 검증 상태는 컨테이너와 컨트롤에 이중으로 표시한다 — 컨테이너에는 `data-invalid`(또는 동등 속성), 실제 입력 요소에는 `aria-invalid`. 비활성 상태도 같은 패턴(`data-disabled`는 컨테이너, `disabled`는 컨트롤)이다. 스타일링과 스크린리더 판정이 같은 진실을 가리키게 하는 목적이며, 한쪽만 표시하면 시각과 보조기술 판정이 어긋난다 [SKILL-SHADCN].
|
||||
|
||||
### 4-5. 오버레이의 z-index를 손으로 조정하지 않는다 — 프로젝트 계약
|
||||
|
||||
Dialog, Sheet, Popover, DropdownMenu, Tooltip, HoverCard 같은 오버레이 컴포넌트는 자체 스태킹 컨텍스트를 관리한다. `z-50`이나 `z-[999]` 같은 임의값을 얹으면 라이브러리가 관리하는 레이어 순서와 충돌해 다른 오버레이 뒤에 가려지거나 앞으로 튀어나온다. 스태킹 순서를 바꿔야 할 실제 필요가 있으면 라이브러리가 제공하는 portal·z-index prop을 먼저 찾고, 없으면 그 사실을 design.md에 남긴다 [SKILL-SHADCN].
|
||||
|
||||
---
|
||||
|
||||
## 5. Base UI vs Radix — API 차이
|
||||
|
||||
**프로젝트 계약.** 같은 컴포넌트 이름이라도 밑바탕 프리미티브가 Base UI인지 Radix인지에 따라 prop 시그니처가 다르다. `components.json`의 `base` 필드나 `info` 출력에서 어느 쪽인지 먼저 확인하고, 프로젝트가 쓰는 쪽의 문법만 쓴다. 섞어 쓰면 조용히 동작하지 않는 코드가 나온다 [SKILL-SHADCN].
|
||||
|
||||
| 상황 | Radix | Base UI |
|
||||
|---|---|---|
|
||||
| 커스텀 트리거 엘리먼트 | `asChild` | `render` |
|
||||
| non-button 엘리먼트로 트리거를 렌더링할 때 | 해당 없음 | `nativeButton={false}` 추가 필요 |
|
||||
| Select 아이템 정의 | JSX로 인라인 | root에 `items` prop 필요 |
|
||||
| Select placeholder | `<SelectValue placeholder="...">` | `{ value: null }` 아이템으로 표현 |
|
||||
| ToggleGroup 단일 선택 | `type="single"`, `defaultValue`는 문자열 | prop 불필요(기본 단일), `defaultValue`는 항상 배열 |
|
||||
| ToggleGroup 다중 선택 | `type="multiple"` | `multiple` boolean prop |
|
||||
| Slider 단일 thumb | 항상 배열 | 단일 숫자 허용(range는 둘 다 배열) |
|
||||
| Accordion | `type="single"\|"multiple"` 필요, `collapsible` 지원, `defaultValue`는 문자열 | `type` prop 없음, `multiple` boolean, `defaultValue`는 항상 배열 |
|
||||
|
||||
트리거를 감싸는 예시:
|
||||
|
||||
```tsx
|
||||
// 틀림 — 트리거를 추가 엘리먼트로 감쌈
|
||||
<DialogTrigger>
|
||||
<div><Button>열기</Button></div>
|
||||
</DialogTrigger>
|
||||
|
||||
// 맞음 (Radix)
|
||||
<DialogTrigger asChild>
|
||||
<Button>열기</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
// 맞음 (Base UI)
|
||||
<DialogTrigger render={<Button />}>열기</DialogTrigger>
|
||||
```
|
||||
|
||||
non-button 렌더 대상 예시(Base UI):
|
||||
|
||||
```tsx
|
||||
// 틀림 — nativeButton={false} 누락
|
||||
<Button render={<a href="/docs" />}>문서 보기</Button>
|
||||
|
||||
// 맞음
|
||||
<Button render={<a href="/docs" />} nativeButton={false}>
|
||||
문서 보기
|
||||
</Button>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. CLI 안전
|
||||
|
||||
**프로젝트 계약.** 컴포넌트 추가·갱신에 CLI(예: `shadcn` CLI)를 쓸 수 있으면 다음 순서를 지킨다.
|
||||
|
||||
1. **먼저 `info`로 프로젝트 설정을 확인한다.** 별칭 경로(`aliases`), Tailwind 버전, `base`(radix/base), `iconLibrary`를 읽고 그 값을 그대로 따른다. 값을 추측하거나 프로젝트 대신 기본값을 고르지 않는다.
|
||||
2. **`--dry-run`·`--diff`로 먼저 미리 본다.** 실제 적용 전에 영향받는 파일과 upstream 대비 diff를 확인한다. 로컬 변경이 없는 파일은 덮어써도 안전하지만, 로컬 변경이 있는 파일은 diff를 읽고 upstream 갱신을 로컬 수정과 함께 병합한다.
|
||||
3. **`--overwrite`는 사용자의 명시적 승인 없이 쓰지 않는다.** "그냥 다 업데이트해" 같은 포괄 지시가 와도 실행 전에 한 번 더 확인한다.
|
||||
4. **레지스트리를 추측하지 말고 묻는다.** 사용자가 "로그인 블록 추가해줘"처럼 레지스트리를 명시하지 않고 요청하면, 기본 레지스트리를 임의로 고르지 않고 어느 레지스트리를 쓸지 먼저 묻는다.
|
||||
5. **원격 파일을 손으로 fetch하지 않는다.** GitHub raw 등에서 파일을 직접 받아오지 않고 CLI의 레지스트리 해석·경로 처리·CSS diff 기능을 쓴다.
|
||||
6. **추가한 컴포넌트는 반드시 읽고 검증한다.** 빠진 하위 컴포넌트(예: `SelectGroup` 없는 `SelectItem`), 빠진 import, §4 합성 규칙 위반을 확인한 뒤 다음 단계로 넘어간다. 아이콘 import는 프로젝트의 `iconLibrary`로 맞춘다.
|
||||
|
||||
[SKILL-SHADCN]
|
||||
|
||||
---
|
||||
|
||||
## 7. Tailwind 프로젝트 요약
|
||||
|
||||
> 언제: 프로젝트가 Tailwind CSS를 쓸 때만. designpaca는 특정 CSS 프레임워크를 전제하지 않으므로, Tailwind가 아닌 프로젝트에는 이 절을 적용하지 않는다.
|
||||
|
||||
**프로젝트 계약.** 컴포넌트 라이브러리가 Tailwind 위에서 동작하도록 설계돼 있으면, 아래는 그 라이브러리와 충돌하지 않기 위한 클래스 위생 요약이다. 취향 규범이 아니라 라이브러리가 이미 semantic 토큰·유틸리티로 값을 관리하고 있어서 수동으로 덮어쓰면 어긋나는 항목들이다.
|
||||
|
||||
- 컴포넌트 색상·타이포그래피를 `className`으로 직접 오버라이드하지 않는다. 레이아웃(간격·정렬·폭)에만 쓴다
|
||||
- 수직 스택은 `space-y-*` 대신 `flex flex-col gap-*`(또는 `grid gap-*`)를 쓴다
|
||||
- 너비와 높이가 같으면 `w-10 h-10` 대신 `size-10`을 쓴다
|
||||
- 잘린 텍스트는 `overflow-hidden text-ellipsis whitespace-nowrap` 대신 `truncate` 축약을 쓴다
|
||||
- 수동 `dark:` 색상 오버라이드 대신 라이브러리의 semantic 토큰(`bg-background`, `text-muted-foreground` 등)을 쓴다 — §3 매핑표 참고
|
||||
- 조건부 클래스는 수동 템플릿 리터럴 삼항 대신 `cn()`류 유틸리티를 쓴다
|
||||
- 오버레이 컴포넌트에는 §4-5의 이유로 수동 `z-index`를 얹지 않는다
|
||||
- built-in variant(`variant="outline"` 등)를 먼저 시도하고, 그걸로 안 될 때만 `className`으로 보강한다
|
||||
|
||||
레지스트리·CLI 플래그·`registry.json` 스키마 자체는 designpaca 범위 밖이므로 다루지 않는다 [SKILL-SHADCN].
|
||||
|
||||
---
|
||||
|
||||
## 8. 화면 유형별 조합 참고
|
||||
|
||||
> 언제: 컴포넌트 기반 위에서 익숙한 화면 유형(설정, 대시보드 등)을 조립할 때. 아래는 **관찰 후보**이며 정답이 아니다.
|
||||
|
||||
**관찰 후보.** 특정 화면 유형에서 자주 관찰되는 컴포넌트 조합 예시다. 그대로 베끼면 템플릿화된 결과가 나온다는 경고와 함께 둔다.
|
||||
|
||||
| 화면 유형 | 자주 보이는 조합(예시일 뿐) |
|
||||
|---|---|
|
||||
| 설정 페이지 | Tabs 또는 사이드 내비 + Card + Form(FieldGroup/Field) |
|
||||
| 대시보드 | 고정 사이드바 + Card + 차트 컴포넌트 + 데이터 테이블 |
|
||||
| 목록 + 상세 | 목록 패널 + 인스펙터 패널(넓은 화면에서만, `layout.md` §4-f(데스크톱 어포던스) 참고) |
|
||||
|
||||
**주의**:
|
||||
|
||||
1. 이 표를 정답으로 채택하지 않는다. 브리프·업종·목표 행동에 따라 완전히 다른 조합이 맞을 수 있다
|
||||
2. `reference-method.md`의 레퍼런스 우선 원칙이 이 표보다 앞선다 — 실제 레퍼런스 조사 없이 이 표만으로 화면을 확정하지 않는다
|
||||
3. 같은 조합을 모든 설정·대시보드 화면에 기계적으로 반복하면 그 자체가 제네릭 신호다(`antipatterns.md` 참고)
|
||||
|
||||
[SKILL-SHADCN]
|
||||
|
||||
---
|
||||
|
||||
## 9. 스트리밍·채팅 UI는 라이브러리 기본 동작을 재발명하지 않는다
|
||||
|
||||
> 언제: 프로젝트의 컴포넌트 라이브러리가 채팅·스트리밍 UI 프리미티브를 이미 제공할 때.
|
||||
|
||||
**프로젝트 계약.** 스트리밍 채팅 화면에서 자동 스크롤 추적·앵커링·"최신으로 이동" 로직은 흔히 재발명되는 영역이다. 라이브러리가 이 동작을 소유하는 컴포넌트(메시지 스크롤 컨테이너류)를 이미 제공하면, `useStickToBottom`이나 수동 `ResizeObserver` 훅으로 직접 재구현하지 않는다. 합성으로 표현할 수 없는 동작만 라이브러리가 제공하는 훅으로 보강하고, 그것도 없을 때만 직접 구현한다. 메시지 행·표면(bubble)·첨부·시스템 구분선도 마찬가지로 라이브러리 프리미티브가 있으면 그것을 합성하고, flex `div`로 직접 재구성하지 않는다 [SKILL-SHADCN].
|
||||
|
||||
---
|
||||
|
||||
## 10. 체크리스트
|
||||
|
||||
- [ ] `components.json`(또는 동등 설정)과 `info` 명령 출력을 프로젝트에서 실제로 확인했다. 값을 추측하지 않았다
|
||||
- [ ] 새 UI를 손으로 짜기 전에 기존 컴포넌트·반복 패턴을 먼저 검색했다(§1)
|
||||
- [ ] 이 작업이 "라이브러리 조립"과 "designpaca 토큰·브랜드 표면·검증" 중 어디에 속하는지 구분했다(§2)
|
||||
- [ ] 새 CSS 변수를 중복 정의하지 않고 §3 매핑표로 기존 라이브러리 변수에 연결했다
|
||||
- [ ] Dialog·Sheet·Drawer에 접근 가능한 제목이 있다(§4-1, 하드 게이트)
|
||||
- [ ] Avatar·Group/Item·폼 필드 묶음·오버레이 z-index를 §4의 합성 규칙대로 처리했다
|
||||
- [ ] 프로젝트가 Base UI인지 Radix인지 확인하고 그 문법만 썼다(§5)
|
||||
- [ ] CLI로 추가·갱신했다면 `info` → `--dry-run`/`--diff` → 검증 순서를 지켰고, `--overwrite`는 승인 없이 쓰지 않았다(§6)
|
||||
- [ ] Tailwind 프로젝트라면 §7 클래스 위생을 지켰다. 아니라면 이 절은 적용하지 않았다
|
||||
- [ ] §8의 화면 유형 조합을 정답이 아니라 참고로만 썼고, 실제 레퍼런스 조사로 확인했다
|
||||
251
packages/skill/references/critique.md
Normal file
251
packages/skill/references/critique.md
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
# critique — 리뷰 경로와 발견 보고 형식
|
||||
|
||||
**리뷰 경로**(SKILL.md 0단계 경로 표의 0 → **5′** 감사 → 보고)와 **5단계 프리플라이트**의 최종 보고에서 읽는다. 이 문서는 두 경로가 공유하는 하나의 규약이다 — "무엇을 검사하는가"는 [preflight.md](preflight.md)·[audit-gate.md](audit-gate.md)가 정하고, 이 문서는 "발견한 것을 어떤 형식·어떤 심각도·어떤 톤으로 사용자에게 보여주는가"만 정한다. 자동 게이트의 범주별 집계 양식([audit-gate.md](audit-gate.md) 리포트 양식)과는 별개로, **개별 발견 하나하나**를 사람이 읽을 보고서로 옮길 때 이 문서를 따른다.
|
||||
|
||||
diff·PR·커밋 범위처럼 **변경분**을 지목받았다면 스코프 계산과 Introduced/Regression 구분은 [change-review.md](change-review.md)가 맡고, 그 산출물도 이 문서의 finding 형식·심각도·톤으로 보고한다.
|
||||
|
||||
## 이 문서를 읽는 법
|
||||
|
||||
| 상황 | 읽을 곳 |
|
||||
|---|---|
|
||||
| 스크린샷·코드·URL 중 무엇을 받았는지에 따라 어떻게 시작할지 | §1 |
|
||||
| 히어로·폼처럼 표면 종류에 따라 얼마나 깊이 볼지 | §2 |
|
||||
| finding 하나를 어떻게 적을지 | §3 |
|
||||
| 심각도를 어떻게 매길지, 무엇이 자동으로 심각도를 끌어올리는지 | §4 |
|
||||
| 수정안을 어떤 순서로 고를지 | §5 |
|
||||
| 같은 문제가 여러 곳에서 나왔을 때, 또는 문제가 하나도 없을 때 | §6 |
|
||||
| 요청 범위가 너무 넓을 때 | §7 |
|
||||
| 확인을 못 한 항목을 어떻게 표시할지 | §8 |
|
||||
| 시각 증거와 코드 증거를 섞어 단정하지 않는 법 | §9 |
|
||||
| 보고서를 강점으로 마무리하는 법 | §10 |
|
||||
| 톤 | §11 |
|
||||
| 리뷰 중 자동으로 고쳐도 되는 항목 | §12 |
|
||||
| 그대로 쓸 한국어 보고 템플릿 | §13 |
|
||||
|
||||
---
|
||||
|
||||
## 0. 언제, 이 문서가 다루는 범위
|
||||
|
||||
리뷰 경로는 **재구현이 목적이 아니다.** 사용자가 "이 화면 어때?", "이 코드 리뷰해줘", "여기 스크린샷 봐줘"처럼 평가만 요청했을 때 0단계에서 범위·입력을 확인한 뒤 곧장 5′(감사)로 가고, 4단계(구현)를 거치지 않는다. 고쳐 달라는 요청으로 바뀌면 그때 국소·연장 경로로 올린다.
|
||||
|
||||
5단계 프리플라이트에서도 이 문서를 쓴다. [preflight.md](preflight.md) §0~§4-3의 체크가 끝난 뒤, 실패·관찰 항목을 사용자에게 보고할 때 §3의 finding 형식과 §4의 심각도를 쓴다. 두 경로의 차이는 **깊이와 입구**뿐이다. 리뷰 경로는 구현이 없으니 입력(스크린샷·코드·URL)에서 바로 시작하고(§1), 프리플라이트는 자기 구현물을 대상으로 이미 렌더된 상태에서 시작한다.
|
||||
|
||||
이 문서는 designpaca가 내리는 판정의 근거인 **판정 위계**(하드 게이트 / 프로젝트 계약 / 스타일 휴리스틱, [SKILL.md](../SKILL.md))를 그대로 쓴다. 새 심각도 체계를 만들지 않는다 — §4가 하는 일은 그 3층을 리뷰 보고서의 언어로 옮기는 것뿐이다 [SKILL-DESIGN-REVIEW].
|
||||
|
||||
---
|
||||
|
||||
## 1. 입력별 진입
|
||||
|
||||
받은 입력의 종류로 시작 방법이 갈린다. 종류를 섞어 짐작하지 않는다.
|
||||
|
||||
| 입력 | 진입 방법 |
|
||||
|---|---|
|
||||
| **스크린샷·이미지** | 바로 시각 판단으로 들어간다. 렌더가 이미 있으므로 추가 확보가 필요 없다 |
|
||||
| **코드 파일**(컴포넌트·스타일시트) | 파일을 읽되 그 파일만 보지 않는다. 관련 CSS·토큰([tokens.md](tokens.md) 역할 이름)까지 함께 열어 finding의 위치를 `path/to/file:line`으로 인용할 수 있게 한다 |
|
||||
| **URL만** | 텍스트 페처(WebFetch류)는 마크업·CSS는 가져와도 **렌더링은 보지 못한다.** 브라우저 도구가 있으면 390·1440 두 폭에서 직접 캡처한다(도구 목록은 [harness.md](harness.md) §6). 브라우저 도구가 없으면 데스크톱·모바일 스크린샷을 사용자에게 요청한다 |
|
||||
| **diff·PR·커밋 범위** | [change-review.md](change-review.md)로 넘긴다. 스코프 해석과 Introduced/Regression 구분은 거기서 하고, finding 자체는 이 문서 §3~§4를 그대로 쓴다 |
|
||||
|
||||
시각 판단이 걸린 리뷰인데 스크린샷도 렌더도 브라우저 도구도 없으면 **추측하지 않는다.** 코드만 읽고 "아마 이렇게 보일 것"이라고 단정하는 것은 §9의 증거 방향성 위반이다. 질문 수단은 [SKILL.md 인터뷰 게이트](../SKILL.md)의 규칙 3(기계적 판정)을 그대로 따른다 — 구조화 질문 도구가 있으면 그것으로, 없으면 평문으로 스크린샷을 요청하고 턴을 끝낸다.
|
||||
|
||||
---
|
||||
|
||||
## 2. 표면별 깊이
|
||||
|
||||
모든 표면을 같은 깊이로 훑지 않는다. 표면 종류를 먼저 식별하고, 그 종류가 가장 잘 무너지는 축을 더 깊이 본다.
|
||||
|
||||
| 표면 | 더 깊이 볼 것 |
|
||||
|---|---|
|
||||
| 마케팅 히어로 | 모션([motion.md](motion.md))·타이포([typography.md](typography.md))·구도, 반사실 제네릭 검증 |
|
||||
| 폼 | 상태 완결성(입력 검증·오류·disabled)·접근성([accessibility.md](accessibility.md)) |
|
||||
| 대시보드·관리 화면 | 밀도·데이터 정합·표(숫자 정렬, 관계 보존) |
|
||||
| 내비게이션 | 도달성(키보드·터치)·현재 위치 신호·좁은 화면 축약 |
|
||||
|
||||
이것은 **다른 축을 생략해도 된다는 뜻이 아니다.** 하드 게이트는 표면 종류와 무관하게 전부 돈다([SKILL.md](../SKILL.md) 규범·기능 하드 게이트 표). 깊이 배분은 스타일 휴리스틱·프로젝트 계약 층의 시간 배분 문제다.
|
||||
|
||||
---
|
||||
|
||||
## 3. finding 형식 — What·Why·Fix
|
||||
|
||||
발견 하나마다 세 요소를 강제한다. 셋 중 하나라도 없으면 finding이 아니라 인상이다.
|
||||
|
||||
- **무엇(What)**: 요소를 인용하거나 `path/to/file:line`을 인용한다. 스크린샷만 있으면 화면 영역과 컴포넌트 이름으로 특정한다.
|
||||
- **왜(Why)**: 한 줄로 위계 3층 중 어디에 해당하는지 밝히고, 규범·근거가 있으면 출처 ID를 붙인다. "이상해 보여서"는 이유가 아니다.
|
||||
- **고침(Fix)**: 정확한 값이나 선택자를 준다. "늘려라", "더 낫게" 같은 방향만 있는 지시는 finding이 아니다 [SKILL-DESIGN-REVIEW].
|
||||
|
||||
표로 묶어 보고한다. 컬럼은 심각도·위치·무엇·왜·고침이다.
|
||||
|
||||
```markdown
|
||||
| 심각도 | 위치 | 무엇 | 왜 | 고침 |
|
||||
|---|---|---|---|---|
|
||||
| Blocking | `Button.tsx:42` | 아이콘 전용 버튼에 접근성 이름 없음 | 하드 게이트 — 스크린리더가 컨트롤 목적을 못 읽음 [WCAG-NRV] | `aria-label="검색"` 추가 |
|
||||
| Important | `Card.module.css:18` | radius가 4px·8px·12px 세 종류 혼재 | 프로젝트 계약 — `design.md`가 `--radius-md`(8px) 단일 체계로 확정 | `--radius-md`로 통일 |
|
||||
| Polish | 히어로 이미지 좌측 | 섹션 간 여백이 위 40px(`--space-5`)·아래 64px(`--space-6`)로 비대칭 | 스타일 휴리스틱 — 리듬 관찰 후보, 브리프 근거가 없으면 대칭이 시작값 [SKILL-BETTER-LAYOUT] | 상하 모두 `--space-6`(64px)으로 통일 검토 |
|
||||
```
|
||||
|
||||
**UI 코드 자체의 수정 전/후를 비교하는 리뷰**(모션 커브 교체, 트랜지션 재조준 등)에는 위 표 대신 `Before | After | Why` 3열 표를 쓴다. 목록형으로 "Before:"와 "After:"를 줄바꿈해 나열하지 않는다 — 나란히 비교해야 차이가 보인다 [SKILL-EMIL-DESIGN-ENG].
|
||||
|
||||
```markdown
|
||||
| Before | After | Why |
|
||||
|---|---|---|
|
||||
| `transition: transform 300ms ease-in` | `transition: transform 300ms var(--ease-out)` | 진입 모션에 `ease-in`을 쓰면 시작이 굼떠 보인다. `--ease-in`은 퇴장 전용이다 |
|
||||
```
|
||||
|
||||
두 표는 병행 가능하다 — 구조 finding은 첫 표, 코드 수정 대안은 두 번째 표로 보조한다.
|
||||
|
||||
---
|
||||
|
||||
## 4. 심각도
|
||||
|
||||
판정 위계를 그대로 3단계 언어로 옮긴다. 새 규칙을 만드는 게 아니라 이미 있는 위계에 이름을 붙이는 것이다.
|
||||
|
||||
| 심각도 | 판정 위계 | 기준 |
|
||||
|---|---|---|
|
||||
| **Blocking** | 하드 게이트 위반 | WCAG·키보드·포커스·대비·감소 모션·진실성처럼 규범·기능에 근거한 항목의 실패. 출시를 막는다 |
|
||||
| **Important** | 프로젝트 계약 위반 또는 과업 방해 | 브리프·`design.md`·토큰·성능 예산과 어긋나거나, 계약 위반이 아니어도 사용자가 과업을 완수하지 못하게 막음 |
|
||||
| **Polish** | 스타일 휴리스틱 관찰(관찰 후보) | 색·radius·정렬·리듬 같은 취향 후보의 다듬기. 통과·보류·채택은 맥락과 렌더로 판단한다 |
|
||||
|
||||
**전부 나열하지 않는다.** 중요한 순으로 소수만 앞세운다. 몇 개까지인지 숫자로 정하지 않는다 — §7의 스코프 규율을 따른다.
|
||||
|
||||
### 에스컬레이션 트리거 — 확인되면 표면이 사소해도 낮추지 않는다
|
||||
|
||||
아래 항목은 확인되는 즉시 표에 적힌 위계로 매긴다(대부분 Blocking). "화면 전체는 괜찮으니 평균 내면 Important"처럼 다른 항목과 평균을 내어 낮추지 않는다 — 트리거는 심각도만 정할 뿐 새 하드 게이트를 만들지는 않는다. 실제 하드 게이트 정의는 [SKILL.md](../SKILL.md)·[accessibility.md](accessibility.md)가 갖고 있고, 이 표는 그 정의를 리뷰 시점에 놓치지 않기 위한 점검표다 [SKILL-BETTER-INTERFACE].
|
||||
|
||||
| 트리거 | 위계 근거 | 이미 있는 designpaca 규칙 |
|
||||
|---|---|---|
|
||||
| 인터랙티브 컨트롤에 접근성 이름이 없음 | 하드 게이트 — 스크린리더가 목적을 읽지 못함 [WCAG-NRV] | [accessibility.md](accessibility.md)의 접근 가능한 이름 규칙 |
|
||||
| 키보드로 도달은 되지만 포커스가 다른 요소에 완전히 가려짐 | 하드 게이트 — 포커스가 있어도 안 보이면 무의미 [WCAG-FOCUS-OBSCURED] | [preflight.md](preflight.md) §1 포커스 표시 항목의 보강 사례 |
|
||||
| 포커스 인디케이터가 인접 색과 구별되지 않음(대비 3:1 미만 등) | 하드 게이트 — 상태를 식별하는 시각 정보라 비텍스트 대비 기준을 받는다 [WCAG-NONTEXT]. 인디케이터의 면적(2px 둘레 기준)까지 요구하는 것은 AAA 이므로 프로젝트가 표방할 때만 계약으로 본다 [WCAG-FOCUS-APPEAR] | [preflight.md](preflight.md) §1 포커스 표시 |
|
||||
| 포인터로만 조작되는 컨트롤(키보드 경로 없음) | 하드 게이트 | [SKILL.md](../SKILL.md) 규범·기능 하드 게이트 1번 |
|
||||
| `prefers-reduced-motion`을 무시하고 위치 이동·시차가 그대로 재생 | 하드 게이트 | [motion.md](motion.md), [SKILL.md](../SKILL.md) 하드 게이트 3번 |
|
||||
| 320px 폭 또는 200% 확대에서 콘텐츠·컨트롤이 잘리거나 도달 불가 | 하드 게이트 | [preflight.md](preflight.md) §3 반응형 |
|
||||
| 텍스트·UI 대비가 적용 WCAG 기준 미달 | 하드 게이트 | [preflight.md](preflight.md) §1 대비 |
|
||||
| 색만으로 상태·의미를 전달하고 다른 단서(아이콘·라벨·패턴)가 없음 | 하드 게이트 — 색맹·흑백 인쇄·저채도 화면에서 정보 유실 | [color.md](color.md) |
|
||||
| 파괴적 행동에 확인·실행취소·구분된 처리가 없음 | 하드 게이트 | [preflight.md](preflight.md) §4-1 |
|
||||
| 잘린(ellipsis 처리된) 콘텐츠에 전체 값으로 가는 경로가 없음 | 하드 게이트 — 정보 접근 수단 자체가 없음 | [SKILL.md](../SKILL.md) 규범·기능 하드 게이트 4번, [typography.md](typography.md) |
|
||||
| 스크롤 경계나 접힘 뒤에 시각적 단서 없이만 도달하는 컨트롤 | 관찰 후보(다듬기) — 다만 그 컨트롤이 필수 과업의 유일한 경로면 과업 방해로 중요(Important) | [layout.md](layout.md) 점진적 공개 절과 같은 층 |
|
||||
| 입력 오류에서 고칠 방법이 알려져 있는데 제시하지 않음 | 하드 게이트(WCAG 3.3.3) | [WCAG-22] |
|
||||
| 그 밖의 오류 문구에 다음 행동이 없음 | 프로젝트 계약 | [product-copy.md](product-copy.md) §6 |
|
||||
| 시맨틱 색 토큰을 의미와 반대로 사용(예: 위험색을 기본 CTA에) | 프로젝트 계약 위반 — 토큰의 의미 계약을 깼다. 동시에 그 오용이 사용자에게 잘못된 위험 신호로 읽히면 하드 게이트로 올린다 | [color.md](color.md), [tokens.md](tokens.md) |
|
||||
| 상태 변화가 모션으로만 전달되고, 모션이 꺼지거나 생략돼도 남는 색·아이콘·라벨이 없음 | 하드 게이트 — 감소 모션 환경에서 상태 자체가 사라짐 | [motion.md](motion.md) |
|
||||
|
||||
이 표에 없는 항목이라도 같은 논리(정보·조작 수단 자체의 유무)가 성립하면 같은 방식으로 Blocking을 매기고 근거를 적는다. 표는 닫힌 목록이 아니다.
|
||||
|
||||
---
|
||||
|
||||
## 5. 저비용 수정 사다리
|
||||
|
||||
Fix를 쓸 때 다섯 단계를 순서대로 검토한다. 더 비싼 단계로 고쳤는데 더 싼 단계가 가능했다면, **그 수정 자체가 별개의 finding이다** [SKILL-BETTER-INTERFACE].
|
||||
|
||||
1. **삭제** — 불필요한 구분선, 과도한 인터랙션 위 애니메이션, 아무도 쓰지 않는 토큰·ARIA 속성은 지우는 것이 가장 싼 수정이다
|
||||
2. **플랫폼 활용** — 커스텀 컴포넌트 대신 네이티브 엘리먼트·포커스 링·컨트롤로 대체할 수 있는가
|
||||
3. **재사용** — 새 값을 만들기 전에 기존 토큰·간격·모션 커브가 이미 있는가
|
||||
4. **값 교정** — 소유 문서(색은 [color.md](color.md), 그림자는 [elevation.md](elevation.md) 등)가 주는 정확한 값으로 바꾼다
|
||||
5. **추가** — 위 넷으로 안 될 때만 새 토큰·wrapper·미디어쿼리·ARIA 속성을 만든다
|
||||
|
||||
---
|
||||
|
||||
## 6. 병합과 패딩 금지
|
||||
|
||||
같은 근본 원인에서 나온 반복 위반은 **하나의 finding**으로 묶고, 확인된 모든 위치를 그 행에 나열한다. 발생마다 행을 나누지 않는다 [SKILL-BETTER-INTERFACE].
|
||||
|
||||
항목 수를 채우려고 같은 문제를 쪼개 보고하지 않는다. 리뷰가 짧아도 되고, **"발견 없음"도 유효한 결과다.** 억지로 Polish를 만들어내는 것은 신뢰를 깎는다.
|
||||
|
||||
---
|
||||
|
||||
## 7. 스코프 규율
|
||||
|
||||
요청 범위가 신뢰성 있게 점검하기에 너무 넓으면, 요청이 중심으로 삼는 **한 흐름**(사용자가 실제로 거치는 진입 경로 하나)으로 좁히고 그 경계를 보고 서두에 명시한다. 어디까지 봤고 무엇을 뺐는지 말하지 않고 넘어가지 않는다.
|
||||
|
||||
**점검하지 않은 표면을 점검한 것처럼 암시하지 않는다.** "전체적으로 괜찮다"는 문장은 실제로 전체를 본 뒤에만 쓴다. 숫자 상한(finding 몇 개까지, 화면 몇 개까지)은 두지 않는다 — 상한은 근거 없는 수치이고, 스코프를 좁히는 판단이 상한보다 먼저다.
|
||||
|
||||
---
|
||||
|
||||
## 8. 미검증·미점검
|
||||
|
||||
실행할 수 없는 체크는 **finding이 아니다.** 두 상태를 구분해 따로 나열한다.
|
||||
|
||||
- **미검증**: 시도했지만 도구·권한·환경 제약으로 확인하지 못했다(예: 실기기가 없어 제스처 QA 불가). 통과로 추정하지 않는다.
|
||||
- **미점검**: §7에서 스코프를 좁히며 의도적으로 뺀 표면이다. "확인 안 함"이라고 명시한다.
|
||||
|
||||
둘 다 finding 개수·심각도 집계에 넣지 않는다. 통과한 체크와 나란히, 별도 목록으로 둔다.
|
||||
|
||||
---
|
||||
|
||||
## 9. 증거 방향성
|
||||
|
||||
런타임 동작이 결과를 좌우할 때는 방향을 섞지 않는다.
|
||||
|
||||
- **시각(스크린샷)만으로 코드-레벨 finding을 단정하지 않는다.** "이 여백은 padding이 아니라 margin일 것"처럼 소스를 안 보고 구현 방식을 추측하지 않는다.
|
||||
- **소스코드만으로 시각적 finding을 단정하지 않는다.** CSS가 의도한 값이어도 폰트 로딩·동적 콘텐츠·다른 규칙과의 캐스케이드로 실제 렌더가 다를 수 있다.
|
||||
|
||||
한쪽 증거만 있으면 그 증거가 답할 수 있는 범위까지만 쓰고, 나머지는 §8의 미검증으로 남긴다.
|
||||
|
||||
---
|
||||
|
||||
## 10. 마무리 — 강점과 최고 레버리지 변경
|
||||
|
||||
지적으로만 끝내지 않는다. 보고서 끝에 **Strengths 2~4개**를 적는다 — 잘 되고 있는 부분 위에 비판이 쌓이게 하는 문화적 장치다 [SKILL-DESIGN-REVIEW]. 지어낸 칭찬이 아니라 실제로 확인한 것만 적는다.
|
||||
|
||||
유용하면 **가장 큰 효과를 낼 변경 하나**를 선택으로 덧붙인다. 여러 Important 중 하나를 고치면 나머지가 함께 해결되거나, 사용자 인지에 가장 크게 걸리는 항목이 후보다.
|
||||
|
||||
---
|
||||
|
||||
## 11. 톤
|
||||
|
||||
모호한 지적을 하지 않는다. "더 나아 보이게", "좀 깔끔하게" 같은 방향만 있는 문장은 쓰지 않는다. 항상 §3의 정확한 Fix와 짝짓는다.
|
||||
|
||||
동료의 작업을 리뷰하듯 직접적이고 정중하게 쓴다. 문제는 정확하고 모호하지 않게 말하되, 사람이 아니라 결과물을 평가한다.
|
||||
|
||||
---
|
||||
|
||||
## 12. 리뷰 중 자동 적용 범위
|
||||
|
||||
사용자가 리뷰와 함께 수정 적용을 요청했을 때(`--apply`류 의도), 안전하게 자동 적용 가능한 범주와 확인이 필요한 범주를 구분한다.
|
||||
|
||||
| 자동 적용 가능 | 확인 필요 |
|
||||
|---|---|
|
||||
| 대비 토큰 교체(이미 있는 역할 토큰으로) | 주관적 판단이 들어가는 변경(색감, 레이아웃 재배치) |
|
||||
| 간격 토큰 교체 | 구조 변경(컴포넌트 분리·합치기, 정보 구조 재배열) |
|
||||
| 포커스 스타일 보강 | 카피 톤·메시지 변경 |
|
||||
| `prefers-reduced-motion` 대응 추가 | 새 컴포넌트·새 패턴 도입 |
|
||||
| 시맨틱 태그로 교체(`div` → `button` 등) | — |
|
||||
| `alt` 텍스트 추가 | — |
|
||||
|
||||
자동 적용 가능 범주도 **수정 후 대비·값을 재검증**한다. 고쳤다고 선언만 하고 재측정을 생략하지 않는다 [SKILL-DESIGN-REVIEW].
|
||||
|
||||
---
|
||||
|
||||
## 13. 한국어 보고 템플릿
|
||||
|
||||
```markdown
|
||||
## 리뷰 범위
|
||||
|
||||
- 대상: <URL/파일/스크린샷>
|
||||
- 본 흐름: <한 흐름으로 좁혔다면 그 경로>
|
||||
- 뺀 것: <미점검으로 둔 표면과 이유>
|
||||
- 증거: <스크린샷/실제 렌더/소스코드, 뷰포트·엔진명>
|
||||
|
||||
## 발견
|
||||
|
||||
| 심각도 | 위치 | 무엇 | 왜 | 고침 |
|
||||
|---|---|---|---|---|
|
||||
| ... | ... | ... | ... | ... |
|
||||
|
||||
발견 없음이면: "이 흐름에서 Blocking·Important 발견 없음. Polish 관찰 후보는 <있으면 나열, 없으면 없음>."
|
||||
|
||||
## 미검증 · 미점검
|
||||
|
||||
- 미검증: <시도했으나 확인 못 한 항목과 이유>
|
||||
- 미점검: <스코프에서 뺀 표면>
|
||||
|
||||
## 잘된 점
|
||||
|
||||
- <강점 1>
|
||||
- <강점 2>
|
||||
|
||||
## 가장 큰 효과를 낼 변경 (선택)
|
||||
|
||||
<하나, 이유 한 줄>
|
||||
```
|
||||
|
||||
이 템플릿은 뼈대다. 리뷰 규모가 작으면 섹션을 생략할 수 있지만(예: 발견이 없으면 "미검증" 섹션도 빈 채로 명시), 거짓으로 채우지는 않는다.
|
||||
|
|
@ -16,6 +16,10 @@
|
|||
| 단서와 선택 설계 | 행동 단서는 지각 가능해야 하고, 선택 비용은 선택지 수만 아니라 구분·라벨·목표에 좌우된다 [NORMAN-SIGN, HICK, KRUG] | 내비·필터·설정 | 7±2로 메뉴 수를 제한한다 | 사용자가 목표 항목을 찾는 시간·오류를 본다 | 9개 항목을 사용자 과업별 3개 묶음으로 재분류 |
|
||||
| 기억보다 인지 | 화면의 상태·다음 행동·되돌림을 드러내면 기억 부담을 낮춘다 [NNG-HEUR, MILLER] | 다단계·파괴적 행동 | 모든 것을 항상 보여야 한다 | 뒤로가기·오류·재진입에서 상태가 복구되는지 시험한다 | 업로드 후 파일명·교체·삭제를 같은 표면에 노출 |
|
||||
| 인지 부하 | 과업에 필요 없는 기억·변환·탐색 단계를 줄이면 문제 해결 부담을 낮출 수 있다 [COGNITIVE-LOAD] | 낯선 절차·설정·비교 | 모든 정보를 숨기거나 단계를 줄이면 더 쉽다 | 첫 사용자 과업의 오류·되돌림·완료 시간을 비교한다 | 설정 값을 군집화하고 선택 결과를 바로 옆에 보인다 |
|
||||
| 간결함 ≠ 미니멀리즘 | 목적을 드러내는 것이 목표이지 요소를 지우는 것 자체가 목표가 아니다. 모든 것을 한 곳에 파묻으면 미니멀해 보여도 단순하지 않다 [SKILL-APPLE-DESIGN] | 정보 밀도를 낮추는 리디자인 전반 | 요소 수를 줄이면 항상 더 쉬워진다 | 핵심 과업에 필요한 정보·컨트롤이 실제로 남아 위계로 정리됐는지 확인한다 | 옵션을 숨기지 않고 흔한 경로를 먼저, 고급 옵션은 한 단계 더 깊이 배치 |
|
||||
| 구체적 라벨 | 직접적이고 구체적인 라벨이 안전하고 일반적인 라벨보다 예측 가능하다 — 내비는 내용물로 짓고("진행 상황", "보관함") "홈" 같은 우산 용어를 피한다 [SKILL-APPLE-DESIGN] | 내비·탭·섹션 이름 짓기 | 라벨은 길고 설명적일수록 항상 낫다 | 사용자가 라벨만 보고 도착 화면을 예측할 수 있는지 시험한다 | "홈" 대신 그 화면이 실제로 보여주는 것으로 탭 이름을 정함 |
|
||||
|
||||
구체적 라벨 규칙을 내비 구조에 적용하는 방법은 [layout.md](layout.md)를 참조한다.
|
||||
|
||||
## 2. 접근성은 시각 스타일과 독립된 통과선이다
|
||||
|
||||
|
|
@ -65,3 +69,17 @@ Core Web Vitals 적합성은 실제 사용자의 field data에서 모바일·데
|
|||
- 구체 검증: 390px에서 실제 클릭 영역과 인접 대상 간격을 측정한다.
|
||||
- 예시 결정: 삭제 아이콘의 보이는 glyph는 16px, 클릭 영역은 28px로 둔다.
|
||||
```
|
||||
|
||||
## 7. 조건부: 개인화와 AI 기능의 책임
|
||||
|
||||
### 개인화 — 조건부
|
||||
|
||||
언제: 웹앱 UI(대시보드·설정 화면처럼 반복해서 쓰는 도구)일 때만 연다. 랜딩 페이지·포트폴리오·마케팅 사이트에는 대체로 해당하지 않는다.
|
||||
|
||||
**관찰 후보**: 단일 레이아웃이 모든 사용자에게 맞지 않을 때는 컨트롤 재배치·안 쓰는 기능 숨기기 같은 개인화 여지를 설계 후보로 검토한다[SKILL-APPLE-DESIGN]. 개인화 자체를 기본값으로 강제하지 않는다 — 브리프나 사용자 조사가 요구할 때만 연다.
|
||||
|
||||
### AI 기능이 있는 브리프의 책임
|
||||
|
||||
언제: 브리프에 AI 기반 기능(추천·생성·자동완성·챗봇 등)이 있을 때 연다.
|
||||
|
||||
**프로젝트 계약**: 오남용과 피해를 미리 예측한다 — 예를 들어 알레르기를 인지하는 레시피 기능은 유해한 재료를 제안하면 안 된다. 적절한 자리에 미리보기(실행 전에 결과를 확인하게 하기)·확인 단계·면책 조항을 두고, 위험이 가치를 넘는 기능은 잘라낸다[SKILL-APPLE-DESIGN]. AI 산출물을 실제 데이터처럼 보여줄 때의 진실 계약은 [trustworthy-showcases.md](trustworthy-showcases.md) §7("AI는 답이 아니라 검토 가능한 기록이다")을 따른다.
|
||||
|
|
|
|||
177
packages/skill/references/elevation.md
Normal file
177
packages/skill/references/elevation.md
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
# elevation — 그림자·깊이·머티리얼
|
||||
|
||||
3단계에서 토큰을 정할 때, 4-2 재질에서 읽는다.
|
||||
|
||||
`tokens.md` §3-b(형태)는 radius·line-width만 정의하고 그림자 토큰 자리가 비어 있다. 이 문서가 그 자리를 채운다. `--dur-*`·`--ease-*`는 여기서 새로 만들지 않고 `tokens.md` §4에 정의된 값을 그대로 재사용한다.
|
||||
|
||||
## 이 문서를 읽는 법
|
||||
|
||||
| 상황 | 읽을 곳 |
|
||||
|---|---|
|
||||
| 그림자 토큰 3단만 빨리 정하면 된다 | §2 |
|
||||
| radius와 그림자를 같이 정의해야 한다 | §3 |
|
||||
| 테두리 대신 그림자를 쓸지 고민 중이다 | §4 |
|
||||
| 다크 모드에서 그림자가 안 보인다 | §5 |
|
||||
| 카드·리스트가 슬롭처럼 보인다 | §6 |
|
||||
| 브리프가 유리·Apple풍 질감을 요구한다 | §7 |
|
||||
| `prefers-reduced-transparency`·`forced-colors` 대응이 필요하다 | §8 |
|
||||
| 커밋 전 확인 목록이 필요하다 | §9 |
|
||||
|
||||
---
|
||||
|
||||
## 0. 언제, tokens.md와의 관계
|
||||
|
||||
그림자·깊이 체계는 3단계에서 토큰과 함께 정하고, 4단계 구현에서 그대로 꺼내 쓴다. `tokens.md`는 타입·색·간격·형태·모션 토큰의 정본이고, 이 문서는 그중 형태(§3-b) 옆에 있어야 할 그림자·머티리얼 절만 담당한다. 토큰 이름은 `tokens.md`가 색에서 쓰는 역할 기반 명명 관례(`--surface`, `--surface-raised`)를 그대로 따라, "얼마나 떠 있는가"라는 역할로 짓는다. 팔레트식 이름(`--shadow-1`, `--shadow-2`)은 쓰지 않는다.
|
||||
|
||||
## 1. 원칙
|
||||
|
||||
아래 원칙은 **관찰 후보**다.
|
||||
|
||||
1. **광원은 하나로 통일한다.** 모든 그림자는 화면 위쪽에서 아래로 비추는 하나의 광원을 가정한다. `offset-y`가 음수인 그림자, 여러 방향에서 비추는 그림자를 같은 화면에 섞으면 표면이 물리적으로 말이 안 되게 읽힌다.
|
||||
2. **깊이는 위계 정보다.** 그림자 강도는 장식이 아니라 "이 표면이 다른 표면보다 얼마나 위에 있는가"를 전달하는 신호다. 강도를 정보 없이 임의로 섞으면 신호가 사라진다.
|
||||
3. **그림자 체계는 하나다.** 한 프로젝트 안에서 정의한 다층 그림자 토큰과 프레임워크 기본 그림자(`shadow-lg` 등)를 섞어 쓰지 않는다. 코너 반경·그림자 단계·아이콘 스트로크 굵기가 화면 전체에서 같은 체계로 파생됐는지는 감사 항목이다 [SKILL-DESIGN-REVIEW][SKILL-BEAUTIFUL-SHADOWS].
|
||||
|
||||
## 2. 다층 그림자 시작값 3단
|
||||
|
||||
아래 값은 **관찰 후보**다. 브리프·기존 토큰이 침묵할 때의 시작값이며, 렌더로 확인하고 조정한다. Tailwind 임의값 문법이 아니라 순수 CSS 커스텀 프로퍼티로 옮긴다 — designpaca는 프레임워크를 전제하지 않는다.
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* 컴팩트 카드, 폼 컨트롤, 필(pill) — 조용한 표면 */
|
||||
--shadow-resting:
|
||||
0px 2px 3px -1px rgba(0, 0, 0, 0.1),
|
||||
0px 1px 0px 0px rgba(25, 28, 33, 0.02),
|
||||
0px 0px 0px 1px rgba(25, 28, 33, 0.08);
|
||||
|
||||
/* 카드, 패널, 팝오버 — 기본 elevated 표면 */
|
||||
--shadow-raised:
|
||||
0px 0px 0px 1px rgba(0, 0, 0, 0.06),
|
||||
0px 1px 1px -0.5px rgba(0, 0, 0, 0.06),
|
||||
0px 3px 3px -1.5px rgba(0, 0, 0, 0.06),
|
||||
0px 6px 6px -3px rgba(0, 0, 0, 0.06),
|
||||
0px 12px 12px -6px rgba(0, 0, 0, 0.06),
|
||||
0px 24px 24px -12px rgba(0, 0, 0, 0.06);
|
||||
|
||||
/* 히어로 미디어, feature callout, 모달류 — 가장 강한 lift */
|
||||
--shadow-overlay:
|
||||
0 2.8px 2.2px rgba(0, 0, 0, 0.034),
|
||||
0 6.7px 5.3px rgba(0, 0, 0, 0.048),
|
||||
0 12.5px 10px rgba(0, 0, 0, 0.06),
|
||||
0 22.3px 17.9px rgba(0, 0, 0, 0.072),
|
||||
0 41.8px 33.4px rgba(0, 0, 0, 0.086),
|
||||
0 100px 80px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
```
|
||||
|
||||
값과 레이어 구조의 출처: [SKILL-BEAUTIFUL-SHADOWS]. `--shadow-raised`는 링 1겹 뒤로 offset·blur·spread가 1→3px 다음부터 2배씩(3·6·12·24px) 커지는 레이어이고, `--shadow-overlay`는 레이어가 멀어질수록(더 흐려질수록) 알파가 0.034→0.12로 비선형으로 진해진다. **레이어가 늘수록 왜 값이 커지고 진해지는지에 대한 근거는 관찰 후보이며 원 출처가 확인되지 않았다** — 자연광 산란을 흉내 낸 설계로 보이지만 이 문장 자체를 근거로 인용하지 않는다.
|
||||
|
||||
### 밀도 → 단계 매핑
|
||||
|
||||
| 표면 | 토큰 | 밀도 |
|
||||
|---|---|---|
|
||||
| 폼 컨트롤, 배지, 필, 컴팩트 카드 | `--shadow-resting` | 조용함 |
|
||||
| 카드, 패널, 팝오버, 드롭다운 | `--shadow-raised` | 기본 |
|
||||
| 히어로 미디어, feature callout, 모달, 시트 | `--shadow-overlay` | 강함 |
|
||||
|
||||
인터랙션이 명확히 elevation을 바꾸는 경우(호버로 카드가 뜨는 등)가 아니면, 같은 컴포넌트의 한 상태에는 그림자 토큰을 하나만 쓴다 [SKILL-BEAUTIFUL-SHADOWS]. elevation이 상태에 따라 바뀔 때는 새 duration 토큰을 만들지 않고 `tokens.md`의 `--dur-instant`·`--ease-out`을 그대로 재사용해 전환한다.
|
||||
|
||||
## 3. radius·표면과 짝짓기
|
||||
|
||||
그림자 프리셋은 깨끗한 표면 채움(fill)과 일관된 radius를 항상 함께 정의한다 [SKILL-BEAUTIFUL-SHADOWS]. 같은 컴포넌트에서 그림자만 바꾸고 radius·표면색을 따로 관리하면 값이 어긋난다.
|
||||
|
||||
중첩된 표면의 radius는 동심원 관계로 계산하는 것이 시작 규칙이다.
|
||||
|
||||
```
|
||||
외곽 radius = 내부 radius + 둘 사이 padding
|
||||
```
|
||||
|
||||
패딩이 24px를 넘으면 동심 계산을 포기하고 두 표면을 독립된 radius로 다룬다 [SKILL-BETTER-UI]. radius 토큰 자체("서로 다른 radius 값은 3종 미만으로")는 [tokens.md](tokens.md) §3-b가 정본이다.
|
||||
|
||||
## 4. 테두리를 대신하는 그림자
|
||||
|
||||
**언제**: 카드·버튼·컨테이너의 깊이·elevation을 표현하는 테두리를 옅은 `box-shadow`로 대체할 때 쓴다. 구분선(`border-top`/`border-bottom`), 셀 경계, `selected`·`focus` 상태를 나타내는 테두리에는 적용하지 않는다 — 그 용도는 레이아웃 분리이지 깊이가 아니다 [SKILL-BETTER-UI].
|
||||
|
||||
그림자는 투명도를 쓰므로 색이 고정된 border와 달리 어떤 배경 위에서도 자연스럽게 섞인다. 이미지나 여러 배경색 위에 놓이는 표면일수록 이 대체가 유효하다.
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* 라이트 모드 — 3겹: 1px 링 + 미세한 lift + ambient 깊이 */
|
||||
--shadow-border:
|
||||
0px 0px 0px 1px oklch(0 0 0 / 0.06),
|
||||
0px 1px 2px -1px oklch(0 0 0 / 0.06),
|
||||
0px 2px 4px 0px oklch(0 0 0 / 0.04);
|
||||
--shadow-border-hover:
|
||||
0px 0px 0px 1px oklch(0 0 0 / 0.08),
|
||||
0px 1px 2px -1px oklch(0 0 0 / 0.08),
|
||||
0px 2px 4px 0px oklch(0 0 0 / 0.06);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
/* 다크 모드 — 레이어드 그림자는 어두운 배경에서 거의 안 보인다.
|
||||
흰 링 1겹으로 단순화한다. */
|
||||
--shadow-border: 0 0 0 1px oklch(1 0 0 / 0.08);
|
||||
--shadow-border-hover: 0 0 0 1px oklch(1 0 0 / 0.13);
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
box-shadow: var(--shadow-border);
|
||||
transition-property: box-shadow;
|
||||
transition-duration: var(--dur-instant);
|
||||
transition-timing-function: var(--ease-out);
|
||||
}
|
||||
.card:hover { box-shadow: var(--shadow-border-hover); }
|
||||
```
|
||||
|
||||
값과 라이트 3겹·다크 1겹 구조의 출처: [SKILL-BETTER-UI]. `transition-duration`은 원문의 150ms를 그대로 새 값으로 두지 않고 `tokens.md`의 폐쇄형 duration 토큰(`--dur-instant` = 100ms)에 매핑했다 — 근거 없는 새 리터럴을 만들지 않는다는 원칙에 따른 **실측 조정**이다.
|
||||
|
||||
## 5. 다크모드의 깊이
|
||||
|
||||
다크 배경에서는 밝은 배경보다 그림자의 명도 대비가 약해 레이어드 그림자가 잘 읽히지 않는다(§4의 "다크 1겹 단순화"가 같은 이유다). 다크 테마의 elevation 위계는 그림자보다 **표면 밝기 차이**로 먼저 전달한다 — `--surface`, `--surface-raised` 같은 역할 토큰([tokens.md](tokens.md) §2)을 elevation 단계가 올라갈수록 조금씩 밝게 다시 정의하고, `--shadow-border`의 흰 링은 경계를 보조하는 역할로만 둔다. 순수 검정에 가까운 표면 위에 그림자만으로 3단 위계를 만들려 하지 않는다.
|
||||
|
||||
## 6. 오용 휴리스틱
|
||||
|
||||
아래는 모두 **관찰 후보**다. 발견 자체가 실패가 아니라, 브리프·실제 렌더로 유지·수정 여부를 판단하는 점검 대상이다.
|
||||
|
||||
| 지문 | 왜 문제 | 대신 |
|
||||
|---|---|---|
|
||||
| 정의한 다층 그림자 체계와 프레임워크 기본 그림자를 한 화면에 섞어 씀 | 위계 신호가 두 체계로 나뉘어 무엇이 무엇보다 위인지 읽히지 않는다 | §2의 3단 토큰 하나만 쓴다 [SKILL-BEAUTIFUL-SHADOWS] |
|
||||
| 강한 색으로 그림자를 틴팅함(`rgba(139,92,246,.5)`류) | 깊이 신호가 브랜드 강조처럼 읽혀 의미가 겹친다 | 브랜드 토큰이 틴팅을 의도적으로 요구할 때만 예외로 둔다. 중립이 기본값이다 [SKILL-BEAUTIFUL-SHADOWS] |
|
||||
| 밀집 리스트·그리드의 각 행에 `--shadow-overlay`급 강한 그림자를 반복 | 시각 노이즈가 쌓이고 리스트 스크롤에서 페인트 비용이 누적된다 | 밀집 목록에는 `--shadow-resting`이나 얇은 경계를 쓴다 [SKILL-BEAUTIFUL-SHADOWS] |
|
||||
| 한 요소에 그림자 토큰 여러 개를 동시에 쌓음 | 어느 레이어가 진짜 위계 신호인지 불명확해진다 | 상태당 그림자 토큰 하나 원칙(§2)을 지킨다 [SKILL-BEAUTIFUL-SHADOWS] |
|
||||
| 대비가 낮은 배경에서 명확한 경계선 대신 옅은 그림자만으로 표면을 구분 | 저대비 환경·확대·forced-colors에서 경계 자체가 사라질 수 있다 | 저대비 배경에는 `--line` 토큰의 실제 테두리를 함께 쓴다 [SKILL-BEAUTIFUL-SHADOWS] |
|
||||
| 여러 카드가 위계와 무관하게 같은 radius·같은 흐린 회색 그림자를 반복(카드 키트 지문) | 콘텐츠가 다른데 표면 처리가 같으면 임의 템플릿으로 읽힌다 | radius·그림자 단계를 콘텐츠 밀도·중요도와 연결해 다시 정한다 [SKILL-FRONTEND-DESIGN] |
|
||||
|
||||
## 7. 머티리얼 위계
|
||||
|
||||
**언제**: 브리프나 프리셋이 유리·Apple풍 질감을 허용할 때만 연다. `swiss-minimal`처럼 재질을 배제하는 프리셋에는 적용하지 않는다([svg-filters.md](svg-filters.md) §1의 프리셋별 재질 표를 먼저 확인한다).
|
||||
|
||||
이 절은 반투명 표면의 깊이 위계를 다룬다. `backdrop-filter` 구현 자체(변위맵, Safari 폴백, 성능 최적화)와 위계·가독성(vibrancy)·스티키 헤더 마스크·진입 애니메이션의 상세 규칙·코드는 [svg-filters.md](svg-filters.md) "머티리얼 위계와 가독성" 절이 정본이다 — 여기서 중복 정의하지 않는다. 핵심만 요약하면, 무겁고 어두운(강한 블러+짙은 배경) 재질은 사이드바 같은 구조적 영역에, 가볍고 밝은 재질은 버튼 같은 인터랙티브 요소에 쓰고, 표면이 클수록 재질도 두껍게 읽혀야 한다 — **머티리얼 무게가 위계를 인코딩한다**는 것이 이 절의 한 줄 요약이다[SKILL-APPLE-DESIGN].
|
||||
|
||||
## 8. 선호 신호
|
||||
|
||||
| 미디어 쿼리 | 위계 | 대응 |
|
||||
|---|---|---|
|
||||
| `prefers-reduced-transparency: reduce` | **프로젝트 계약**(§7 적용 시) | 반투명 표면의 배경 불투명도를 높이고 블러를 낮춰 더 서리 낀 상태로 전환한다. 2026-09-24 기준 Chrome/Edge 119만 구현했고 Firefox·Safari는 미지원이라 점진적 향상으로 다룬다 — 미지원 브라우저에서는 아무 효과가 없을 뿐 오류가 나지 않는다 [WEB-BASELINE] |
|
||||
| `prefers-contrast: more` | **프로젝트 계약** | 거의 불투명한 배경과 뚜렷한 테두리로 전환한다. 2022년 5월부터 주요 브라우저 전반에서 지원되는 안정 기능이다 [WEB-BASELINE] |
|
||||
| `forced-colors: active` | **하드 게이트** | 강제 색상 모드에서는 `box-shadow`가 `none`으로 강제되어 사라진다(대체되지 않는다). `border` 등 색은 시스템 팔레트로 바뀐다. 그림자만으로 표시한 경계는 이 모드에서 사라지므로, §4의 `--shadow-border`로만 경계를 나타낸 표면에는 실제 `border`나 `outline`을 함께 정의해 forced-colors에서도 경계가 남게 한다. `forced-colors`는 2022년 9월부터 안정 지원된다 [WEB-BASELINE][WCAG-NONTEXT] |
|
||||
|
||||
```css
|
||||
@media (prefers-reduced-transparency: reduce) {
|
||||
.glass { background: var(--surface-raised); backdrop-filter: none; }
|
||||
}
|
||||
@media (prefers-contrast: more) {
|
||||
.card { border: 1px solid var(--line); }
|
||||
}
|
||||
@media (forced-colors: active) {
|
||||
.card { border: 1px solid CanvasText; box-shadow: none; }
|
||||
}
|
||||
```
|
||||
|
||||
## 9. 검증
|
||||
|
||||
- **두 테마**에서 그림자·표면 밝기가 elevation 단계를 실제로 구분해 보이는지 확인한다(§5).
|
||||
- `backdrop-filter`를 쓴 표면에는 **최악의 배경 콘텐츠**(다색 이미지, 밝은 텍스트가 섞인 스크린샷)를 뒤에 두고 vibrancy 처리로 텍스트가 읽히는지 확인한다(§7).
|
||||
- **밀집 목록·그리드**에서 그림자를 반복 적용했을 때 시각 노이즈나 스크롤 프레임 저하가 없는지 확인한다(§6).
|
||||
- **200% 확대**와 `forced-colors`·필터를 전부 끈 상태([svg-filters.md](svg-filters.md) §7의 감사 주입 패턴)에서 표면 경계가 여전히 구분되는지 확인한다(§8).
|
||||
|
|
@ -44,6 +44,58 @@
|
|||
| LUPTON | [Thinking with Type, 3rd ed.](https://www.chroniclebooks.com/products/thinking-with-type-1) · 2024 | bibliography | Lupton 저작의 3판 출판 정보를 확인하는 출발점이다 | 이 ledger는 본문 요약이나 보편 수치 근거가 아니다 | 판본을 확보한 뒤 개념·예시 페이지를 별도 기록한다 |
|
||||
| MULLER-BROCKMANN | [Grid systems in graphic design](https://niggli.ch/en/products/rastersysteme-fur-die-visuelle-gestaltung) · 확인 2026-09-12 | bibliography | Müller-Brockmann 저작의 출판사 서지 정보를 확인하는 출발점이다 | 그리드를 모든 과업의 정답으로 만들지 않는다 | 판본을 확보한 뒤 grid 선택의 목적·예외·검증을 분리 기록한다 |
|
||||
|
||||
## 외부 스킬 출처 (2026-09-24 흡수)
|
||||
|
||||
조사일: 2026-09-24. 아래는 `docs/design-skills-intake-plan-20260924.md`로 흡수한 외부 디자인 스킬 10종과 better-interface 자매 스킬 7종의 출처 ID다. 저장소·커밋·라이선스 전문은 [THIRD_PARTY_NOTICES.md](../THIRD_PARTY_NOTICES.md)에 있다. 이 스킬들의 종류는 모두 `practice`(실무자가 정리한 설계 절차)이며, "실제로 지지하는 주장"은 그 스킬이 실제로 제공하는 실무 지침·수치 시작값으로 좁혀 쓴다. 공통된 잘못된 일반화는 스킬 저자 개인·소속 팀의 관찰이나 수치를 웹 디자인 전반의 보편 규범으로 확대하는 것이다.
|
||||
|
||||
| ID | 출처·날짜 | 종류 | 실제로 지지하는 주장 | 적용 조건 / 잘못된 일반화 | 구체 검증 |
|
||||
|---|---|---|---|---|---|
|
||||
| SKILL-FRONTEND-DESIGN | [anthropics/skills — skills/frontend-design](https://github.com/anthropics/skills/tree/34040c9/skills/frontend-design) · 커밋 `34040c9` (2026-09-10) · Apache-2.0 | practice | AI가 생성한 웹 디자인이 반복하는 제네릭 클러스터(색상·타이포·카드·모션 패턴)를 실무자가 정리한 점검표이며, 특정 액센트 hex가 Claude 계열 도구의 지문으로 읽힐 수 있다는 관찰을 포함한다 | 저자가 관찰한 클러스터 분류나 예시를 웹 디자인 전반의 보편 규범으로 확대하지 않는다. 저자 자신의 프로젝트 경험에서 추출한 관찰 후보다 | 지목된 hex·패턴이 실제 렌더에 나타나는지 대조하고, "다른 업종 브리프에도 같은 선택이 그대로 나왔을까"라는 반사실 질문으로 재확인한다 |
|
||||
| SKILL-APPLE-DESIGN | [emilkowalski/skills — skills/apple-design](https://github.com/emilkowalski/skills/tree/85e8e23/skills/apple-design) · 커밋 `85e8e23` · MIT | practice | Apple 플랫폼의 인터럽트 가능한 제스처·스프링·모멘텀·러버밴드 구현을 정리한 수치 시작값과 구현 패턴이다(damping/response, 속도 인계, 모멘텀 투사 계수, 러버밴드 계수 등) | 저자가 제시한 계수를 물리 법칙이나 플랫폼 표준으로 취급하지 않는다. Apple풍 미학을 모든 브리프의 기본값으로 삼지 않는다(조건부 항목). 원 스킬이 수치에 1차 출처(WWDC 영상·타임스탬프)를 달지 않았다 — 3rd-party 재해석으로 취급하고 옮길 때 그 사실을 밝힌다 | 실제 구현에서 계수를 실측 조정하고, Apple풍이 브리프에 맞는 조건일 때만 적용한다 |
|
||||
| SKILL-EMIL-DESIGN-ENG | [emilkowalski/skills — skills/emil-design-eng](https://github.com/emilkowalski/skills/tree/85e8e23/skills/emil-design-eng) · 커밋 `85e8e23` · MIT | practice | 모션 구현 실무 지침(사용 빈도별 애니메이션 여부, 트랜지션과 키프레임의 차이, clip-path·드래그·툴팁 구현 레시피, 모션 QA 절차)이다 | "ease-in 절대 금지" 같은 단정을 그대로 규범화하지 않는다. velocity 0.11 같은 매직넘버는 저자의 실측값이지 보편 상수가 아니다 | 레시피를 실제 컴포넌트에 적용해 프레임 단위로 재생해 보고, duration·easing은 프로젝트 토큰(`--dur-*`, `--ease-*`)에 맞춰 재조정한다 |
|
||||
| SKILL-BEAUTIFUL-SHADOWS | [MengTo/Skills — agent-skills/web-design/beautiful-shadows](https://github.com/MengTo/Skills/tree/a965851/agent-skills/web-design/beautiful-shadows) · 커밋 `a965851` · MIT | practice | 3단(sm/md/lg) 다층 box-shadow 값과 컴포넌트 밀도별 단계 매핑을 제시하는 시작값 세트다 | 레이어 수가 늘수록 값이 커지고 진해지는 이유를 저자도 밝히지 않으므로 그 근거를 인용하지 않는다. Tailwind 임의값 문법은 그대로 쓰지 않고 CSS로 옮겨 쓴다 | 실제 배경·밀도에서 렌더해 과도한 틴팅·중첩을 확인한다 |
|
||||
| SKILL-WEB-A11Y | [addyosmani/web-quality-skills — skills/accessibility](https://github.com/addyosmani/web-quality-skills/tree/afa8da9/skills/accessibility) · 커밋 `afa8da9` · MIT | practice | Lighthouse·axe 자동 검사 → 실패 노드 국소화 → 수동 검증 → 재감사로 이어지는 증거 우선 접근성 감사 루프와 "자동 점수 100은 준수를 뜻하지 않는다"는 절차 지침이다 | 이 검사 순서를 WCAG 적합성 판정 자체로 착각하지 않는다. 자동화 도구가 잡아내는 범위는 WCAG 성공 기준의 일부일 뿐이다 | DevTools MCP나 axe-core가 있으면 실행하고, 없으면 미검증으로 보고하며 통과로 추정하지 않는다 |
|
||||
| SKILL-DESIGN-REVIEW | [Superfuture/design-review — design-review/skills/design-review](https://github.com/Superfuture/design-review/tree/d4d2609/design-review/skills/design-review) · 커밋 `d4d2609` · MIT(plugin.json 선언, 저장소에 LICENSE 파일 없음 — THIRD_PARTY_NOTICES.md 참고) | practice | 디자인 리뷰 finding을 What·Why·Fix 3요소와 심각도로 구조화하고, 입력 종류별 진입·자동 적용 화이트리스트를 정리한 리뷰 절차다 | 서체 개수 상한·타입 스케일 단계 수 같은 저자의 규범적 수치는 취향 규칙이지 하드 게이트가 아니다. 텔레메트리·유료 게이팅 요소는 흡수하지 않는다 | finding마다 What·Why·Fix가 모두 채워졌는지, 심각도가 판정 위계와 일치하는지 확인한다 |
|
||||
| SKILL-SHADCN | [shadcn-ui/ui — skills/shadcn](https://github.com/shadcn-ui/ui/tree/98a1fe6/skills/shadcn) · 커밋 `98a1fe6` · MIT | practice | 기존 컴포넌트 우선·합성 규칙(Dialog Title 필수 등)과 "추측하지 말고 사용자 대신 기본값을 고르지 말라"는 안전 병합 절차다 | CLI 플래그·레지스트리 스키마·Tailwind 매핑 치트시트는 designpaca가 프레임워크를 전제하지 않는다는 원칙과 충돌해 흡수하지 않는다. shadcn 같은 컴포넌트 기반은 브랜드 디자인 시스템 자체가 아니다 | components.json 존재로 감지 여부를 판정하고, 감지했을 때만 component-systems.md 절을 연다 |
|
||||
| SKILL-IMPECCABLE | [pbakaus/impeccable — skill/reference/adapt.md](https://github.com/pbakaus/impeccable/tree/e0881d2/skill/reference/adapt.md), [skill/reference/adapt.native.md](https://github.com/pbakaus/impeccable/tree/e0881d2/skill/reference/adapt.native.md)(iOS↔Android 관용구 대응표) · 커밋 `e0881d2` · Apache-2.0 | practice | 입력 방식을 화면 크기와 별개 축으로 다루는 pointer·hover 쿼리, 컨테이너 쿼리, 커스텀 컨트롤 제스처 검증(레이아웃 통과 ≠ 제스처 통과)에 대한 구현 코드와 절차, 그리고 iOS↔Android 플랫폼 관용구 대응표다 | 모바일 하단 내비 기본값 같은 저자의 결론은 기각한다(designpaca 결정표가 더 엄밀하다). 빌드 시점 하네스별 본문 컴파일 방식은 흡수하지 않는다 | 실제 포인터·터치 입력에서 코드를 재현하고, 제스처 검증은 레이아웃 검사와 분리해 수행한다 |
|
||||
| SKILL-BETTER-INTERFACE | [jakubkrehel/skills — skills/better-interface](https://github.com/jakubkrehel/skills/tree/267330e/skills/better-interface) · 커밋 `267330e` · MIT | practice | 에스컬레이션 트리거(규칙 소유·심각도 분리), 저비용 수정 사다리(삭제→플랫폼→재사용→값 교정→추가), 미검증·미점검 구분, 스코프 축소 규율을 제시하는 리뷰 절차다 | "근사가 아니라 정확히 이 값" 같은 규율은 기각한다. finding 상한 같은 수치는 근거가 없어 절차만 채택하고 숫자는 채택하지 않는다 | 리뷰 결과에서 저비용 수정 사다리 순서를 실제로 따랐는지, 미검증 항목이 finding으로 세어지지 않았는지 확인한다 |
|
||||
| SKILL-BETTER-A11Y | [jakubkrehel/skills — skills/better-accessibility](https://github.com/jakubkrehel/skills/tree/267330e/skills/better-accessibility) · 커밋 `267330e` · MIT | practice | 키보드 위젯 계약(tabindex, roving tabindex, APG 패턴, inert, SPA 라우트 포커스)과 폼 접근성(autocomplete, inputmode, disabled와 aria-disabled 구분) 실무 지침이다 | 고정 하한값이나 "반드시 이 패턴" 같은 단정은 하드 게이트 문맥(WCAG 근거가 있는 것)에만 남기고, 나머지는 프로젝트 계약·관찰 후보로 낮춘다 | 실제 키보드 탐색과 스크린리더로 각 위젯의 계약을 재현한다 |
|
||||
| SKILL-BETTER-COLORS | [jakubkrehel/skills — skills/better-colors](https://github.com/jakubkrehel/skills/tree/267330e/skills/better-colors) · 커밋 `267330e` · MIT | practice | 색 램프 생성, APCA 참고, 그라디언트 보간 공간, 다크모드 재조정에 대한 실무 절차다 | APCA는 WCAG 2 대비를 대체하는 규범이 아니라 보조 진단으로만 쓴다(APCA-STATUS 참고). 색의 문화적 의미는 원문(주로 서구 관례)을 그대로 쓰지 않고 ko-KR 관례로 다시 쓴다 | 실제 배경·전경 조합에서 WCAG 2 대비를 1차 판정 기준으로 측정한다 |
|
||||
| SKILL-BETTER-LAYOUT | [jakubkrehel/skills — skills/better-layout](https://github.com/jakubkrehel/skills/tree/267330e/skills/better-layout) · 커밋 `267330e` · MIT | practice | RTL·논리 속성, 그루핑 비율, 점진적 공개 레시피, 컨트롤·정적 텍스트 구별에 대한 레이아웃 실무 지침이다 | 그루핑 비율 같은 수치는 휴리스틱이지 하드 게이트가 아니다. RTL 절은 RTL 로케일일 때만 연다(조건부) | 실제 좁은 폭·RTL 전환에서 레이아웃이 깨지지 않는지 확인한다 |
|
||||
| SKILL-BETTER-TYPE | [jakubkrehel/skills — skills/better-typography](https://github.com/jakubkrehel/skills/tree/267330e/skills/better-typography) · 커밋 `267330e` · MIT | practice | text-wrap, 밑줄 메트릭, text-box trim, 문장부호, 잘린 텍스트 도달 수단에 대한 조판 실무 지침이다 | 고정 글자 크기·행간 하한 규율은 기각한다. 문장부호는 원문(영어 관례) 그대로 쓰지 않고 ko-KR 문장부호 규정(KO-PUNCT)으로 다시 쓴다 | 실제 다국어·긴 콘텐츠에서 줄바꿈과 밑줄 겹침을 렌더로 확인한다 |
|
||||
| SKILL-BETTER-UI | [jakubkrehel/skills — skills/better-ui](https://github.com/jakubkrehel/skills/tree/267330e/skills/better-ui) · 커밋 `267330e` · MIT | practice | 아이콘 도메인 일관성, 동심 radius, 광학 정렬, 테두리 대신 그림자, 테마 전환 트랜지션 억제에 대한 UI 마감 실무 지침이다 | 특정 미학을 기본값으로 서술하지 않는다. 동심 radius 공식은 tokens.md의 기존 토큰 이름 체계를 따라 재정의 없이 참조한다 | 실제 컴포넌트 중첩에서 radius·그림자가 시각적으로 어긋나지 않는지 확인한다 |
|
||||
| SKILL-BETTER-WRITING | [jakubkrehel/skills — skills/better-writing](https://github.com/jakubkrehel/skills/tree/267330e/skills/better-writing) · 커밋 `267330e` · MIT | practice | 용어 일관성, 톤과 위험의 매트릭스, 문장 조각 조립 금지, 오류·빈 상태 문구 내용에 대한 UX 카피 실무 지침이다 | "카피 finding은 소스만으로 충분하다"는 예외는 잘림·줄바꿈에 영향받는 카피(버튼·제목)에는 적용하지 않고 렌더로 확인한다. 문장 조각 조립 규칙은 한국어 조사 처리를 별도로 더해야 한다(원문은 영어 전제) | 실제 렌더에서 버튼·제목 카피가 잘리지 않는지 확인하고, 소스 검토는 나머지 카피에만 1차 판정으로 쓴다 |
|
||||
| SKILL-INTERFACE-REVIEW | [jakubkrehel/skills — skills/interface-review](https://github.com/jakubkrehel/skills/tree/267330e/skills/interface-review) · 커밋 `267330e` · MIT | practice | diff 스코프 계산, 제거된 쪽 읽기, Introduced·Regression·Pre-existing 구분에 대한 변경 리뷰 절차다 | 파급 범위 컨슈머 5개 같은 고정 수치는 근거가 없어 절차만 채택한다 | 실제 diff에서 제거된 코드를 읽고 회귀 신호가 있는지 확인한다 |
|
||||
| SKILL-INTERACTION-DESIGN | [wshobson/agents — plugins/ui-design/skills/interaction-design](https://github.com/wshobson/agents/tree/4236bb9/plugins/ui-design/skills/interaction-design) · 커밋 `4236bb9`(출처 미기재라 가장 널리 쓰이는 저장소를 선정) · MIT | practice | 토스트, 스켈레톤 시머, 스와이프, 당겨서 새로고침, 낙관적 업데이트 등 인터랙션 레시피와 스프링 프리셋·cubic-bezier 근사값이다 | 리플 이펙트를 기본값으로 삼지 않고 조건부로만 둔다. "질문 없이 코드 기본값으로 고정"하는 태도는 기각한다. 스프링 프리셋 수치는 실측 조정 시작값이다 | 실제 인터랙션에서 클린업 규율(리스너 해제 등)이 지켜지는지 확인하고, 스프링 값은 프로젝트에서 재조정한다 |
|
||||
|
||||
## WCAG 2.2·ARIA·플랫폼 출처 보강 (2026-09-24)
|
||||
|
||||
| ID | 출처·날짜 | 종류 | 실제로 지지하는 주장 | 적용 조건 / 잘못된 일반화 | 구체 검증 |
|
||||
|---|---|---|---|---|---|
|
||||
| WCAG-INPUT-PURPOSE | [SC 1.3.5 Identify Input Purpose](https://www.w3.org/TR/WCAG22/#input-purposes) · WCAG 2.2, 확인 2026-09-24 | normative | Level AA. 사용자 정보를 수집하는 입력 필드가 Input Purposes 목록에 정의된 목적을 가지면 프로그램적으로 식별 가능해야 한다 | 목록에 없는 임의 입력 필드까지 autocomplete 속성을 요구하는 근거로 확대하지 않는다 | 실제 폼 필드의 autocomplete·name 속성이 목록의 목적과 일치하는지 대조한다 |
|
||||
| WCAG-NONTEXT | [SC 1.4.11 Non-text Contrast](https://www.w3.org/TR/WCAG22/#non-text-contrast) · WCAG 2.2, 확인 2026-09-24 | normative | Level AA. UI 컴포넌트와 그래픽 객체의 시각적 표현은 인접 색상과 3:1 이상 대비를 가져야 한다(비활성 컴포넌트·필수 로고·장식용 그래픽 등 예외 있음) | 모든 장식 요소에 3:1을 강제하지 않는다. 예외 목록을 먼저 확인한다 | 실제 컴포넌트 테두리·아이콘·포커스 링의 대비를 측정한다 |
|
||||
| WCAG-HOVER | [SC 1.4.13 Content on Hover or Focus](https://www.w3.org/TR/WCAG22/#content-on-hover-or-focus) · WCAG 2.2, 확인 2026-09-24 | normative | Level AA. 호버·포커스로 나타나는 추가 콘텐츠는 Dismissible·Hoverable·Persistent 세 조건을 만족해야 한다 | 모든 툴팁·팝오버에 세 조건을 기계적으로 적용하기 전에 필수 콘텐츠인지부터 판정한다 | 실제 툴팁을 호버·포커스로 열고 마우스를 이동시켜 세 조건을 재현한다 |
|
||||
| WCAG-FLASH | [SC 2.3.1 Three Flashes or Below Threshold](https://www.w3.org/TR/WCAG22/#three-flashes-or-below-threshold) · WCAG 2.2, 확인 2026-09-24 | normative | Level A. 웹 페이지는 1초 동안 3회를 초과해 깜빡이는 콘텐츠를 포함하지 않아야 한다(일반 섬광·적색 섬광 임계값 이하는 예외) | "깜빡임 초당 3회 이하"를 스타일 취향이 아니라 하드 게이트로 취급한다 | 자동 재생 애니메이션·로딩 인디케이터의 초당 깜빡임 횟수를 측정한다 |
|
||||
| WCAG-LINK | [SC 2.4.4 Link Purpose (In Context)](https://www.w3.org/TR/WCAG22/#link-purpose-in-context) · WCAG 2.2, 확인 2026-09-24 | normative | Level A. 링크의 목적은 링크 텍스트만으로, 또는 링크 텍스트와 프로그램적으로 결정 가능한 맥락을 함께 사용해 판단할 수 있어야 한다(일반 사용자에게 모호한 경우는 예외) | "더보기"류 링크 텍스트를 무조건 금지하는 규범으로 확대하지 않는다. 주변 맥락으로 목적이 프로그램적으로 판단 가능하면 허용된다 | 스크린리더의 링크 목록 탐색에서 텍스트만으로 목적을 알 수 있는지 확인한다 |
|
||||
| WCAG-FOCUS-OBSCURED | [SC 2.4.11 Focus Not Obscured (Minimum)](https://www.w3.org/TR/WCAG22/#focus-not-obscured-minimum) · WCAG 2.2, 확인 2026-09-24 | normative | Level AA. 키보드 포커스를 받은 컴포넌트는 저자가 만든 콘텐츠(스티키 헤더·모달 등)에 완전히 가려지지 않아야 한다 | 완전 가림만 금지한다(일부 가림까지 금지하는 것은 AAA인 2.4.12다). AA와 AAA를 혼동하지 않는다 | 스티키 헤더·모달이 있는 화면에서 Tab 이동 시 포커스 요소가 완전히 가려지는지 확인한다 |
|
||||
| WCAG-FOCUS-APPEAR | [SC 2.4.13 Focus Appearance](https://www.w3.org/TR/WCAG22/#focus-appearance) · WCAG 2.2, 확인 2026-09-24 | normative | Level AAA. 보이는 포커스 표시기는 비포커스 상태 대비 2 CSS px 두께 둘레 이상의 면적과 3:1 이상 대비를 가져야 한다(사용자 에이전트 기본 표시기이거나 저자가 수정하지 않은 경우는 예외) | AAA 기준이므로 하드 게이트가 아니라 프로젝트 계약(고대비·접근성 강화 프로젝트)으로 채택한다 | 커스텀 포커스 링의 두께·대비를 실제로 측정한다 |
|
||||
| WCAG-LABEL-NAME | [SC 2.5.3 Label in Name](https://www.w3.org/TR/WCAG22/#label-in-name) · WCAG 2.2, 확인 2026-09-24 | normative | Level A. 텍스트나 텍스트 이미지를 포함한 라벨을 가진 UI 컴포넌트는 접근 가능한 이름에 시각적으로 표시된 텍스트를 포함해야 한다 | 아이콘 전용 버튼처럼 시각 라벨이 없는 컴포넌트는 적용 대상이 다르다(별도로 aria-label 등을 판단) | 시각 라벨과 스크린리더가 읽는 접근 가능한 이름을 대조한다 |
|
||||
| WCAG-DRAG | [SC 2.5.7 Dragging Movements](https://www.w3.org/TR/WCAG22/#dragging-movements) · WCAG 2.2, 확인 2026-09-24 | normative | Level AA. 드래그 동작으로 작동하는 모든 기능은 드래그 없이 단일 포인터로도 수행할 수 있어야 한다(드래그가 필수적이거나 사용자 에이전트가 결정하고 저자가 수정하지 않은 기능은 예외) | 브라우저 기본 스크롤·당겨서 새로고침처럼 사용자 에이전트가 결정하는 동작까지 대체 수단을 요구하지 않는다 | 슬라이더·정렬·스와이프 삭제 등 커스텀 드래그 기능에 탭·버튼 등 단일 포인터 대안이 있는지 확인한다 |
|
||||
| WCAG-HELP | [SC 3.2.6 Consistent Help](https://www.w3.org/TR/WCAG22/#consistent-help) · WCAG 2.2, 확인 2026-09-24 | normative | Level A. 웹 페이지 집합 안에서 반복되는 도움 메커니즘(연락처·챗봇·자가 도움 등)은 사용자가 변경을 시작하지 않는 한 다른 콘텐츠와 상대적으로 같은 순서에 나타나야 한다 | 도움 메커니즘이 아예 없는 사이트에 새로 만들라고 요구하는 기준이 아니다. 이미 있는 도움 메커니즘의 위치 일관성만 요구한다 | 여러 화면에서 고객센터·챗봇 버튼의 상대적 위치가 같은지 확인한다 |
|
||||
| WCAG-REDUNDANT | [SC 3.3.7 Redundant Entry](https://www.w3.org/TR/WCAG22/#redundant-entry) · WCAG 2.2, 확인 2026-09-24 | normative | Level A. 같은 프로세스에서 이전에 입력했거나 제공된 정보를 다시 입력하도록 요구할 때는 자동으로 채우거나 선택할 수 있게 해야 한다(재입력이 필수적이거나 보안상 필요하거나 정보가 더 이상 유효하지 않은 경우는 예외) | 결제 단계의 보안 재확인처럼 예외에 해당하는 재입력까지 위반으로 판정하지 않는다 | 다단계 폼에서 같은 값을 다시 입력해야 하는 필드가 있는지, 자동 채움이 되는지 확인한다 |
|
||||
| WCAG-AUTH | [SC 3.3.8 Accessible Authentication (Minimum)](https://www.w3.org/TR/WCAG22/#accessible-authentication-minimum) · WCAG 2.2, 확인 2026-09-24 | normative | Level AA. 인증 과정의 어떤 단계도 대안·보조 메커니즘·객체 인식·개인 콘텐츠 인식 중 하나를 제공하지 않는 한 인지 기능 시험(비밀번호 암기, 퍼즐 풀이 등)을 요구해서는 안 된다 | 모든 비밀번호 로그인을 금지하는 기준이 아니다. 브라우저 자동완성·비밀번호 관리자 지원처럼 대안이 있으면 허용된다 | 로그인·본인 확인 흐름에서 붙여넣기 차단이나 자동완성 차단이 있는지 확인한다 |
|
||||
| WCAG-NRV | [SC 4.1.2 Name, Role, Value](https://www.w3.org/TR/WCAG22/#name-role-value) · WCAG 2.2, 확인 2026-09-24 | normative | Level A. 커스텀 UI 컴포넌트의 이름과 역할은 프로그램적으로 결정 가능해야 하고, 사용자가 설정할 수 있는 상태·속성·값은 프로그램적으로 설정 가능해야 하며, 변경 알림이 보조 기술에 제공되어야 한다 | 네이티브 HTML 컨트롤에는 대개 자동으로 충족되며, 커스텀 컴포넌트(div로 만든 버튼 등)에 특히 적용된다. 예외·세부 문구는 Understanding 문서로 재확인 필요 | 스크린리더로 커스텀 컴포넌트의 이름·역할·상태 변경이 공지되는지 확인한다 |
|
||||
| WCAG-STATUS | [SC 4.1.3 Status Messages](https://www.w3.org/TR/WCAG22/#status-messages) · WCAG 2.2, 확인 2026-09-24 | normative | Level AA. 마크업 언어로 구현된 콘텐츠에서 상태 메시지는 포커스를 받지 않고도 보조 기술이 사용자에게 전달할 수 있도록 role이나 속성으로 프로그램적으로 결정 가능해야 한다 | 포커스를 받는 오류 대화상자 등 4.1.2로 이미 다뤄지는 경우와 겹치지 않게 구분한다(정확한 예외 문구는 Understanding 문서 재확인 필요) | 폼 제출 성공·실패 메시지가 라이브 리전으로 스크린리더에 전달되는지 확인한다 |
|
||||
| ARIA-APG | [WAI-ARIA Authoring Practices Guide — Patterns](https://www.w3.org/WAI/ARIA/apg/patterns/) · 확인 2026-09-24 | practice | 탭·메뉴버튼·콤보박스·리스트박스·모달 대화상자 등 위젯별 권장 키보드 상호작용(Tab·화살표·Home/End·Escape 동작)을 정의한다 | ARIA role만 붙이고 키보드 동작을 구현하지 않으면 패턴을 따랐다고 할 수 없다. 네이티브 HTML 요소로 같은 동작을 얻을 수 있으면 네이티브를 우선한다 | 실제 키보드만으로 각 위젯 패턴의 상호작용을 재현한다 |
|
||||
| APCA-STATUS | [APCA-W3 GitHub README](https://github.com/Myndex/apca-w3/blob/master/README.md) · [WCAG 3.0 Working Draft](https://www.w3.org/TR/wcag-3.0/)(2026-09-10) · 확인 2026-09-24 | practice | WCAG 3.0 초안(2026-09-10)은 대비 알고리즘을 아직 정하지 않았다고 명시하며 APCA를 이름으로 언급하지 않는다. APCA 자체는 버전 0.1.9(98G4g)의 베타 상태이며 저장소 README가 "미래 표준을 위해 평가 중"이라고 스스로 밝힌다 | APCA를 이미 채택된 규범적 대비 알고리즘처럼 서술하지 않는다. WCAG 2 대비(4.5:1/3:1)가 여전히 하드 게이트이고 APCA는 보조 진단으로만 쓴다 | 대비 판정은 WCAG 2 공식(WCAG-READ)으로 먼저 하고, APCA 값은 참고 수치로만 병기한다 |
|
||||
| WWDC-FLUID | [Designing Fluid Interfaces — WWDC 2018, 세션 803](https://developer.apple.com/videos/play/wwdc2018/803/) · Apple Developer, 확인 2026-09-24 | practice | iPhone X 세대 제스처 인터페이스에서 인터럽트 가능성(현재 렌더값에서 재시작), 속도 인계, 멀티모달 피드백을 설계한 Apple 엔지니어의 발표 내용이다 | Apple의 자체 설계 사례를 모든 플랫폼·모든 브리프에 적용해야 할 표준으로 확대하지 않는다. Apple풍 미학이 브리프에 맞을 때만 조건부로 연다 | 발표에서 제시한 구현 아이디어를 실제 코드로 재현한 뒤 체감 반응성을 확인한다 |
|
||||
| KO-PUNCT | [한글 맞춤법 문장 부호 개정안 — 국립국어원](https://www.korean.go.kr/front/etcData/etcDataView.do?mn_id=46&etc_seq=431) · 고시 2014-12-05 · 시행 2015-01-01 · 확인 2026-09-24 | normative | 문화체육관광부가 고시한 한글 맞춤법 개정으로 마침표·물음표·느낌표·쉼표·가운뎃점·쌍점·빗금·따옴표(2종)·괄호(3종)·낫표(2종)·화살괄호(2종)·줄표·붙임표·물결표 등 문장 부호 용법을 정한다 | 영어 문장부호 관례(예: 세미콜론 용법)를 한국어 카피에 그대로 옮기지 않는다. 이 개정안이 다루지 않는 세부 조항(예: 줄임표 표기 형태)은 별도 확인 없이 단정하지 않는다 | 한국어 UI 카피의 마침표·쉼표·따옴표 사용을 이 개정안 목록과 대조한다 |
|
||||
| KO-KRDS | [KRDS 컴포넌트 — 버튼 · 행정안전부](https://www.krds.go.kr/html/site/component/component_05_02.html) · 확인 2026-09-24 | practice | 행정안전부 KRDS(Korea Design System) 공식 가이드는 버튼 텍스트 라벨을 원칙적으로 동사형으로 제공하도록 규정하며, "완료·닫기·취소·추가·삭제"처럼 일반적으로 통용되는 경우는 예외로 둔다 | 정부 서비스 가이드를 모든 상업 제품의 카피 규범으로 강제하지 않는다. 이 스킬에서는 버튼 카피의 참고 관례로만 쓴다 | 실제 버튼 라벨이 동사형인지, 예외에 해당하는 일반 동작인지 확인한다 |
|
||||
| KO-TOSS-WRITING | [토스가 글을 쓰는 방법 — toss.tech](https://toss.tech/article/8-writing-principles-of-toss) · 확인 2026-09-24 | practice | 토스 기술 블로그가 공개한 글쓰기 코어밸류 5가지(Clear·Concise·Casual·Respect·Emotional)와 실행 원칙 8가지(예측 가능한 힌트, 군더더기 제거, 핵심 메시지 집중 등)다 | 한 기업의 브랜드 보이스를 모든 한국어 제품의 보편 톤으로 확대하지 않는다. 톤 슬롯이 다른 브리프(공공·금융 격식체 등)에는 그대로 적용하지 않는다 | 실제 카피가 원칙과 충돌하는지(불필요한 격식체, 숨은 감정 무시 등)를 문장 단위로 대조한다 |
|
||||
| WEB-BASELINE | [webstatus.dev](https://webstatus.dev/) · [MDN Baseline](https://developer.mozilla.org/en-US/docs/Glossary/Baseline/Compatibility) · 확인 2026-09-24 | practice | 특정 CSS·JS 기능이 Limited/Newly available/Widely available 중 어느 지원 상태인지, 각 브라우저가 언제 지원을 시작했는지를 추적한다. Widely available은 Newly available(전 주요 엔진 지원 완료) 시점으로부터 약 30개월이 지나야 붙는다 | 한 번 확인한 지원 상태를 날짜 없이 영구 사실처럼 인용하지 않는다. webstatus.dev와 MDN이 다른 값을 보일 때(예: text-box-trim)는 더 최신 확인일 쪽에 주의 문구를 남긴다 | 기능을 채택하기 전 webstatus.dev나 MDN의 현재 상태와 확인일을 다시 조회한다 |
|
||||
| PLATFORM-HIG-TARGET | Apple Human Interface Guidelines, Layout(터치 타깃 지침) · developer.apple.com/design/human-interface-guidelines/layout · **URL 미확인** — 2026-09-24 WebFetch로 재확인을 시도했으나 페이지가 클라이언트 사이드 렌더링(SPA)이라 본문을 가져오지 못했고, 이 세션의 WebSearch 예산이 소진돼 대체 인용을 확보하지 못했다 | bibliography | Apple HIG가 iOS 컨트롤의 최소 히트 영역을 44×44pt로 권장한다는 것은 업계에 널리 알려진 수치이지만, 이 세션에서는 1차 문서 원문 문구를 직접 확인하지 못했다 | 브리프가 모바일 앱 수준을 요구할 때만 적용하는 **프로젝트 계약**이며, WCAG AA의 보편 요건(24×24 CSS px, [WCAG-TARGET])으로 확대하지 않는다 | 다음 세션에서 Apple Developer 사이트를 JS 렌더링 가능한 브라우저 도구로 재조회해 정확한 문구·URL을 확정한다 |
|
||||
| PLATFORM-M3-TARGET | Material 3, Accessible design 또는 Layout 섹션 · m3.material.io · **URL 미확인** — 2026-09-24 WebFetch로 재확인을 시도했으나 페이지가 클라이언트 사이드 렌더링(SPA)이라 본문을 가져오지 못했고, 이 세션의 WebSearch 예산이 소진돼 대체 인용을 확보하지 못했다 | bibliography | Material 3가 터치 타깃 최소 크기를 48×48dp로 권장한다는 것은 업계에 널리 알려진 수치이지만, 이 세션에서는 1차 문서 원문 문구를 직접 확인하지 못했다 | 브리프가 모바일 앱 수준을 요구할 때만 적용하는 **프로젝트 계약**이며, WCAG AA의 보편 요건(24×24 CSS px, [WCAG-TARGET])으로 확대하지 않는다 | 다음 세션에서 m3.material.io를 JS 렌더링 가능한 브라우저 도구로 재조회해 정확한 문구·URL을 확정한다 |
|
||||
|
||||
## 사용할 때
|
||||
|
||||
결정마다 `출처 ID / 지지 주장 / 적용 조건 / 잘못된 일반화 / 구체 검증 / 예시 결정`을 남긴다. 예: `WCAG-TARGET / 24×24 AA 최소 / 포인터 대상 / 44px 의무라고 확대하지 않음 / 실제 버튼 box 측정 / 작은 아이콘을 24px 이상 hit area로 확장`.
|
||||
|
|
|
|||
190
packages/skill/references/harness.md
Normal file
190
packages/skill/references/harness.md
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
# harness — 하네스별 기능과 질문 방법 원장
|
||||
|
||||
0단계에서, 그리고 하네스 기능(계획 도구·서브에이전트·브라우저 검증)을 쓸 때 읽는다. [brief-interview.md](brief-interview.md)가 "무엇을 언제 묻는가"를 정한다면, 이 문서는 "이 하네스에서 그것을 어떤 도구로, 몇 개까지, 어떤 조건에서 물을 수 있는가"를 정한다. [SKILL.md](../SKILL.md)의 인터뷰 게이트 여섯 규칙은 여기서 바뀌지 않는다.
|
||||
|
||||
## 이 문서를 읽는 법
|
||||
|
||||
| 상황 | 읽을 곳 |
|
||||
|---|---|
|
||||
| 지금 세션이 어떤 하네스인지 확인하고 질문 도구를 고른다 | §1 |
|
||||
| 몇 개까지 묻고 어떤 순서로 배치할지 정한다 | §2 |
|
||||
| 사용자가 없거나(`claude -p`, `codex exec`, CI), 빈 답이 왔거나, 서브에이전트로 실행 중이다 | §3 |
|
||||
| 0~6단계를 계획·할 일 도구로 추적하고 싶다 | §4 |
|
||||
| 1단계 레퍼런스 조사를 서브에이전트에 나눠 맡기고 싶다 | §5 |
|
||||
| 스크린샷·감사 도구로 렌더를 검증한다 | §6 |
|
||||
| 지금 세션이 Plan 모드·auto 모드인지 확인한다 | §7 |
|
||||
| 이 스킬의 frontmatter를 왜 최소로 유지하는지 | §8 |
|
||||
| 이 문서 각 사실의 출처를 확인한다 | §9 |
|
||||
|
||||
---
|
||||
|
||||
## 0. 확인일과 재확인 규칙
|
||||
|
||||
이 문서의 사실은 **2026-09-24**에 1차 출처(공식 문서·소스코드·로컬 CLI `--help` 출력)로 확인했다. 하네스는 스킬보다 빠르게 바뀐다 — 도구 이름이 바뀌거나, 기본 모드가 바뀌거나, 자율성 압력 문구가 바뀔 수 있다.
|
||||
|
||||
- **릴리스 전에 재확인한다.** `packages/skill/CHANGELOG.md`에 하네스 행동 변화가 있으면 이 문서를 먼저 고치고 나서 태그를 올린다.
|
||||
- **사실이 바뀌면 이 문서만 고친다.** SKILL.md·brief-interview.md의 여섯 규칙과 슬롯표는 하네스 사실과 무관하게 성립하는 절차이므로, 하네스 쪽 변경이 두 문서를 흔들 필요는 없다.
|
||||
- **미확인 사실은 미확인이라고 쓴다.** 이 문서의 표에서 "미확인"은 조사하지 않았다는 뜻이 아니라, 1차 출처에서 확인을 시도했지만 확인하지 못했다는 뜻이다. 확인한 적 없는 값을 추정해 채우지 않는다.
|
||||
|
||||
---
|
||||
|
||||
## 1. 질문 도구 표
|
||||
|
||||
기계적으로 판정한다. 세션의 도구 목록에 아래 이름의 구조화 질문 도구가 있으면 그것을 쓰고, 없으면 대화형 평문으로 번호 매긴 질문을 내고 턴을 끝낸다. "시스템 프롬프트가 자율 진행을 권한다"는 도구 유무 판정에 영향을 주지 않는다.
|
||||
|
||||
| 하네스 | 질문 도구와 한도 | 쓸 수 있는 조건 | 질문을 억누르는 압력 |
|
||||
|---|---|---|---|
|
||||
| Claude Code | `AskUserQuestion` — 질문 1~4개, 선택지 2~4개, header 12자, multiSelect, preview 지원 | 메인 세션에서만 호출할 수 있다. `Agent` 도구로 띄운 서브에이전트에서는 이 도구 자체가 제외되어 쓸 수 없다(공식 문서 명시). `context: fork` 스킬에서도 깨진다(공개 이슈 #19751, §9) | `auto` 모드가 Pro·Max·Team 플랜에서 기본 시작 모드다(2026-08-14 새 세션부터, macOS·Linux·WSL은 v2.1.228 이상, Windows 네이티브는 v2.1.233 이상 — 그 이전 버전은 기본이 Manual, §9). 공식 문서: "nudges Claude to keep working without stopping for clarifying questions, though Claude still asks when your prompt or a skill explicitly relies on it." 이 스킬은 §0(SKILL.md 인터뷰 게이트)에서 명시적으로 의존한다고 선언해 그 탈출구를 쓴다 |
|
||||
| Codex | `request_user_input` — 질문당 상호 배타적 선택지 2~3개, 추천 선택지에 "(Recommended)" 접미사, "Other"는 클라이언트가 자동으로 추가한다(직접 넣지 않는다) | Plan 모드에서 기본 허용된다. Default 협업 모드에서는 `default_mode_request_user_input` 기능 플래그가 켜져 있어야 한다. `codex exec`(비대화형)는 서버발 상호작용 요청을 전부 즉시 거부한다 — 질문 도구도, 실행 승인도 기다리지 않고 하드코딩된 거부 메시지로 응답한다 | 내장 프롬프트의 "Autonomy and Persistence" 절: 턴 안에서 끝까지 처리하라. Prompting Guide: "Bias to action … do not end your turn with clarifications unless truly blocked" |
|
||||
| Gemini CLI | `ask_user` — 질문 1~4개, choice·text·yesno 혼합, multiSelect | Plan 모드가 기본 활성이다. Plan을 벗어나면 YOLO 모드로 전환된다. YOLO 모드에서는 빈 답이 자동으로 통과되는 버그가 보고돼 있다(공개 이슈 #18540, §9) | 실행 단계로 자동 전환되는 것 자체가 질문 라운드를 건너뛰는 경로가 된다 |
|
||||
| VS Code Copilot | `askQuestions` — 질문 캐러셀 UI, 질문 사이 이동(Alt+N/Alt+P), 스크린리더용 위치 안내("Question 1 of 3"). v1.110(2026-02)에서 VS Code 코어로 들어왔다(§9) | agent 모드에서 메인 에이전트가 쓴다. 서브에이전트에는 질문 도구가 없다(공식 문서 명시) | 클라우드 에이전트는 비동기 실행이라 대화 자체가 없다 |
|
||||
| Cursor | 공식 문서는 "Agent asks clarifying questions to understand your requirements"라고만 설명할 뿐, 특정 이름의 전용 도구를 명시하지 않는다. **도구명은 미확인** — 평문 폴백을 기본으로 삼는다 | Plan Mode에서 하네스가 질문 흐름을 주입한다. Agent 모드에서는 이 흐름을 쓸 수 없다 | — |
|
||||
| Windsurf, Antigravity, Zcode, AGENTS.md 경로(그 외 하네스) | 공개 문서로 확인되는 구조화 질문 도구가 없다 | 채팅으로 직접 묻는 것은 가능하다 | Zcode의 일반 질문에 5분 타임아웃이 있다는 것은 2차 조사 결과이며 이번 확인 범위에서 1차 출처로 재확인하지 못했다 — 재확인 필요로 남긴다 |
|
||||
|
||||
도구가 없거나 확인되지 않는 행에서는 [brief-interview.md §6(c) 평문 폴백판](brief-interview.md)을 쓴다. "도구 이름을 모른다"는 질문을 생략할 근거가 아니다 — 평문으로 대체하고 턴을 끝낸다.
|
||||
|
||||
---
|
||||
|
||||
## 2. 질문 예산과 배치
|
||||
|
||||
| 하네스 | 방법 | 예산과 배치 |
|
||||
|---|---|---|
|
||||
| Claude Code | `AskUserQuestion` | 질문 4개: 업종·성격 / 목표 행동(multiSelect) / 톤(preview로 ASCII 목업 비교) / 브랜드 자산. 고유명사는 Other 자유 답이나 2라운드로 |
|
||||
| Codex(Plan 모드, 또는 Default + 플래그) | `request_user_input` | 질문 3개(업종·성격, 가장 중요한 목표 행동 하나, 톤), 선택지 2~3개. 선택지가 서로 배타적이라 나머지 목표 행동과 다른 슬롯은 같은 턴에 평문 한 블록 |
|
||||
| Codex(Default 모드, 도구 없음) | 평문 번호 질문 + 턴 종료 | 첫 응답에서 "긴 디자인 작업은 Plan 모드에서 시작하면 선택지 UI로 답할 수 있다"를 한 줄 안내 |
|
||||
| Gemini CLI | `ask_user` | 질문 4개, choice·text 혼합 |
|
||||
| VS Code Copilot | `askQuestions` | 질문 4개 |
|
||||
| Cursor | Plan 모드면 하네스가 주입한 질문 흐름, Agent 모드면 평문 | — |
|
||||
| 그 밖의 하네스, AGENTS.md 경로 | 평문 번호 질문(선택지는 알파벳, 자유 답 허용) + 턴 종료 | — |
|
||||
|
||||
배치 순서와 카드 형식은 [brief-interview.md §5·§6](brief-interview.md)을 따른다 — 결과를 크게 바꾸는 축(업종·성격, 목표 행동)을 톤·고유명사보다 앞에 낸다.
|
||||
|
||||
빈 답 처리도 여기서 정한다. 자동 해제로 들어온 빈 답(Codex의 autoResolution, Gemini의 YOLO)은 **무응답**으로 취급한다. §3의 가정 처리로 넘기고, 어떤 선택지를 고른 것으로 해석하지 않는다.
|
||||
|
||||
---
|
||||
|
||||
## 3. 비대화형·빈 답·서브에이전트
|
||||
|
||||
`claude -p`, `codex exec`, CI 실행은 구조화 질문 도구로 답을 기다릴 수 없는 실행이다. 이 절은 [SKILL.md 인터뷰 게이트](../SKILL.md) 규칙 3·5를 구체화한다.
|
||||
|
||||
1. **자동 해제로 들어온 빈 답은 무응답이다.** Codex의 autoResolution(60~240초 뒤 빈 답 자동 제출), Gemini CLI YOLO 모드의 자동 통과가 여기 해당한다. 이 빈 답을 특정 선택지로 해석하지 않는다.
|
||||
2. **한 번 실행하고 끝나는 실행도 평문으로 묻고 끝낸다.** `codex exec`는 `request_user_input`·실행 승인·패치 승인·권한 요청을 모두 하드코딩된 거부 메시지로 즉시 돌려보낸다(소스코드로 확인). 그러니 구조화 도구로 기다릴 수는 없다. 그래도 최종 응답에 브리프 카드 초안과 번호 질문을 남기고 파일을 만들지 않은 채 끝낼 수는 있다 — 답은 세션 재개나 다음 실행 프롬프트로 온다. 빈 브리프로 가정 위에 전체 디자인을 만드는 것은 사용자가 명시적으로 위임했을 때만이다([brief-interview.md §11](brief-interview.md)).
|
||||
3. **서브에이전트는 사용자에게 직접 질문할 수 없다는 것이 여러 하네스에서 공식적으로 확인된다.**
|
||||
- Claude Code: `AskUserQuestion`은 서브에이전트 도구 목록에서 명시적으로 제외된다.
|
||||
- VS Code Copilot: "The built-in tools for asking clarifying questions and managing todo items are unavailable to subagents"라고 명시한다.
|
||||
- Codex: 위임 스레드(`codex_delegate`)는 승인 요청 자체를 절대 보내지 않으며 `approval_policy`가 `never`여야 한다.
|
||||
- Gemini CLI, Windsurf, Antigravity, Zcode: 서브에이전트가 사용자에게 직접 질문할 수 있는지 공식 문서에 명시가 없다(미확인). 명시가 없다는 것을 "질문할 수 있다"로 해석하지 않는다 — 확인되지 않은 것은 안전한 쪽(질문 불가로 가정)으로 취급한다.
|
||||
|
||||
따라서 서브에이전트로 실행 중이면 0단계를 끝내려 하지 않는다. 브리프 카드 초안과 남은 질문을 호출자에게 반환하고 멈춘다. 0단계는 반드시 메인 세션에서 끝낸다.
|
||||
4. **오케스트레이터가 워커에게 디자인 구현을 넘길 때는 브리프 카드를 작업 패킷에 포함한다.** 워커가 다시 사용자에게 묻는 일이 없도록 확정된 슬롯과 미확정 가정을 함께 넘긴다(§5도 참고).
|
||||
|
||||
---
|
||||
|
||||
## 4. 계획·할 일 도구
|
||||
|
||||
하네스의 계획·할 일 도구로 0단계(브리프 게이트)부터 6단계(결정을 남긴다)까지 항목화하면 단계 보고와 연결하기 쉽다. 도구 이름과 가용성은 모델·버전에 따라 갈린다.
|
||||
|
||||
| 하네스 | 도구 | 주의 |
|
||||
|---|---|---|
|
||||
| Claude Code | `TaskCreate`/`TaskGet`/`TaskUpdate`/`TaskList`(Task 도구 4종). `CLAUDE_CODE_ENABLE_TASKS=0`이면 `TodoWrite`로 대체된다 | 이 기본값은 Claude Code v2.1.268 이상에 적용된다. Task 도구(및 TodoWrite)는 **Claude 3.x, Opus 4~4.7, Sonnet 4~4.6, Haiku 4.5에서만 기본 제공**된다. 그 외 모델(인식되지 않는 모델 ID 포함)에서는 `allowedTools` 지정이나 `CLAUDE_CODE_ENABLE_TODO_TOOLS=1`로 옵트인해야 쓸 수 있다 |
|
||||
| Codex | `update_plan`(체크리스트·진행상황·TODO 도구) | `update_plan`은 Plan 모드와는 **별개**다. Plan 모드 중에 `update_plan`을 쓰려고 하면 오류가 반환된다 — Plan 모드에서 계획을 추적하고 싶으면 Plan 모드 자체의 대화 흐름을 쓰고, `update_plan`은 Plan 모드를 벗어난 뒤(또는 Default 모드에서) 쓴다 |
|
||||
| Gemini CLI | `write_todos` | 다단계 요청의 내부 서브태스크 목록을 관리한다 |
|
||||
| VS Code Copilot | 서브에이전트 문서가 "할 일 관리 도구"의 존재는 간접 확인하지만, **정확한 공식 명칭은 미확인**이다 | 이름을 확정하지 않은 채 "할 일 도구가 있다"고만 안내한다 |
|
||||
| Zcode | Goal Mode(`/goal`, `/goal <objective>`, `/goal replace`, `/goal pause`, `/goal resume`, `/goal clear`) | 계획 나열이 아니라 **세션 목표**를 설정하는 도구다. 매 라운드 후 목표 달성 여부를 자동 검증하며, 검증은 변경된 파일·커맨드 출력·테스트 결과 같은 실제 증거를 요구하고 계획·체크리스트·그럴듯한 답변만으로는 인정하지 않는다. 0~6단계를 체크리스트로 나열하는 용도가 아니라 "이번 세션의 목표 = 6단계까지 완료"처럼 상위 목표를 거는 용도에 가깝다 |
|
||||
|
||||
**0~6단계 항목화 방법**: 도구가 있으면 단계마다 한 항목을 만든다(0 브리프 게이트, 1 레퍼런스 조사, 2 방향 결정, 3 디자인 토큰과 예산, 4 구현, 5 프리플라이트 감사, 6 결정을 남긴다). 국소·연장 경로에서 건너뛰는 단계는 항목을 만들지 않거나 "건너뜀 — 이유"로 바로 완료 처리한다. 도구가 없는 하네스에서는 항목화를 생략하고 텍스트로 단계 진행을 보고한다 — 항목화 자체가 게이트 조건은 아니다.
|
||||
|
||||
---
|
||||
|
||||
## 5. 서브에이전트
|
||||
|
||||
| 하네스 | 이름·호출 방식 | 제약 |
|
||||
|---|---|---|
|
||||
| Claude Code | `Agent`(v2.1.63 이전 이름 `Task`, 별칭으로 계속 동작) — 자동 위임, `@agent-<name>` 명시 호출, `claude --agent <name>`(세션 전체 지정) | 병렬 실행 가능. 세션당 동시 서브에이전트 20개 제한(`CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS`로 조정), 메인 대화 아래 중첩 최대 3단계. `AskUserQuestion` 사용 불가(§3) |
|
||||
| Codex | `multi_agent_v1`/`v2` 네임스페이스(spawn_agent 등, 기본값 비활성·설정으로 켬), 별도로 `codex_delegate`(승인 요청을 절대 보내지 않는 하위 스레드) | 동시 실행 개수 상한이 실제로 존재하는지, GA(정식) 상태인지는 1차 소스로 확인하지 못했다(미확인) — "다수 동시 실행이 무제한 보장된다"고 안내하지 않는다 |
|
||||
| Gemini CLI | 빌트인 4종(`codebase_investigator`, `cli_help`, `generalist`, `browser_agent`) — 자동 위임 또는 프롬프트 맨 앞 `@이름`으로 명시 지정. 커스텀 서브에이전트는 YAML frontmatter가 있는 `.md` 파일(`.gemini/agents/*.md` 또는 `~/.gemini/agents/*.md`) | 사용자에게 직접 질문 가능한지는 미확인(§3) |
|
||||
| VS Code Copilot | 서브에이전트 — 독립적인 조사·분석·리뷰 작업을 수행하고 메인 에이전트에 요약 보고, 대개 에이전트가 주도적으로 위임(agent-initiated) | 무상태(stateless), 후속 메시지 불가. 기본적으로 추가 서브에이전트를 스폰할 수 없다(활성화 가능). 중첩 최대 5단계. 요청 모델은 메인 모델의 비용 등급을 넘을 수 없다. 질문·할 일 도구 사용 불가(§3) |
|
||||
| Zcode | General-Purpose(모든 도구 접근) / Explore(읽기 전용, 파일 생성·수정·이동·삭제 불가) / 커스텀(베타) — 자동 또는 `@`로 수동 지정 | 서브에이전트는 다른 서브에이전트를 스폰할 수 없다. 세션 중간에 연결된 MCP 서버는 서브에이전트에 보이지 않는다. 사용자에게 직접 질문 가능한지는 미확인(§3) |
|
||||
| Windsurf/Cascade, Antigravity | Antigravity는 `/browser` 명령으로 부르는 Browser 서브에이전트를 공식 문서가 확인한다(§6). 그 밖의 서브에이전트 제약은 이번 확인 범위에서 1차 출처를 찾지 못했다(미확인) | — |
|
||||
|
||||
**설계 규칙**: 1단계(레퍼런스 조사)의 R1·R2·R3 실측은 서브에이전트를 지원하면 나눠 맡긴다. 단 **방향 결정과 판정은 항상 메인 세션이 한다** — 서브에이전트는 관찰과 원문 인용을 모아 올 뿐이다. **0단계(브리프 게이트)는 서브에이전트가 대신 끝낼 수 없다.** 위 표의 모든 하네스에서 서브에이전트는 사용자에게 직접 질문하는 경로가 없거나(확인됨) 미확인이므로, 미확인도 안전한 쪽(불가)으로 다룬다(§3-3).
|
||||
|
||||
**오케스트레이터 → 워커**: 디자인 구현을 서브에이전트·외부 워커에 넘길 때는 작업 패킷에 **브리프 카드**(브리프 세 줄, [brief-interview.md](brief-interview.md) §3 슬롯의 확정값, §12 형식의 가정 목록)를 포함한다. 브리프 카드가 없는 패킷은 워커가 사용자 대신 브리프를 다시 추측하게 만든다.
|
||||
|
||||
---
|
||||
|
||||
## 6. 브라우저·시각 도구
|
||||
|
||||
렌더 검증은 실제 브라우저 도구가 있으면 그것을 우선 쓴다. 증거 출처(에뮬레이션·실기기·엔진명)는 보고에 적는다 — "확인함"이라고만 쓰고 무엇으로 확인했는지 적지 않으면 재현할 수 없다.
|
||||
|
||||
| 도구 | 무엇을 하는가 |
|
||||
|---|---|
|
||||
| chrome-devtools MCP `lighthouse_audit` | 접근성·성능 카테고리 자동 감사(Lighthouse) |
|
||||
| chrome-devtools MCP `take_snapshot` | 접근성 트리(이름·역할·상태·랜드마크·헤딩) 스냅샷 |
|
||||
| chrome-devtools MCP `take_screenshot` | 실제 렌더 스크린샷 |
|
||||
| playwright MCP | 탐색·클릭·폼 입력·스크린샷·콘솔/네트워크 로그를 포함한 브라우저 자동화 |
|
||||
| Codex `view_image` | 로컬 파일시스템의 이미지 경로를 받아 시각적으로 확인한다(디스크에 이미 있는 이미지 전용 — 페이지를 직접 탐색하지 않는다) |
|
||||
| Gemini CLI `browser_agent` | 빌트인 서브에이전트로 브라우저 작업을 위임한다 |
|
||||
| Antigravity Browser 서브에이전트 | `/browser` 명령으로 호출하며 Chrome DevTools MCP와 네이티브로 통합되고 webm 녹화를 지원한다 |
|
||||
|
||||
**위 도구가 하나도 없으면** `designpaca tools`로 설치되는 headed 브라우저(방법은 `galleries.md` §5)와 `tools/design-gate.mjs`를 쓴다. design-gate.mjs가 하는 일과 한계는 [audit-gate.md](audit-gate.md)를 따른다 — 자동 검사는 계측이고 계측에는 공백이 있다는 원칙이 여기서도 그대로 적용된다.
|
||||
|
||||
---
|
||||
|
||||
## 7. 모드 안내
|
||||
|
||||
세션이 어떤 모드인지 한 줄로 먼저 확인한다.
|
||||
|
||||
- **Claude Code**: `auto` 모드는 "질문 없이 계속 진행"을 권하지만, 공식 문서 자체가 "스킬이 명시적으로 의존하면 묻는다"는 탈출구를 남겨 둔다. 이 스킬은 [SKILL.md 인터뷰 게이트](../SKILL.md)에서 그 의존을 명시적으로 선언했으므로, `auto` 모드에서도 0단계는 그대로 수행한다. Plan mode는 `EnterPlanMode`/`ExitPlanMode`(Shift+Tab 반복 또는 `--permission-mode plan`)로 켜며, §4의 계획 도구와 함께 쓰면 단계 진행을 사용자에게 보여주기 좋다.
|
||||
- **Codex**: 긴 디자인 작업은 Plan 모드로 시작하기를 권한다. Plan 모드에서는 `request_user_input`이 기본 허용되어 선택지 UI로 답할 수 있다. Default 모드는 기능 플래그가 없으면 질문 도구가 없다(§1).
|
||||
- **Gemini CLI**: Plan 모드가 기본이다. Plan을 벗어나 YOLO로 전환하면 질문 라운드와 승인 절차가 함께 약해진다는 것을 인지하고 진행한다.
|
||||
- **Cursor**: Plan 모드에서만 질문 흐름이 하네스에 의해 주입된다. Agent 모드에서 디자인 브리프 작업을 시작하게 되면 평문 폴백으로 0단계를 수행한다.
|
||||
|
||||
---
|
||||
|
||||
## 8. frontmatter는 하네스 공통 필드만 쓴다
|
||||
|
||||
이 스킬의 frontmatter는 `name`·`description`만 쓴다. Claude 전용 필드(`allowed-tools`, `context: fork`, 동적 명령 주입 `!command`)를 넣지 않는 이유는 다음과 같다.
|
||||
|
||||
1. **다른 하네스에서 무력화되거나 오작동한다.** Codex·Cursor·Gemini CLI 등은 이런 필드를 인식하지 않거나 다른 의미로 해석할 수 있다. 한 하네스에 맞춘 필드를 나머지 9개 설치 타깃에 그대로 복사하면, 해당 필드가 조용히 무시되거나(정보 손실 없이 끝나면 그나마 안전) 예상 밖으로 동작한다.
|
||||
2. **`context: fork`는 `AskUserQuestion`을 깨뜨린다.** 공개 이슈 #19751(§9)이 이를 확인한다. 이 스킬은 0단계 인터뷰에 명시적으로 의존하므로, 인터뷰 도구를 깨뜨릴 수 있는 필드는 애초에 후보가 아니다.
|
||||
3. **`allowed-tools`는 Claude Code 전용 권한 부여다.** 스킬을 호출한 턴 동안 특정 도구를 묻지 않고 쓰게 하는 필드인데, 다른 하네스에서는 의미가 없고 이 스킬은 특정 명령을 사전 허용할 필요가 없다.
|
||||
|
||||
---
|
||||
|
||||
## 9. 출처 표
|
||||
|
||||
| 주장 | URL | 확인일 |
|
||||
|---|---|---|
|
||||
| Claude Code Task 도구 4종·TodoWrite·모델별 가용성 | https://code.claude.com/docs/en/sdk/sdk-todo-tracking | 2026-09-24 |
|
||||
| Claude Code Plan mode(EnterPlanMode/ExitPlanMode) | https://code.claude.com/docs/en/common-workflows | 2026-09-24 |
|
||||
| Claude Code `Agent`(서브에이전트, 구 `Task`) 이름 변경·동시 실행 제한·중첩 깊이 | https://code.claude.com/docs/en/sub-agents | 2026-09-24 |
|
||||
| Claude Code `AskUserQuestion`과 서브에이전트 제외 | https://code.claude.com/docs/en/sub-agents | 2026-09-24 |
|
||||
| Claude Agent SDK `canUseTool`의 `AskUserQuestion` 처리(질문·응답 페이로드 형식) | https://code.claude.com/docs/en/agent-sdk/user-input | 2026-09-24 |
|
||||
| Claude Code `auto` 모드가 Pro·Max·Team 기본 시작 모드, 버전 조건(v2.1.228/v2.1.233), "nudges Claude to keep working…" 인용문 | https://code.claude.com/docs/en/permission-modes | 2026-09-24 |
|
||||
| Claude Code `auto` 모드 기본값 전환 날짜("Starting on August 14, new sessions on Pro, Max, and Team plans will run in auto mode.") | https://claude.com/blog/auto-mode-default-in-claude-code | 2026-09-24 |
|
||||
| Claude Code `context: fork` 스킬이 `AskUserQuestion`을 깨뜨리는 버그(이슈 #19751, Duplicate로 종료) | https://github.com/anthropics/claude-code/issues/19751 | 2026-09-24 |
|
||||
| Codex `update_plan`(Plan 모드와 별개, Plan 모드 중 오류) | https://raw.githubusercontent.com/openai/codex/main/codex-rs/collaboration-mode-templates/templates/plan.md | 2026-09-24 |
|
||||
| Codex `view_image` 도구 | https://raw.githubusercontent.com/openai/codex/main/codex-rs/core/src/tools/handlers/view_image_spec.rs | 2026-09-24 |
|
||||
| Codex 멀티에이전트 네임스페이스(v1/v2, 기본 비활성) | https://raw.githubusercontent.com/openai/codex/main/codex-rs/core/src/tools/handlers/multi_agents_spec.rs | 2026-09-24 |
|
||||
| Codex `codex_delegate`(승인 요청 없는 하위 스레드) | https://raw.githubusercontent.com/openai/codex/main/codex-rs/core/src/codex_delegate.rs | 2026-09-24 |
|
||||
| Codex `request_user_input`(선택지 2~3, 기능 플래그) | https://raw.githubusercontent.com/openai/codex/main/codex-rs/core/src/tools/handlers/request_user_input_spec.rs | 2026-09-24 |
|
||||
| Codex `codex exec` 비대화형 모드의 서버 요청 즉시 거부 | https://github.com/openai/codex/blob/main/codex-rs/exec/src/lib.rs | 2026-09-24 |
|
||||
| Gemini CLI `write_todos` | https://geminicli.com/docs/tools/todos/ | 2026-09-24 |
|
||||
| Gemini CLI 빌트인 서브에이전트 4종·호출 방식 | https://geminicli.com/docs/core/subagents/ | 2026-09-24 |
|
||||
| Gemini CLI YOLO 모드에서 `ask_user` 빈 답 자동 통과 버그(이슈 #18540, Closed) | https://github.com/google-gemini/gemini-cli/issues/18540 | 2026-09-24 |
|
||||
| Cursor Plan Mode 활성화, 전용 질문 도구명 미기재 | https://cursor.com/docs/agent/plan-mode | 2026-09-24 |
|
||||
| VS Code Copilot Plan mode 활성화 | https://code.visualstudio.com/docs/copilot/agents/planning | 2026-09-24 |
|
||||
| VS Code `askQuestions` 도구의 코어 편입(질문 캐러셀, Alt+N/Alt+P, 위치 안내) | https://code.visualstudio.com/updates/v1_110 | 2026-09-24 |
|
||||
| VS Code Copilot 서브에이전트 제약(질문·할 일 도구 사용 불가, 무상태, 중첩 5단계) | https://code.visualstudio.com/docs/agents/run/subagents | 2026-09-24 |
|
||||
| Zcode Goal Mode 명령 체계 | https://zcode.z.ai/en/docs/goal | 2026-09-24 |
|
||||
| Zcode 서브에이전트 종류·제약 | https://zcode.z.ai/en/docs/subagents | 2026-09-24 |
|
||||
| Antigravity `/plan`·Implementation Plan·Browser 서브에이전트 | https://antigravity.google/docs/features/ | 2026-09-24 |
|
||||
| Windsurf/Cascade 서브에이전트·투두 도구(1차 확인 실패, 미확인으로 남김) | https://docs.devin.ai/desktop/cascade/mcp | 2026-09-24 |
|
||||
| Gemini CLI Plan 모드 기본 활성·YOLO 전환 | https://raw.githubusercontent.com/google-gemini/gemini-cli/main/docs/cli/plan-mode.md | 2026-09-24 |
|
||||
| Gemini CLI `ask_user` | https://geminicli.com/docs/tools/ask-user/ | 2026-09-24 |
|
||||
| Codex Prompting Guide "Bias to action" | https://developers.openai.com/cookbook/examples/gpt-5/codex_prompting_guide | 2026-09-24 |
|
||||
| Codex 내장 프롬프트 "Autonomy and Persistence" | https://raw.githubusercontent.com/openai/codex/main/codex-rs/core/gpt_5_1_prompt.md | 2026-09-24 |
|
||||
| Codex `request_user_input` 자동 해제(autoResolution) 60~240초 | https://github.com/openai/codex/pull/27256 , https://github.com/openai/codex/pull/28235 | 2026-09-24 |
|
||||
|
||||
세부 인용문 일부는 `outputs/skill-intake/p0/facts.json`에, 나머지는 위 표의 URL 원문에 있다. 재확인할 때는 이 표의 URL을 다시 연다.
|
||||
222
packages/skill/references/icons.md
Normal file
222
packages/skill/references/icons.md
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
# 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`(토큰 밖 값이므로 실측 조정) |
|
||||
|
||||
```css
|
||||
/* 좋음: 라벨 웨이트에 맞춘 스트로크 */
|
||||
.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`로 구동한다.
|
||||
|
||||
```css
|
||||
.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; }
|
||||
```
|
||||
|
||||
```html
|
||||
<!-- 좋음: 자산 하나, 상태는 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](interaction-feel.md) §1과 motion.md §5 "접근성 — 타협 없음"과 같은 요구이고, `prefers-reduced-motion`에서는 코드 H와 같은 원칙(정적 단서는 유지)을 따르되 scale 기반 전환이라 transition 자체를 끈다 — 전환이 없어도 상태는 즉시 바뀌어 보여야 한다.
|
||||
|
||||
### 5-3. 전환 레시피 — 관찰 후보 (시작값, 실측 조정)
|
||||
|
||||
컨텍스트에 따라 나타나거나 사라지는 아이콘(호버로 드러나는 액션, 토글 전환)은 표시 여부를 토글하는 대신 `opacity`·`scale`로 전환한다.
|
||||
|
||||
```css
|
||||
.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로 고정) |
|
||||
|
||||
```css
|
||||
/* 방향에 의미가 있는 아이콘만 좌우 반전한다 */
|
||||
[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](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](component-systems.md)를 함께 본다.
|
||||
|
||||
---
|
||||
|
||||
## 10. 체크리스트
|
||||
|
||||
- [ ] 아이콘 세트가 프로젝트 기존 세트와 같다. 한 표면에 두 세트가 섞이지 않았다
|
||||
- [ ] 스트로크 굵기가 인접 텍스트의 웨이트 토큰과 맞거나, 세트 제약으로 못 맞추면 그 사실을 기록했다
|
||||
- [ ] 가장 작게 렌더되는 크기(보통 16px)에서 실제로 확인했다
|
||||
- [ ] 상태별 색이 `currentColor` + CSS로 구동되고, 상태마다 별도 자산이 없다
|
||||
- [ ] outline/filled 상태쌍이 있으면 일관되게 default/active로 쓰인다
|
||||
- [ ] 아이콘 전환이 있으면 색·상태 속성 같은 정적 단서가 함께 있고, `prefers-reduced-motion`에서 즉시 전환된다
|
||||
- [ ] 아이콘+텍스트 버튼·비대칭 아이콘의 광학 정렬을 실제 렌더에서 확인했다
|
||||
- [ ] RTL을 지원하는 프로젝트라면 방향성 아이콘만 뒤집었다
|
||||
- [ ] 아이콘 전용 버튼에 접근 가능한 이름이 있다(`accessibility.md`)
|
||||
- [ ] 아이콘을 문자열 키가 아니라 컴포넌트로 직접 참조했다
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
|
||||
이 순위는 기본 탐색 순서일 뿐이다. 자산 용도·품질·권리·진실성 판단보다 앞서지 않는다.
|
||||
|
||||
사용자가 이미 자산을 제공했거나 사용 가능한 사진이 없다고 명시했거나, `design.md`·저장소에서 확인할 수 있으면 다시 묻지 않는다. 기존 정보로 확인할 수 없고 자산 유무가 결정에 필요할 때만 **0단계에서 묻는다**: "쓸 수 있는 사진이 있나요?"
|
||||
사용자가 이미 자산을 제공했거나 사용 가능한 사진이 없다고 명시했거나, `design.md`·저장소 파일로 확인할 수 있으면 다시 묻지 않는다. 그 밖의 경우에는 **0단계 브리프 슬롯(기존 브랜드 자산)** 으로 반드시 묻는다: "쓸 수 있는 사진이 있나요?" — 자산 유무가 이번 화면 구성을 얼마나 바꿀지를 스스로 판단해 질문을 생략하지 않는다. 슬롯 확인·질문 카드는 [brief-interview.md](brief-interview.md)를 따른다.
|
||||
없다는 답만으로 생성 경로를 자동 선택하지 않는다. 슬롯별 자산 용도·품질·권리·진실성을 비교해 경로를 고르고, 생성물을 쓰면 **생성물이라는 사실을 `design.md` 에 적는다.**
|
||||
나중에 실제 사진으로 바꿀 가능성이 있으면 레이아웃을 다시 짜지 않도록 비율을 미리 고정해 둔다. 구도·크롭·타이포 검토는 [art-direction.md](art-direction.md)를 따른다.
|
||||
|
||||
|
|
@ -135,6 +135,34 @@ await sharp(src).resize({ width: w }).webp({ quality: 82 }).toFile(out);
|
|||
| 본문 폭 이미지 | 1100px |
|
||||
| 카드·썸네일 | 640px |
|
||||
|
||||
## 3-b. 화면별로 다른 크기를 보낸다 — srcset·sizes·picture
|
||||
|
||||
**프로젝트 계약 — 히어로처럼 화면 전체에 걸리는 이미지는 `srcset`으로 화면별 실제 크기를 보낸다.** §3에서 만든 여러 해상도 파일을 하나의 `<img>`에 폭 서술자(`w`)로 나열하고, `sizes`로 뷰포트별 실제 표시 폭을 알려준다. 브라우저가 다운로드 시점에 화면 폭·픽셀 밀도에 맞는 파일을 고른다.
|
||||
|
||||
```html
|
||||
<img
|
||||
srcset="hero-640.webp 640w, hero-1100.webp 1100w, hero-1600.webp 1600w"
|
||||
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 90vw, 1600px"
|
||||
src="hero-1100.webp"
|
||||
width="1600" height="900" alt="..." />
|
||||
```
|
||||
|
||||
`sizes`는 CSS 레이아웃이 실제로 이미지에 주는 폭과 일치해야 한다. 전체폭 배경은 `100vw`에 가깝고, 그리드 안의 카드 이미지는 그리드 열 폭을 반영한다 — 대충 적으면 브라우저가 필요 이상으로 큰 파일을 내려받는다 [SKILL-IMPECCABLE].
|
||||
|
||||
**모바일과 데스크톱에서 피사체 자체가 다르게 잘려야 하면(art direction) `<picture>`로 분기한다.** `srcset`은 같은 그림의 해상도만 바꾼다. §2의 이미지 바이블에서 모바일 히어로를 별도 원본으로 정했다면 `<source>`로 명시적으로 분기한다. 각 `<source>`에도 `sizes`를 함께 준다 — 없으면 브라우저가 `100vw`로 가정해 필요 이상으로 큰 파일을 고를 수 있다.
|
||||
|
||||
```html
|
||||
<picture>
|
||||
<source media="(max-width: 640px)" srcset="hero-mobile-750.webp 750w, hero-mobile-1080.webp 1080w" sizes="100vw">
|
||||
<source media="(min-width: 641px)" srcset="hero-1100.webp 1100w, hero-1600.webp 1600w" sizes="(max-width: 1024px) 90vw, 1600px">
|
||||
<img src="hero-1100.webp" width="1600" height="900" alt="..." />
|
||||
</picture>
|
||||
```
|
||||
|
||||
`<picture>`는 항상 대체용 `<img>`로 닫는다 — 어떤 `<source>`도 조건에 맞지 않을 때의 폴백이자, `alt`·`width`·`height`가 실제로 적용되는 자리다 [SKILL-IMPECCABLE].
|
||||
|
||||
---
|
||||
|
||||
## 4. `<img>` 에 `width`/`height` 를 적었다면 `height: auto` 를 반드시 줘라
|
||||
|
||||
CLS 를 막으려고 속성을 적는 것은 맞다. 그런데 CSS 에서 높이를 정하지 않으면
|
||||
|
|
@ -159,6 +187,36 @@ img { max-width: 100%; height: auto; } /* 이 한 줄이 없으면 */
|
|||
|
||||
## 5. `alt` 는 장식이 아니면 반드시 쓴다
|
||||
|
||||
**하드 게이트 — 이미지의 용도를 다섯 가지로 나누고 각각 다르게 쓴다.** 장식과 비장식을 한 덩어리로 다루면 정보성·기능성·텍스트 이미지가 각각 요구하는 다른 문장을 놓친다 [SKILL-BETTER-A11Y].
|
||||
|
||||
| 분류 | 무엇인가 | `alt`에 적는 것 | 예시 |
|
||||
|---|---|---|---|
|
||||
| 장식 | 배경 텍스처·그레인처럼 정보가 없는 순수 시각 장식 | `alt=""` + `aria-hidden="true"` | 헤더 뒤 그라디언트 패턴 |
|
||||
| 정보성 | 내용을 전달하는 사진·일러스트 | 무엇이 보이는지 서술 | `alt="작업대 위에 놓인 마른 꽃 다발과 가위"` |
|
||||
| 기능성 | 클릭·이동 같은 동작을 일으키는 이미지(아이콘 버튼, 링크가 된 로고) | 이미지 묘사가 아니라 **행동**을 서술 | `alt="장바구니로 이동"` |
|
||||
| 텍스트 이미지 | 이미지 안에 글자가 박혀 있다(로고, 스캔한 포스터) | 이미지 속 **텍스트를 그대로** 옮긴다 | `alt="2026 가을 컬렉션"` |
|
||||
| 복합 이미지 | 차트·인포그래픽·스크린샷처럼 요약이 필요하다 | 짧은 요약을 `alt`에 쓰고, 상세 값은 본문 표·목록으로 따로 둔다 | `alt="최근 3개월 방문자 추이, 9월에 40% 증가"` + 본문 수치 표 |
|
||||
|
||||
- 내용을 나르는 이미지 → **무엇이 보이는지** 적는다. "이미지", "사진" 은 alt 가 아니다
|
||||
- 순수 장식(배경 텍스처·그레인) → `alt=""` + `aria-hidden="true"`
|
||||
- 캡션이 이미 설명하고 있으면 alt 는 짧게. 같은 문장을 두 번 읽히지 않는다
|
||||
|
||||
**하드 게이트 — `alt` 속성 자체를 빼먹는 것은 빈 `alt=""` 보다 나쁘다.** 스크린리더는 `alt` 속성이 없는 `<img>`를 만나면 파일 경로나 파일명을 대신 읽는다. 빈 `alt=""`는 "건너뛰어도 된다"는 의도된 신호지만, 속성 누락에는 그 의도가 없어 스크린리더가 의미 없는 문자열을 그대로 읽는다 — 아무것도 전달하지 않는 것보다 나쁘다 [SKILL-BETTER-A11Y].
|
||||
|
||||
---
|
||||
|
||||
## 6. 이미지 1px 중립 외곽선 — 관찰 후보
|
||||
|
||||
밝은 배경 위의 흰 계열 사진, 어두운 배경 위의 검정 계열 사진은 경계가 배경에 녹아 사라진다. 색조 없는 중립 1px 외곽선을 후보로 검토한다.
|
||||
|
||||
```css
|
||||
img {
|
||||
outline: 1px solid oklch(0 0 0 / 0.1);
|
||||
outline-offset: -1px;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
img { outline-color: oklch(1 0 0 / 0.1); }
|
||||
}
|
||||
```
|
||||
|
||||
`border` 대신 `outline`을 쓰는 이유는 레이아웃 폭에 영향을 주지 않기 때문이다 — 이미지 표시 크기가 그대로 유지된다. slate·zinc 계열처럼 색조가 있는 중성색은 피한다. 사진 표면 위에서 "때"처럼 보이기 쉽다. 이 값은 시작값이며, 사진이 이미 카드 테두리·그림자 같은 뚜렷한 경계를 갖고 있으면 불필요하다 [SKILL-BETTER-UI].
|
||||
|
|
|
|||
326
packages/skill/references/interaction-feel.md
Normal file
326
packages/skill/references/interaction-feel.md
Normal file
|
|
@ -0,0 +1,326 @@
|
|||
# interaction-feel — 입력 축의 촉감
|
||||
|
||||
4-4에서 [motion.md](motion.md)와 함께 읽는다. `motion.md`는 **시간 축**을 다룬다 — 입력이 없어도 흐르는 움직임, duration·이징 토큰, 스크롤·페이지 전환. 이 문서는 **입력 축**을 다룬다 — 사용자가 누르고 끌고 기다리는 동안 화면이 무엇을 돌려주는가다. 겹치는 지점(눌림 트랜지션, hold-to-confirm의 clip-path)에서는 `motion.md`의 코드를 그대로 가리키고 여기서 다시 정의하지 않는다. `--dur-*`·`--ease-*` 토큰도 `tokens.md` §4가 정본이며, 이 문서는 새 토큰을 만들지 않는다.
|
||||
|
||||
## 이 문서를 읽는 법
|
||||
|
||||
| 상황 | 읽을 곳 |
|
||||
|---|---|
|
||||
| 입력에 반응이 굼뜨다는 지적을 받았다 | §0 |
|
||||
| hover·focus·active 같은 상태를 정리해야 한다 | §1 |
|
||||
| 버튼·카드 누름 피드백만 빨리 정하면 된다 | §2 |
|
||||
| 직접 드래그·슬라이더·캐러셀을 만든다 | §3 |
|
||||
| 애니메이션 도중 다시 조작하면 끊긴다 | §4 |
|
||||
| 스와이프·던지기·바텀시트 스냅을 구현한다 | §5 |
|
||||
| 스프링 라이브러리를 쓰거나 스프링감을 흉내 낸다 | §6 |
|
||||
| 삭제·파괴적 동작의 확인 UI를 고른다 | §7 |
|
||||
| 토스트·토글·스켈레톤 같은 흔한 컴포넌트를 만든다 | §8 |
|
||||
| 사운드·햅틱을 쓸지 고민한다 | §9 |
|
||||
| 모바일 폭에서 주요 컨트롤 위치를 정한다 | §10 |
|
||||
| 커스텀 제스처 컴포넌트를 감사한다 | §11 |
|
||||
| 구현 직전 확인 | §12 |
|
||||
|
||||
---
|
||||
|
||||
## 0. 반응성
|
||||
|
||||
**포인터다운은 화면 어딘가를 즉시 바꿔야 한다.** 하이라이트든 프레스 스케일이든, 사용자가 누른 순간과 화면이 반응하는 순간 사이에 빈 시간이 있으면 그 인터페이스는 무겁게 느껴진다. 이 원칙 자체는 `motion.md` §0의 "피드백" 역할과 같지만, 여기서는 **지연의 출처**를 감사 대상으로 삼는다.
|
||||
|
||||
**프로젝트 계약 — 입력 경로 지연 감사.** `tokens.md` §5 성능 예산에 다음을 포함해 확인한다. 필수가 아닌 지연은 전부 제거 대상이다 [SKILL-APPLE-DESIGN].
|
||||
|
||||
- **디바운스**: 사용자가 누른 것을 그대로 보여주지 않고 지연시키는 디바운스가 입력 경로 위에 있는가(검색어 자동완성처럼 디바운스가 필요한 곳과, 단순 클릭처럼 필요 없는 곳을 구분한다)
|
||||
- **인위적 타이머**: `setTimeout`으로 "그럴듯해 보이려고" 넣은 지연이 있는가(가짜 로딩 등)
|
||||
- **전환 대기**: 이전 트랜지션이 끝나야 다음 입력을 받는 코드가 있는가 — 이는 §4 인터럽트 가능성이 막는 문제와 같다
|
||||
- **탭 지연**: `touch-action: manipulation`으로 더블탭 줌 판정 지연을 제거했는가(`mobile-app-ux.md`가 이미 전역 적용을 권한다)
|
||||
|
||||
**관찰 후보 — 체감 성능은 실제 로딩 시간과 다른 축이다.** 같은 대기 시간이라도 스피너 등장이 빠르면 더 짧게 느껴지고, 연속 조작(셀렉트 옵션 전환 등)은 트랜지션 자체를 줄이면 반응이 가벼워진다. 이징이 이 지각을 증폭시킨다는 점도 염두에 둔다 — 도착이 급정거처럼 보이는 이징은 빠른 반응도 무겁게 만든다 [SKILL-EMIL-DESIGN-ENG].
|
||||
|
||||
---
|
||||
|
||||
## 1. 상태 매트릭스
|
||||
|
||||
인터랙티브 요소는 여러 상태를 동시에 가진다. 상태마다 시각 신호·모션·접근성 속성을 따로 정해야 어느 하나가 비어도 사용자가 상태를 잃지 않는다.
|
||||
|
||||
**하드 게이트 — 모션은 유일한 신호가 되면 안 된다.** `prefers-reduced-motion`에서 모션이 꺼졌을 때 그 상태를 알려줄 정적 단서(색·아이콘·라벨·테두리)가 남아 있어야 한다. 모션이 유일한 신호였다면 감소 모션 사용자에게는 상태 자체가 사라진다 — `motion.md` §5 "접근성 — 타협 없음"과 같은 이유다 [SKILL-BETTER-UI].
|
||||
|
||||
| 상태 | 시각 신호 | 모션 | 접근성 속성 |
|
||||
|---|---|---|---|
|
||||
| hover | 배경·테두리·밝기 변화 | `--dur-instant`, `--ease-out` | **`@media (hover: hover) and (pointer: fine)` 안에서만 적용한다.** coarse 포인터(터치)에 hover 스타일을 걸면 탭 이후 눌린 채로 고정된 것처럼 보인다(`mobile-app-ux.md`의 실측 사고와 같은 부류) [SKILL-EMIL-DESIGN-ENG] |
|
||||
| focus-visible | 포커스 링, 애니메이션 없이 즉시 | 없음 — 즉시 나타나고 즉시 사라진다 | `:focus-visible`만 쓰고 `:focus` 단독은 쓰지 않는다. 상세 판정 기준과 대비 요건은 [accessibility.md](accessibility.md) |
|
||||
| active(누름) | §2 프레스 피드백 | `--dur-instant` | 포커스 링과 겹칠 때 순서는 프레스가 위, 링은 즉시 |
|
||||
| disabled | 낮은 대비·커서 `not-allowed` | 없음 | `disabled`와 `aria-disabled`의 구분, 제출 버튼을 조기에 비활성화하지 않는 규칙은 [accessibility.md](accessibility.md) |
|
||||
| loading | 스켈레톤 또는 스피너(§8) | 진입은 있어도 루프는 작은 면적만 | 진행 알림은 `role="status"`/라이브 리전 — [accessibility.md](accessibility.md) |
|
||||
| selected | 배경·체크 표시·`aria-current`/`aria-selected` | 상태 전환만, 반복 없음 | 색만으로 표시하지 않는다(선택 안 된 항목과의 구분 신호를 색 외에도 둔다) |
|
||||
| error | 테두리·아이콘·문구 동시 | 검증 실패 시 짧은 흔들림은 **관찰 후보**(과하면 산만함) | `aria-invalid`+`aria-describedby` 짝, 타이밍은 [accessibility.md](accessibility.md) |
|
||||
|
||||
---
|
||||
|
||||
## 2. 누름 피드백
|
||||
|
||||
**관찰 후보 — 시작값 `scale(0.96)`~`scale(0.98)`, `2~4%`를 넘기지 않는다.** duration은 새 리터럴을 만들지 않고 `tokens.md` §4가 이미 정한 `--dur-instant`(100ms)에 맞춘다 — 출처 스킬마다 `0.96`·`0.97`·`0.98`·`150ms`로 조금씩 다르지만 그 차이는 실질적이지 않다(이미 `tokens.md` §4가 이 매핑을 확정했다) [SKILL-APPLE-DESIGN][SKILL-BETTER-UI].
|
||||
|
||||
```css
|
||||
.btn:active { scale: 0.97; transition: scale var(--dur-instant) var(--ease-out); }
|
||||
```
|
||||
|
||||
**정적 토글 패턴.** 프레스 스케일을 컴포넌트 단위로 끌 수 있는 불리언 옵션(예: `static`)을 둔다. 전역 감소 모션 설정과는 별개로, 밀도 높은 데이터 테이블의 행 버튼처럼 프레스 모션이 소음이 되는 맥락에서 개별적으로 끄기 위함이다 [SKILL-BETTER-UI].
|
||||
|
||||
버튼·링크의 실제 CSS 레시피(밝기·그림자를 `opacity` 레이어로 바꾸는 것까지 포함한 전체 코드)는 `motion.md` 코드 A를 그대로 쓴다. 여기서 다시 적지 않는다.
|
||||
|
||||
---
|
||||
|
||||
## 3. 직접 조작
|
||||
|
||||
드래그 가능한 요소는 손가락·포인터를 **1:1로 그대로 따라가야** 한다. 배율을 걸거나 지연시키면 "끌려온다"는 느낌이 사라진다.
|
||||
|
||||
```js
|
||||
el.addEventListener('pointerdown', (e) => {
|
||||
if (isDragging) return; // 관찰 후보 — 멀티터치 가드. 추가로 들어오는 터치를
|
||||
// 무시하지 않으면 손가락을 바꿔 쥘 때 요소가 점프한다 [SKILL-EMIL-DESIGN-ENG]
|
||||
isDragging = true;
|
||||
el.setPointerCapture(e.pointerId); // 포인터가 요소 경계를 벗어나도 트래킹을 유지한다
|
||||
grabOffset = e.clientY - el.getBoundingClientRect().top; // 잡은 지점을 보존한다 — 중심으로 스냅하지 않는다
|
||||
startTracking(e);
|
||||
});
|
||||
```
|
||||
|
||||
**관찰 후보 — 히스테리시스 시작값 약 10px.** `pointerdown` 즉시 하이라이트는 켜되, 실제 드래그로 판정하기까지 약 10px(실측 조정)의 이동 여유를 둔다. 그보다 작은 떨림은 탭으로 처리한다 [SKILL-APPLE-DESIGN].
|
||||
|
||||
**병렬 제스처 인식.** 같은 축을 두고 경쟁하는 제스처가 있으면(세로 스크롤 vs 카드 가로 스와이프) 포인터다운 시점에는 아직 승자를 정하지 않는다. 초기 이동 벡터로 어느 쪽이 의도된 제스처인지 가른 뒤 패자 쪽 제스처를 취소한다 [SKILL-APPLE-DESIGN].
|
||||
|
||||
**터치 표면 스코프.** `mobile-app-ux.md`가 이미 전역에 건 `touch-action: manipulation`과 별개로, 자체 드래그·줌을 구현하는 표면에는 `touch-action: none`을 그 요소에만 스코프해 적용한다 — 전역에 걸면 페이지 스크롤 자체가 막힌다 [SKILL-BETTER-A11Y].
|
||||
|
||||
**하드 게이트 — 드래그 전용 기능에는 단일 포인터 대체 조작이 있어야 한다** [WCAG-DRAG]. 구체적인 대체 조작 레시피는 §8 스와이프·당겨서 새로고침을 본다.
|
||||
|
||||
---
|
||||
|
||||
## 4. 인터럽트 가능성
|
||||
|
||||
WWDC 2018 *Designing Fluid Interfaces*가 제시하는 단일 최우선 원칙이다 [WWDC-FLUID][SKILL-APPLE-DESIGN].
|
||||
|
||||
**관찰 후보 — 전환 도중 입력을 잠그지 않는다.** 사용자가 애니메이션 중간에 다시 잡으면(재드래그, 다시 클릭), 새 애니메이션은 목표값이 아니라 **현재 렌더된 값(presentation value)**에서 시작한다. 방향을 반전할 때도 속도를 하드컷 없이 블렌드한다 — 순간 정지 후 반대로 출발하면 "벽에 부딪힌" 느낌을 준다.
|
||||
|
||||
이 원칙은 CSS 메커니즘 선택과 바로 연결된다. `@keyframes`는 인터럽트되면 처음부터 재시작하지만, `transition`은 중단된 지점에서 새 목표로 **재조준(retarget)**한다. 그래서 **재트리거될 수 있는 모든 UI(토스트 추가, 재드래그, 빠르게 토글되는 상태)는 키프레임 대신 트랜지션을 쓴다** [SKILL-EMIL-DESIGN-ENG].
|
||||
|
||||
2D로 움직이는 요소는 X/Y를 독립된 스프링으로 분해한다. 한 축만 반전돼도 다른 축의 진행이 깨지지 않는다.
|
||||
|
||||
가역적 전환(뒤로 가기처럼 같은 경로를 반대로 도는 것)은 이징도 미러링한다 — 나가는 경로에 쓴 커브의 대칭점을 들어오는 경로에 쓴다. `motion.md` 코드 D의 "팝오버는 클릭한 곳에서 나와야 한다"(트리거 위치를 `transform-origin`으로 삼아 전환의 시작점과 도착점을 대칭으로 맞추는 원칙, motion.md §"코드 D — 모달·팝오버 진입/퇴장")와 같은 계열이다 [SKILL-APPLE-DESIGN].
|
||||
|
||||
---
|
||||
|
||||
## 5. 던지기 물리
|
||||
|
||||
스와이프·드래그를 놓았을 때(release) 무엇으로 착지시킬지 정하는 절차다. 여기 나오는 수치는 전부 **시작값**이며 실측 조정이 필요하다.
|
||||
|
||||
### 5-1. 속도 인계
|
||||
|
||||
```
|
||||
relativeVelocity = gestureVelocity / (targetValue − currentValue)
|
||||
```
|
||||
|
||||
릴리즈 속도를 다음 애니메이션의 초기 속도로 넘긴다. 일부 스프링 API는 남은 거리로 정규화한 **상대 속도**를 요구하고, 다른 라이브러리는 px/s 절대 속도를 그대로 받는다 — 라이브러리 문서에서 velocity 파라미터의 단위를 반드시 먼저 확인한다 [SKILL-APPLE-DESIGN].
|
||||
|
||||
### 5-2. 모멘텀 투사
|
||||
|
||||
```js
|
||||
// decelerationRate ≈ 0.998(일반 스크롤 감각), 0.99(더 스내피한 느낌) — 시작값, 실측 조정
|
||||
function project(releaseVelocity /* px/s */, decelerationRate = 0.998) {
|
||||
return (releaseVelocity / 1000) * decelerationRate / (1 - decelerationRate);
|
||||
}
|
||||
|
||||
const projectedEndpoint = currentPosition + project(releaseVelocity);
|
||||
const target = nearestSnapPoint(projectedEndpoint); // 릴리즈 지점이 아니라 "투사된 종착점" 기준으로 스냅
|
||||
animateSpringTo(target, { velocity: releaseVelocity }); // 5-1의 속도를 그대로 인계한다
|
||||
```
|
||||
|
||||
**왜 교과서식 `v²/(2·decel)` 공식이 맞지 않는가.** 그 공식은 등가속도(감속도가 일정함)를 전제한다. 위 감쇠 모델은 매 프레임 속도에 상수 비율(`decelerationRate`)을 곱하는 **지수 감쇠**다 — 감속도가 속도에 비례해 줄어든다. 그래서 등가속도 공식이 아니라 기하급수 합으로 총 이동 거리가 정해진다 [SKILL-APPLE-DESIGN].
|
||||
|
||||
### 5-3. 러버밴드(경계 저항)
|
||||
|
||||
```js
|
||||
// overshoot: 경계를 넘은 거리, dimension: 드래그 가능한 표면의 크기, k=0.55(시작값)
|
||||
function rubberband(overshoot, dimension, k = 0.55) {
|
||||
return (overshoot * dimension * k) / (dimension + k * Math.abs(overshoot));
|
||||
}
|
||||
```
|
||||
|
||||
경계를 하드 클램프하는 대신, 넘어갈수록 저항이 커지는 점진적 저항을 쓴다. "실제 물체는 멈추기 전에 먼저 느려진다"는 감각이다 [SKILL-APPLE-DESIGN].
|
||||
|
||||
### 5-4. dismiss 임계값
|
||||
|
||||
```js
|
||||
const velocity = Math.abs(dragDistance) / elapsedTime; // px/ms
|
||||
if (Math.abs(dragDistance) >= DISTANCE_THRESHOLD || velocity > VELOCITY_THRESHOLD) {
|
||||
dismiss();
|
||||
}
|
||||
```
|
||||
|
||||
**관찰 후보 — 매직넘버, 반드시 실측 조정한다.** 참고 시작값: 카드형 스와이프 삭제는 거리 임계값을 카드 폭의 `25~30%`(또는 `100px`) 정도로 두고[SKILL-INTERACTION-DESIGN], 속도 임계값은 빠른 플릭 하나로도 거리 조건 없이 dismiss되도록 별도로 잡는다. 외부 출처(emil-design-eng)는 속도 임계값 약 `0.11`을 제시하지만 단위를 밝히지 않아(px/ms로 추정될 뿐) 그대로 옮기지 않는다[SKILL-EMIL-DESIGN-ENG]. 실제 기기에서 빠른 플릭이 거리 조건 없이도 통과하는지 확인한다.
|
||||
|
||||
---
|
||||
|
||||
## 6. 스프링
|
||||
|
||||
> 언제: 브리프·0단계 인터뷰([brief-interview.md](brief-interview.md))에서 스프링감(경쾌함·플레이풀함)이 톤으로 선택됐을 때, 또는 프로젝트가 이미 스프링 기반 라이브러리를 쓰고 있을 때만 연다. 절제된 톤에서는 스프링을 쓰지 않는다.
|
||||
|
||||
같은 "스프링감"을 세 가지 표기 체계가 서로 다른 언어로 규정한다. 병기하고, 프로젝트가 이미 쓰는 라이브러리가 있으면 그 표기를 우선한다.
|
||||
|
||||
| 표기 | 파라미터 | 주로 쓰는 곳 | 값(관찰 후보·시작값) |
|
||||
|---|---|---|---|
|
||||
| damping ratio + response | `damping`, `response`(초) | Apple 계열 모션 API | 기본 UI: `damping 1.0`(임계감쇠, 오버슈트 없음), `response 0.3~0.4s`. 모멘텀 제스처(플릭·던지기) 뒤에만: `damping ~0.8` [SKILL-APPLE-DESIGN] |
|
||||
| stiffness + damping + mass | `stiffness`, `damping`, `mass` | 전통적 물리 기반 스프링 라이브러리 | `default 170/26/1`, `gentle 120/14/1`, `wobbly 180/12/1`, `stiff 210/20/1`, `slow 280/60/1`, `molasses 280/120/1` [SKILL-INTERACTION-DESIGN] |
|
||||
| duration + bounce | `duration`(초), `bounce`(-1~1) | Apple 방식을 옮긴 Motion API | UI 전반 `duration 0.3~0.5`, `bounce`는 `0.1~0.3`로 은은하게 — 대부분의 UI 맥락에서는 `bounce 0`(오버슈트 없음)이 기본이고, drag-to-dismiss나 장난스러운 인터랙션에만 올린다 [SKILL-EMIL-DESIGN-ENG] |
|
||||
|
||||
**표기 사이의 변환식.** 감쇠 조화 진동자의 표준 관계식이다. stiffness `k`, damping `c`, mass `m`일 때:
|
||||
|
||||
```
|
||||
dampingRatio ζ = c / (2 * sqrt(k * m)) # 1 = 임계감쇠(오버슈트 없음), 1 미만 = 바운스
|
||||
response(초) = 2π * sqrt(m / k) # 감쇠가 없을 때 한 주기. duration+bounce 표기의 duration 과 같은 뜻으로 쓴다
|
||||
bounce = 1 - ζ # ζ ≤ 1 일 때. ζ > 1(과감쇠)은 음수 bounce 로 표기하는 라이브러리가 있으니 문서를 확인한다
|
||||
```
|
||||
|
||||
예: `default 170/26/1` → ζ ≈ 1.0, response ≈ 0.48초(바운스 거의 없음). `wobbly 180/12/1` → ζ ≈ 0.45, response ≈ 0.47초. 라이브러리마다 `duration` 정의가 조금씩 다르므로 변환 후 실제 렌더로 확인한다.
|
||||
|
||||
외부 출처가 함께 제시한 `duration = sqrt(stiffness) / damping` 근사식은 위 관계식과 맞지 않아(예: `280/60/1` 이 0.28초로 나온다) 옮기지 않는다 [SKILL-INTERACTION-DESIGN]. cubic-bezier 로 흉내 내는 방법(`bounce > 0` 이면 `cubic-bezier(0.34, 1.56, 0.64, 1)` 처럼 끝값을 넘기는 곡선)은 한 번의 오버슈트만 표현하는 거친 근사다 — 여러 번 흔들리는 스프링은 아래 `linear()` 로 근사한다.
|
||||
|
||||
**CSS `linear()`로 라이브러리 없이 근사한다.** `motion.md` §3이 이미 "무한 루프, `linear()` 스프링 근사"를 CSS 네이티브로 분류해 둔 것과 같은 방법이다 — damping/response 값을 표본점 배열로 뽑아 `animation-timing-function: linear(...)`에 채운다. 표본값은 프로젝트마다 도구로 생성하는 것이지 여기서 고정값을 두지 않는다.
|
||||
|
||||
**톤과 연결한다.** 플레이풀·장식적 톤일 때만 스프링에 바운스를 허용하고, 절제된 톤(스위스·럭셔리 계열 등)에서는 스프링 자체를 쓰지 않는다 [SKILL-EMIL-DESIGN-ENG].
|
||||
|
||||
**감소 모션에서는 바운스가 없다.** `prefers-reduced-motion`에서는 오버슈트·바운스를 모두 제거하고 임계감쇠(`damping 1.0`/`bounce 0`)로만 전환한다 — `motion.md` 코드 H와 같은 원칙이다.
|
||||
|
||||
---
|
||||
|
||||
## 7. 확인과 되돌림
|
||||
|
||||
**되돌리기가 확인보다 낫다.** 흐름이 끊기지 않기 때문이다(`preflight.md`의 "파괴적 행동에는 되돌림이 있다" 원칙과 같다). 확인 대화상자는 되돌리기 UI를 만들 수 없을 때의 차선책으로만 쓴다.
|
||||
|
||||
> 언제: 삭제처럼 파괴적이고 즉시 실행되는 동작에, 되돌리기 UI가 마땅치 않을 때만 hold-to-confirm을 고려한다.
|
||||
|
||||
**hold-to-confirm의 비대칭 타이밍.** 누르는 동작(사용자가 결정을 내리는 중)은 느리게, 놓는 반응(시스템이 응답하는 순간)은 항상 빠르게 — 결정에는 시간을, 반응에는 즉시성을 준다는 원칙이다. clip-path 오버레이 자체의 CSS 레시피(누르는 동안 채워지는 애니메이션, 놓았을 때의 스냅백)는 `motion.md` 코드 C-1("clip-path를 애니메이션할 때")을 그대로 쓴다. 프레스 피드백은 §2의 `scale(0.96)`~`0.98)`을 함께 건다 [SKILL-EMIL-DESIGN-ENG].
|
||||
|
||||
**2단계 확인(재클릭)은 대안일 뿐 기본값이 아니다.** designpaca는 이미 "되돌리기 우선"이라는 더 엄격한 원칙을 갖고 있다. N초 안에 다시 눌러야 확정되는 패턴은 되돌리기 UI가 정말 불가능할 때의 대안으로만 쓴다 [SKILL-INTERACTION-DESIGN].
|
||||
|
||||
**하드 게이트 — 실패한 요청을 성공 상태로 남기지 않는다**(SKILL.md 규범·기능 하드 게이트 5).
|
||||
|
||||
**낙관적 업데이트와 롤백 알림.** 좋아요·북마크·투표처럼 위험이 낮고 즉시 반응이 필요한 토글은 낙관적으로 먼저 반영하고 실패 시 되돌린다. 롤백은 §8 토스트로 알리고, 판정은 `preflight.md`의 "판정은 순수 함수다" 원칙에 따라 원래 검증 함수로 다시 심사한다 — UI 핸들러가 임의로 판단하지 않는다 [SKILL-INTERACTION-DESIGN].
|
||||
|
||||
---
|
||||
|
||||
## 8. 마이크로 레시피
|
||||
|
||||
### 토스트
|
||||
|
||||
**하드 게이트 — 상태 메시지는 포커스를 가져가지 않고 알려야 한다** [WCAG-STATUS]. 성공·정보 메시지는 `role="status"`, 오류는 `role="alert"`로 실질적인 라이브 리전을 만든다(자세한 라이브 리전 선택 기준은 [accessibility.md](accessibility.md) §7).
|
||||
|
||||
- 자동 소멸은 저위험 확인에만 쓰고, 쓴다면 **5초 이상**이 바닥값이다([accessibility.md](accessibility.md) §7) — 메시지 길이에 따라 연장하고, hover·focus 중에는 타이머를 정지한다. 외부 출처의 `3000ms`는 이 바닥값보다 짧아 옮기지 않는다 [SKILL-BETTER-A11Y][SKILL-INTERACTION-DESIGN]
|
||||
- **행동(실행취소 등)이 있는 토스트는 자동 소멸시키지 않는다** — 사용자가 다 읽기 전에 행동 기회가 사라진다 [SKILL-BETTER-A11Y]
|
||||
- 진입·퇴장 트랜지션은 새 토큰을 만들지 않고 `--dur-quick`/`--ease-out`(진입), `calc(var(--dur-quick) * 0.65)`/`--ease-in`(퇴장)을 그대로 재사용한다
|
||||
- 스택 방향(위로 쌓을지 아래로 쌓을지)은 프로젝트 계약이다
|
||||
|
||||
### 토글 스위치
|
||||
|
||||
`role="switch"`, `aria-checked` 필수 — ARIA Authoring Practices Guide의 스위치 패턴을 따른다 [ARIA-APG]. 손잡이 이동은 `translate`(저비용 기본 선택, `motion.md` §2)로 하고, `transition: translate var(--dur-instant) var(--ease-out)`가 기본이다. 스프링감이 필요하면 §6의 조건(톤이 확인됐을 때)에서 `linear()` 근사를 컴포넌트에 로컬로 쓴다 — 전역 토큰을 새로 만들지 않는다.
|
||||
|
||||
### 스켈레톤 시머
|
||||
|
||||
원칙("로딩은 실제 비동기에만 쓴다", "스피너보다 스켈레톤")은 이미 `preflight.md`에 있다. 구현은 그라디언트 스윕 애니메이션이다.
|
||||
|
||||
```css
|
||||
.skeleton {
|
||||
background: linear-gradient(90deg, var(--surface-raised) 25%, var(--surface) 50%, var(--surface-raised) 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s linear infinite;
|
||||
}
|
||||
@keyframes shimmer { to { background-position: -200% 0; } }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.skeleton { animation: none; background: var(--surface-raised); } /* 정적 회색 블록으로 대체 */
|
||||
}
|
||||
```
|
||||
|
||||
무한 반복이지만 작은 면적이라 `motion.md` §0의 "무한 반복되는 큰 면적 움직임 실격" 규칙에 걸리지 않는다.
|
||||
|
||||
### 글자 수 카운터
|
||||
|
||||
**관찰 후보, 기본값 아님.** 표시 여부와 `maxLength`는 프로젝트 계약이다. 쓴다면 남은 글자 수가 `20`자 이하일 때 경고 신호, `0` 미만이면 초과 신호를 준다(둘 다 색만이 아니라 문구도 함께) [SKILL-INTERACTION-DESIGN].
|
||||
|
||||
### 툴팁 그룹 즉시 열림
|
||||
|
||||
첫 툴팁은 지연을 두고 등장한다(실수로 스친 hover를 걸러낸다). 하나가 열려 있는 동안 인접 툴팁으로 옮기면, 그 뒤로는 지연·등장 애니메이션 없이 즉시 연다 — `data-instant` 같은 속성으로 그룹 상태를 표시한다 [SKILL-EMIL-DESIGN-ENG].
|
||||
|
||||
### 스와이프 동작과 대체 조작
|
||||
|
||||
**하드 게이트 — 드래그·스와이프만으로 끝내지 않는다** [WCAG-DRAG]. 스와이프 삭제·재정렬에는 키보드·스크린리더로 도달 가능한 대체 조작(메뉴 버튼 등)을 반드시 병행한다. 임계 거리는 §5-4와 같은 시작값을 쓰고 실측 조정한다.
|
||||
|
||||
### 당겨서 새로고침
|
||||
|
||||
> 언제: 네이티브 앱 느낌이 브리프의 명시적 요구일 때만.
|
||||
|
||||
당겨서 새로고침도 같은 원칙([WCAG-DRAG])이 적용된다. 제스처 하나에 단일 의존하지 않고 **명시적 새로고침 버튼을 항상 병행**한다. 임계값(예: `60px`)은 근거 없는 시작값이므로 실측 조정 문구를 반드시 남긴다 [SKILL-INTERACTION-DESIGN].
|
||||
|
||||
### scroll-snap 캐러셀
|
||||
|
||||
```css
|
||||
.snap-row { scroll-snap-type: x mandatory; overflow-x: auto; }
|
||||
.snap-item { scroll-snap-align: start; }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.snap-row { scroll-behavior: auto; } /* snap 자체는 유지 — WCAG 2.3.3 대상 애니메이션이 아니다 */
|
||||
}
|
||||
```
|
||||
|
||||
**스크롤 트랩 경고.** 휠·키보드 스크롤로 항목 사이를 지나가야 하는데 snap이 한 항목씩 강하게 붙잡으면, 사용자가 목록을 빠져나가지 못하는 트랩처럼 느껴질 수 있다. `mandatory` 대신 `proximity`를 검토하거나 스크롤 컨테이너 경계에서 탈출이 되는지 실제로 확인한다.
|
||||
|
||||
### 리플 이펙트
|
||||
|
||||
**조건부 — 기본값이 아니다.** Material Design 특유의 시그니처 이펙트를 모든 버튼에 기본으로 걸면 특정 미학을 무조건 강요하는 것이 된다(antipatterns.md 원칙과 같은 이유). 브리프가 그 미학을 명시했거나 사용자가 선택했을 때만 조건부로 쓴다 [SKILL-INTERACTION-DESIGN].
|
||||
|
||||
---
|
||||
|
||||
## 9. 멀티모달 피드백
|
||||
|
||||
> 언제: 모바일 앱 수준 UI(PWA 포함)에서 사운드·햅틱이 브리프·타깃 기기상 유효할 때만. 데스크톱 랜딩페이지 같은 프로젝트에는 대부분 해당하지 않는다.
|
||||
|
||||
세 원칙 — Causality(인과성)·Harmony(조화)·Utility(유용성) [SKILL-APPLE-DESIGN].
|
||||
|
||||
- **Causality**: 피드백은 실제로 일어난 인과적 사건에서 트리거한다(토글이 뒤집히는 순간, 아이템이 제자리에 스냅되는 순간). 캐릭터를 행동의 물리성에 맞춘다.
|
||||
- **Harmony**: 시각·사운드·햅틱은 같은 프레임에서 발화해야 한다. CSS 트랜지션이 오디오·햅틱을 지연시키지 않게 한다.
|
||||
- **Utility**: 의미 있는 순간(성공·오류·커밋·스냅)에만 아껴 쓴다. 과잉 피드백은 사용자가 전부 무시하도록 학습시킨다.
|
||||
|
||||
**프로젝트 계약 — 소리는 기본 꺼짐(opt-in).** 사용자가 먼저 켜지 않은 채로 소리가 나면 대부분의 맥락에서 침해적이다.
|
||||
|
||||
**프로젝트 계약 — 햅틱은 점진적 향상이며 단독 신호가 될 수 없다.** Vibration API(`navigator.vibrate`)는 webstatus.dev 기준 Chromium 계열(Chrome·Edge, Android 포함)만 구현했고 Safari(iOS 포함) 구현 기록은 없다[WEB-BASELINE]. 그래서 지원 여부를 가정하지 않고, 햅틱은 항상 시각·상태 신호와 함께 켜는 보강 채널로만 쓴다. 이는 §1의 "모션이 유일한 신호가 되면 안 된다"는 규칙을 감각 채널 전체로 확장한 것이다.
|
||||
|
||||
---
|
||||
|
||||
## 10. 엄지 영역
|
||||
|
||||
모바일 폭에서 1차 조작(주로 쓰는 버튼·주요 CTA)은 엄지가 자연스럽게 닿는 영역에 배치한다. 하단이 상단보다 도달하기 쉽다는 원칙은 `mobile-app-ux.md`가 이미 하단 시트·바텀내비의 근거로 쓰고 있다(썸존 = 화면 하단 약 35%) [SKILL-IMPECCABLE]. 이 문서에서 새로 정의하지 않고 자세한 레이아웃 규칙은 [mobile-app-ux.md](mobile-app-ux.md)를 따른다.
|
||||
|
||||
---
|
||||
|
||||
## 11. 커스텀 컨트롤 제스처 QA
|
||||
|
||||
**레이아웃 통과는 제스처 통과가 아니다.** 정적 스크린샷이나 시각 회귀는 슬라이더·캐러셀·스와이프 카드 같은 커스텀 컨트롤이 실제로 조작 가능한지 증명하지 않는다 [SKILL-IMPECCABLE].
|
||||
|
||||
확인할 것:
|
||||
|
||||
- **드래그 완주**: 시작점부터 끝점까지 전체 경로가 끊기지 않고 동작하는가(중간 프레임에서 판정이 끊기지 않는가)
|
||||
- **가로지르는 스와이프 vs 축을 따르는 드래그**: 컨트롤 자체의 축을 따라 미는 제스처와, 그 축을 가로지르며 스크롤과 경쟁하는 제스처를 구분해서 각각 검증한다(§3의 병렬 제스처 인식이 실제로 옳은 쪽을 골랐는지 확인하는 자리다)
|
||||
- **증거 출처를 보고에 명시한다**: 에뮬레이션(DevTools 터치 에뮬레이션 등)·합성 터치 이벤트(스크립트로 디스패치한 이벤트)·실기기 중 무엇으로 확인했는지, 어떤 엔진(WebKit·Blink·Gecko)이었는지 적는다. 실기기 검증 절차는 [mobile-app-ux.md](mobile-app-ux.md) "실기기 검증" 절을 따른다
|
||||
- **미검증은 통과로 세지 않는다.** 확인하지 못한 항목은 "미검증(공백)"으로 보고에 남긴다 — [audit-gate.md](audit-gate.md)의 미검증 상태값 원칙과 같다
|
||||
|
||||
---
|
||||
|
||||
## 12. 체크리스트
|
||||
|
||||
- [ ] 입력 경로에 불필요한 디바운스·타이머·전환 대기가 없다(§0)
|
||||
- [ ] hover 전용 스타일이 `@media (hover: hover) and (pointer: fine)`로 게이트돼 있다(§1)
|
||||
- [ ] 상태 변화(hover·active·selected·error 등)마다 모션이 꺼져도 남는 정적 신호가 있다(§1)
|
||||
- [ ] 프레스 피드백이 `scale(0.96~0.98)` 범위이고 `--dur-instant`를 쓴다. 새 duration 리터럴을 만들지 않았다(§2)
|
||||
- [ ] 커스텀 드래그 요소가 `setPointerCapture`·그랩 오프셋 보존·멀티터치 가드를 갖췄다(§3)
|
||||
- [ ] 재트리거되는 UI에 `@keyframes` 대신 `transition`을 썼다(§4)
|
||||
- [ ] 스와이프·당겨서 새로고침에 단일 포인터 대체 조작이 있다(§8, WCAG 2.5.7)
|
||||
- [ ] 토스트가 `role="status"`/`role="alert"`이고, 행동이 있는 토스트는 자동 소멸하지 않는다(§8)
|
||||
- [ ] 스프링을 썼다면 브리프·인터뷰로 톤이 확인됐고, 감소 모션에서는 바운스가 사라진다(§6)
|
||||
- [ ] 사운드는 기본 꺼짐이고, 햅틱은 단독 신호로 쓰이지 않는다(§9)
|
||||
- [ ] 커스텀 컨트롤을 실기기·엔진 근거로 검증했거나, 못 했다면 미검증으로 보고에 남겼다(§11)
|
||||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
4-1 단계에서 읽는다. **이 단계가 끝나면 아무 이펙트 없이도 완성된 페이지**여야 한다. 그것이 이후 모든 폴백의 기반이다.
|
||||
|
||||
접근성 규범(포커스·키보드·라이브 리전·히트 영역)은 [accessibility.md](accessibility.md), 입력 촉감·제스처·스프링은 [interaction-feel.md](interaction-feel.md), 아이콘 정렬·상태·RTL 뒤집기는 [icons.md](icons.md), 인쇄·이메일 적응은 [print-email.md](print-email.md)에서 각각 다룬다. 이 문서는 그리드·시선 흐름·타이포그래피 적용·반응형 구조를 다룬다.
|
||||
|
||||
---
|
||||
|
||||
## 1. 그리드를 먼저 정한다
|
||||
|
|
@ -43,6 +45,10 @@
|
|||
- 그리드에서 한 칸을 의도적으로 비우기
|
||||
- 카드 하나만 규칙을 깨고 밖으로 나가기
|
||||
|
||||
### 선택자 특정성은 조용히 상쇄된다
|
||||
|
||||
**관찰 후보 — 엔지니어링 관행.** 상위 컨테이너 클래스(`.section`)와 하위 컴포넌트 클래스(`.cta`)는 특정성이 같아(0,1,0) 로드 순서가 이긴다. `section.hero`(0,1,1)처럼 특정성이 다르면 로드 순서와 무관하게 한쪽이 이긴다. 4-e의 초광폭 분할 히어로 사례(상위 container selector와 wide stage의 `padding-inline` 중 한쪽만 남아 텍스트 폭이 붕괴한 것)는 이 원칙의 구체형이다. 4단계에서 새 컴포넌트 선택자를 쓸 때는 그 선택자가 덮으려는 상위 선택자의 특정성과, 두 선택자가 같은 속성을 정의하는지를 함께 확인한다. [SKILL-FRONTEND-DESIGN]
|
||||
|
||||
---
|
||||
|
||||
## 2. 시선 흐름
|
||||
|
|
@ -54,6 +60,20 @@
|
|||
3. 강조하거나 역할을 전환해야 할 때는 리듬·크기·여백·색의 차이로 시선을 머물게 할 수 있다. 모든 섹션의 리듬을 일부러 깨야 하는 것은 아니다
|
||||
4. 스크롤 뒤에도 새 정보나 과업 진전이 있는지 확인한다. 반복 카드가 이탈을 만든다는 보편 증거는 없으므로, 실제 콘텐츠와 사용자 행동으로 판단한다
|
||||
|
||||
### 그루핑 도구에는 선호 순서가 있다
|
||||
|
||||
**관찰 후보 — 시작값.** 관련 요소를 묶을 때는 이 순서로 검토한다.
|
||||
|
||||
1. **네거티브 스페이스** — 기본값. 관련 항목은 가깝게, 무관한 항목은 멀게 둔다.
|
||||
2. **배경 도형** — 카드·채워진 컨테이너. 그룹이 반드시 하나의 단위로 읽혀야 할 때(선택 가능한 행, 드래그 가능한 카드 등)만 쓴다.
|
||||
3. **구분선** — 최후 수단. 공간이 너무 비싼 밀집 데이터(표, 긴 설정 목록)에서만 쓴다. 헤어라인 두께, 낮은 대비로 두고, 이미 공간이 구조를 만들었다면 큰 gap과 중복해서 쓰지 않는다.
|
||||
|
||||
그룹 내 간격과 그룹 간 간격의 비율은 **1:2를 시작값**으로 둔다 — 그룹 내 `--space-2`(8px)면 그룹 간은 `--space-3`(16px) 이상. 비율이 이보다 좁으면 눈이 그룹 경계를 못 잡고 노이즈로 읽는다. 프로젝트 spacing 스케일에 이미 값이 있으면 그것을 그대로 쓰고, 이 비율로 다시 맞추지 않는다. [SKILL-BETTER-LAYOUT]
|
||||
|
||||
### 뷰당 주요 액션은 하나다
|
||||
|
||||
**관찰 후보.** 첫 화면은 목차이지 책 전체가 아니다. 뷰당 주요 액션을 하나로 정하고(색으로 강제하는 방법은 [color.md](color.md) 참고), 보조 액션이 2~3개를 넘으면 메뉴 뒤로 묶는다. 레벨 1에서 모든 것을 보여주는 긴 뷰보다, 더 깊이 링크하는 짧은 뷰를 우선한다. [SKILL-BETTER-LAYOUT]
|
||||
|
||||
### 의미가 다르면 섹션 토폴로지도 달라야 한다
|
||||
|
||||
일관성은 같은 거시 그리드를 복제하는 것이 아니다. **연속한 섹션의 역할이 다른데 모두 `왼쪽 큰 제목 + 오른쪽 목록`이면 구조 슬롭**이다. 색·배경·카드 radius를 바꿔도 읽는 동작은 같다.
|
||||
|
|
@ -91,6 +111,57 @@
|
|||
|
||||
닫힘 상태에서 목록 오른쪽 끝과 작업대 오른쪽 끝의 차이를 2px 이하로 재고, 행 선택 뒤 2열이 복원되는 것도 함께 단언한다.
|
||||
|
||||
### 컨트롤은 정적 텍스트와 다르게 보여야 한다
|
||||
|
||||
**관찰 후보 — 양방향.** 모든 상호작용 요소는 배경·테두리·일관된 배치 구역 중 하나로 정적 텍스트와 구별되어야 한다. 주변 텍스트와 똑같이 스타일된 컨트롤은 컨트롤로 읽히지 않는다. 역방향 함정도 같은 비중으로 본다 — 옆의 진짜 버튼과 똑같이 생긴 클릭 불가 배지·라벨은 헛클릭을 모은다. [SKILL-BETTER-LAYOUT]
|
||||
|
||||
### 광학 정렬
|
||||
|
||||
아이콘이 있는 버튼, 재생 삼각형, 비대칭 아이콘처럼 기하학적 중심은 맞아도 눈에는 어긋나 보이는 보정은 [icons.md](icons.md) §6을 본다.
|
||||
|
||||
### 점진적 공개에는 눈에 보이는 단서가 필요하다
|
||||
|
||||
**관찰 후보 — 시작값.** 숨긴 콘텐츠에 단서가 없으면 없는 것과 같다. 프로젝트에 기존 disclosure 패턴이 있으면 그것을 쓰고, 없을 때만 아래 레시피를 시작값으로 검토한다.
|
||||
|
||||
- **피킹 아이템**: 가로 스크롤러·캐러셀에서 다음 카드가 컨테이너 엣지 너머로 `16px`~`32px` 삐져나오게 한다.
|
||||
- **디스클로저 컨트롤**: 접힌 섹션에는 쉐브론이나 "더 보기"류 컨트롤을 두되, 숨겨진 개수를 라벨에 명시한다 — 예: "결과 12개 더 보기". 개수 없는 "더 보기"만으로는 약하다.
|
||||
- **잘림 단서**: 클램프된 텍스트는 말줄임표와 함께 전체 값에 도달할 수단(펼치기·링크·툴팁)을 같이 둔다.
|
||||
|
||||
```css
|
||||
.scroller {
|
||||
--peek: 24px; /* 다음 카드가 비치는 폭 — 시작값 16~32px, 실측 조정 */
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
overflow-x: auto;
|
||||
padding-inline: var(--space-4);
|
||||
scroll-padding-inline: var(--space-4);
|
||||
scroll-snap-type: x mandatory;
|
||||
}
|
||||
.scroller > * {
|
||||
/* 100% 는 패딩을 뺀 콘텐츠 폭이다. 오른쪽 패딩 영역에도 다음 카드가 비치므로 그만큼 덜 뺀다 */
|
||||
flex: 0 0 calc(100% - var(--space-2) - max(0px, var(--peek) - var(--space-4)));
|
||||
scroll-snap-align: start;
|
||||
}
|
||||
```
|
||||
|
||||
실제 렌더에서 피크 폭이 16~32px인지 확인한다.
|
||||
|
||||
[SKILL-BETTER-LAYOUT]
|
||||
|
||||
### 표의 숫자는 끝에, 텍스트는 시작에 정렬한다
|
||||
|
||||
**관찰 후보.** 정렬 기준선의 작은 집합을 고르고 고수한다. 표 안에서 숫자는 트레일링 엣지에, 텍스트는 리딩 엣지에 정렬한다 — 숫자 자체의 자릿수 정렬(`tabular-nums`)은 별개 축이며 [typography.md](typography.md)를 따른다. 물리적 left/right 대신 리딩/트레일링으로 사고하는 이유는 4-j "방향 독립 레이아웃" 절을 본다. [SKILL-BETTER-LAYOUT]
|
||||
|
||||
### 구체적인 내비 라벨이 우산 용어보다 낫다
|
||||
|
||||
**관찰 후보.** 내비게이션이나 섹션 라벨을 지을 때 "홈"·"메뉴" 같은 우산 용어보다, 그 화면이 실제로 하는 일을 가리키는 구체적 이름("진행 상황"·"보관함")이 더 잘 읽힌다. 구체적 라벨은 클릭 전에 무엇을 보게 될지 예측하게 한다. [SKILL-APPLE-DESIGN]
|
||||
|
||||
### 모달 액션 행은 스크롤 영역과 분리한다
|
||||
|
||||
**프로젝트 계약.** 리사이즈 가능한 패널 바닥, 고정 높이 모달의 폴드 아래, 확장하는 키보드 뒤처럼 잘릴 수 있는 자리에 확인·취소 같은 핵심 액션을 두지 않는다. 모달 콘텐츠가 스크롤되면 액션 행은 함께 스크롤되지 않고 안정된 자리(스티키 footer 또는 뷰 상단)에 남는다. native `<dialog>` 포지셔닝·내부 scroll owner 검증은 [preflight.md](preflight.md)의 모달 체크리스트와 상호 참조한다. [SKILL-BETTER-LAYOUT]
|
||||
|
||||
액션이 잘려 도달할 수 없으면 조작 불능으로 하드 게이트다.
|
||||
|
||||
---
|
||||
|
||||
## 3. 타이포그래피
|
||||
|
|
@ -132,11 +203,70 @@
|
|||
|
||||
`auto-fit` + `minmax`는 유동 반복 grid에 유용하다. 미디어쿼리와 container query는 구조·밀도·행동이 바뀌는 실제 실패 지점에서 선택한다.
|
||||
|
||||
### 입력 방식은 화면 크기와 별개 축이다
|
||||
|
||||
**관찰 후보 — 구현 패턴.** 터치스크린 노트북, 키보드가 달린 태블릿처럼 화면 크기만으로는 입력 방식을 알 수 없다. `pointer`·`hover` 미디어쿼리로 화면 크기 브레이크포인트와 별개 축으로 다룬다. 호버로만 접근되는 기능을 만들지 않는다는 원칙(아래 "모바일에서 먼저 확인해라") 자체는 이미 하드 게이트이고, 아래는 그 구현 패턴이다.
|
||||
|
||||
```css
|
||||
/* 정밀 포인터(마우스·트랙패드) */
|
||||
@media (pointer: fine) {
|
||||
.button { padding: 8px 16px; }
|
||||
}
|
||||
|
||||
/* 거친 포인터(터치·스타일러스) — 데스크톱 폭에서도 나타날 수 있다 */
|
||||
@media (pointer: coarse) {
|
||||
.button { padding: 12px 20px; }
|
||||
}
|
||||
|
||||
/* 호버를 지원하는 디바이스 */
|
||||
@media (hover: hover) {
|
||||
.card:hover { transform: translateY(-2px); }
|
||||
}
|
||||
|
||||
/* 호버를 지원하지 않는 디바이스(터치) — active로 대체 */
|
||||
@media (hover: none) {
|
||||
.card { /* hover 상태 없음 */ }
|
||||
}
|
||||
```
|
||||
|
||||
**프로젝트 계약 — 데스크톱을 고성능·비터치로 가정하지 않는다.** 넓은 화면이 강력한 디바이스·마우스 전용을 뜻하지 않는다. 저사양 노트북, 터치스크린 노트북, 접근성 보조기기가 모두 데스크톱 폭에 있을 수 있다. `pointer: coarse` 대응과 성능 예산을 데스크톱 폭에서도 함께 시험한다. 성능 예산 자체는 뷰포트가 아니라 [tokens.md](tokens.md) §5를 그대로 따른다. [SKILL-IMPECCABLE]
|
||||
|
||||
### 컨테이너 쿼리 — 크기부터, 스타일은 조건부
|
||||
|
||||
**관찰 후보 — 구현 기법.** 컴포넌트 단위 적응에는 뷰포트 기준 media query보다 컨테이너 쿼리를 우선 검토한다. 카드는 뷰포트가 아니라 자신이 속한 컬럼에 반응해야 좁은 사이드바 안에서도 깨지지 않는다.
|
||||
|
||||
```css
|
||||
/* 크기 쿼리 — 2023년 2월부터 널리 지원 */
|
||||
.card-list { container-type: inline-size; }
|
||||
@container (max-width: 400px) {
|
||||
.card { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
/* 스타일 쿼리 — 2026년 5월부터 갓 지원(아직 널리는 아님), 폴백 경로를 함께 둔다 */
|
||||
.theme-scope { container-type: inline-size; container-name: theme; }
|
||||
@container theme style(--variant: compact) {
|
||||
.card { padding: var(--space-2); }
|
||||
}
|
||||
```
|
||||
|
||||
크기 쿼리(`@container` 크기 조건)는 2023-02부터 널리(Widely) 지원되지만, 스타일 쿼리(`style()`)는 2026-05-19부터 갓(Newly) 지원되어 아직 모든 브라우저에 널리 퍼지지 않았다 — 스타일 쿼리에 의존하는 레이아웃은 지원 확인 후 폴백을 함께 둔다. [WEB-BASELINE]
|
||||
|
||||
### em·rem 브레이크포인트가 확대를 따라가는 이유
|
||||
|
||||
**관찰 후보.** `px` 단위 미디어쿼리는 사용자가 브라우저의 텍스트 확대(브라우저 줌이 아니라 base font-size 배율 조정)를 써도 반응하지 않는다. `em`·`rem` 단위 쿼리는 확대된 base font-size를 따라 브레이크포인트가 함께 움직인다. `preflight.md`가 이미 경고하는 "px 고정 폰트는 OS 확대를 게이트가 보장하지 못한다"는 문제의 해법 중 하나가 이것이다.
|
||||
|
||||
| 항목 | 단위 | 이유 |
|
||||
|---|---|---|
|
||||
| `font-size`, `max-width`, 브레이크포인트, 스케일링 spacing | `rem` | 확대된 base font-size에 반응해야 한다 |
|
||||
| border, focus outline, box-shadow, 고정 장식 | `px` | 확대와 무관하게 일정한 두께를 유지해야 한다 |
|
||||
|
||||
이 표는 시작값이다. 프로젝트에 이미 다른 단위 관례가 있으면 그것을 따른다. [SKILL-BETTER-A11Y]
|
||||
|
||||
### 모바일에서 먼저 확인해라
|
||||
데스크톱에서 아름다운 것이 모바일에서 무너지는 것이 기본이고, 그 반대는 드물다. 특히:
|
||||
- 큰 타이포는 모바일에서 줄 수·가려짐·다음 행동의 가시성을 보고 크기·폭·문구·구도를 함께 조정한다. 단순 축소가 유일한 해법은 아니다
|
||||
- 가로 스크롤이 생기는 요소를 찾아라 (`overflow-x: hidden` 으로 덮지 말고 원인을 고쳐라)
|
||||
- 터치 타깃은 과업 빈도·오입력 위험과 WCAG 2.2 AA의 24×24 CSS px 최소 기준(예외 포함)을 함께 검토한다
|
||||
- 터치 타깃은 과업 빈도·오입력 위험과 WCAG 2.2 AA의 24×24 CSS px 최소 기준(예외 포함)을 함께 검토한다. 시각 크기보다 히트 영역을 넓히는 구체 기법(의사요소 확장, 확장 영역 겹침 방지, 장식 레이어의 `pointer-events`)은 [accessibility.md](accessibility.md) §8을 본다
|
||||
- 호버로만 접근되는 기능을 만들지 마라
|
||||
|
||||
반응형 장면은 폭만 기록하지 않는다. 높이·종횡비·zoom·스크롤·상태가 짧은 화면, 긴 화면, 확대에서 결과를 바꾸는지 먼저 보고 대표 viewport를 고른다. 프로젝트의 특정 수치를 공통 규칙으로 만들지 않으며, 실제 측정 artifact와 실패 처리는 [audit-gate.md](audit-gate.md)의 렌더 측정 계약을 따른다.
|
||||
|
|
@ -217,7 +347,7 @@ new Set([...document.querySelectorAll('.nav a')]
|
|||
1440px에서 멀쩡한 페이지가 1920·2560px에서 깨지는 방식은 반대다. 모바일처럼 넘치지는 않지만, 본문은 끝없이 늘어나고 고정 폭 헤드라인은 빈 여백에 밀려나며 표는 너무 좁아진다. **컨테이너 하나를 무조건 풀거나 조이지 마라.** 읽는 표면과 비교·명세 표면의 요구가 다르다.
|
||||
|
||||
```css
|
||||
:root { --measure: 42rem; --container-wide: 74rem; }
|
||||
:root { --measure: 42rem; /* 실제 폰트 기준 60~75자(한글 25~40자)로 재검증 */ --container-wide: 74rem; }
|
||||
.prose { max-width: var(--measure); }
|
||||
.specification { width: min(100% - 2 * var(--pad-inline), var(--container-wide)); }
|
||||
|
||||
|
|
@ -254,6 +384,114 @@ Apple도 다양한 화면 크기에서 적응형 레이아웃과 읽기 좋은
|
|||
|
||||
---
|
||||
|
||||
## 4-f. 데스크톱 어포던스 — 조건부
|
||||
|
||||
언제: 워크벤치·콘솔·전문가용 도구처럼 마우스·키보드 중심의 넓은 화면을 1차 대상으로 하는 프로젝트일 때만 연다. 일반 랜딩·마케팅 페이지에는 적용하지 않는다.
|
||||
|
||||
**관찰 후보.** 이런 프로젝트는 좁은 화면 패턴을 그대로 확대하지 않고, 넓은 입력 표면이 실제로 주는 것을 쓴다.
|
||||
|
||||
- 부가 정보용 hover 상태(단, 키보드·터치 대체 경로를 반드시 같이 둔다)
|
||||
- 키보드 단축키(잦은 동작에)
|
||||
- 우클릭 컨텍스트 메뉴
|
||||
- 도움이 되는 곳의 드래그 앤 드롭
|
||||
- Shift·Cmd로 다중 선택
|
||||
- 여러 정보 패널을 동시에 노출(점진적 공개를 줄임)
|
||||
|
||||
이 목록은 워크벤치·콘솔의 시작값이지 일반 웹의 규범이 아니다 — 브리프가 다른 성격이면 열지 않는다. [SKILL-IMPECCABLE]
|
||||
|
||||
## 4-g. 모바일에서 표를 카드로 바꾼다
|
||||
|
||||
**관찰 후보 — 구현 패턴.** 좁은 화면에서 다열 표를 그대로 축소하면 가로 스크롤이나 텍스트 붕괴가 난다. `display: block`과 `data-label` 속성으로 각 셀을 헤더-값 쌍으로 보여주는 카드로 바꾸는 방법을 시작값으로 검토한다.
|
||||
|
||||
```css
|
||||
@media (max-width: 40rem) {
|
||||
table, thead, tbody, th, td, tr { display: block; }
|
||||
thead { display: none; }
|
||||
tr { margin-block-end: var(--space-3); }
|
||||
td {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding-inline-start: 50%;
|
||||
position: relative;
|
||||
}
|
||||
td::before {
|
||||
content: attr(data-label);
|
||||
position: absolute;
|
||||
inset-inline-start: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<td data-label="가격">₩12,000</td>
|
||||
```
|
||||
|
||||
변환 후에는 표의 의미(헤더-값 관계)가 스크린리더에서도 유지되는지 확인한다 — 시각적으로 카드가 되어도 마크업은 여전히 `<table>`이어야 관계가 보존된다. 다만 일부 브라우저는 표 요소의 `display`를 바꾸면 표 의미 자체를 접근성 트리에서 버린다. 변환할 때는 `role="table"`·`rowgroup`·`row`·`columnheader`·`cell`을 명시해 두고, 접근성 트리 스냅샷이나 스크린리더로 실제로 확인한다([accessibility.md](accessibility.md) §13). 레이아웃 검증(카드로 보이는지)과 의미 검증(관계가 읽히는지)은 별개이며 둘 다 확인한다. [SKILL-IMPECCABLE]
|
||||
|
||||
## 4-h. 전문가 도구는 밀도를 보존한다
|
||||
|
||||
언제: 프로젝트가 이미 확립된 밀도(콘솔, 관리자 도구, 데이터 그리드)를 갖고 있을 때.
|
||||
|
||||
**관찰 후보.** 컴팩트한 전문가용 도구는 히트 영역이 겹치지 않고 컨트롤이 구별되는 한, 위 "그루핑 도구" 절의 간격 시작값보다 적게 써도 된다. 확립되고 실제로 쓰이고 있는 밀도를, 이 문서의 시작값에 맞추려고 컨트롤을 키우거나 간격을 벌리지 않는다 — 이는 사용자·기존 토큰을 기본값보다 우선하는 원칙과 같은 방향이다. [SKILL-BETTER-LAYOUT]
|
||||
|
||||
## 4-i. 브레드크럼 — 조건부
|
||||
|
||||
언제: 3단계 이상의 깊은 계층 구조를 가진 콘솔·문서형·대시보드류 프로젝트일 때. 얕은 계층의 랜딩·포트폴리오에는 가치가 낮다.
|
||||
|
||||
**관찰 후보.** 깊은 계층에서 컨텍스트를 유지하는 수단으로 브레드크럼을 후보로 검토한다. 좁은 화면에서는 마지막 1~2단계만 보이거나 상위 단계를 축약 기호로 접는다. [SKILL-IMPECCABLE]
|
||||
|
||||
## 4-j. 방향 독립 레이아웃
|
||||
|
||||
언제: 프로젝트가 RTL 로케일(아랍어·히브리어 등)을 지원하기로 했을 때만 연다. RTL을 지원하지 않는 프로젝트에는 이 절이 적용되지 않는다 — [icons.md](icons.md) §7의 아이콘 뒤집기 표가 이 절을 참조한다.
|
||||
|
||||
**프로젝트 계약(조건부).** 방향 의존 수평 위치는 물리 속성 대신 논리 속성으로 쓴다. `dir="rtl"`에서 레이아웃이 자동으로 미러된다.
|
||||
|
||||
| Physical | Logical |
|
||||
|---|---|
|
||||
| `margin-left` | `margin-inline-start` |
|
||||
| `padding-right` | `padding-inline-end` |
|
||||
| `left: 0` | `inset-inline-start: 0` |
|
||||
| `text-align: left` | `text-align: start` |
|
||||
| `border-right` | `border-inline-end` |
|
||||
|
||||
```css
|
||||
/* 좋음 — dir="rtl"에서 자동 미러 */
|
||||
.section { padding-inline: var(--space-4); }
|
||||
.section .child { margin-inline-start: var(--space-3); }
|
||||
|
||||
/* 나쁨 — RTL에서 깨짐 */
|
||||
.section { padding-left: 24px; padding-right: 24px; }
|
||||
.section .child { margin-left: 16px; }
|
||||
```
|
||||
|
||||
노치 대응 같은 **진짜 물리적 기하**나 제스처 방향처럼 언어와 무관한 것에는 물리 속성을 그대로 남긴다.
|
||||
|
||||
**진행 시퀀스는 미러되지만 손으로 배치한 요소는 아니다.** 별점, 스텝 인디케이터, 진행 바처럼 배치가 진행을 인코딩하는 요소는 RTL에서 시퀀스가 미러되어 트레일링부터 채워진다. 논리 속성을 쓴 flexbox·grid는 자동으로 미러되지만, `position: absolute` 같은 수동 배치 요소는 미러되지 않는다는 함정이 있다 — 별도로 확인한다. 숫자 내부의 자리 순서는 방향과 무관하게 절대 뒤집히지 않는다.
|
||||
|
||||
읽기 순서는 좌우가 아니라 **리딩→트레일링**으로 사고한다. 위 §2 "표의 숫자는 끝에, 텍스트는 시작에 정렬한다" 절의 끝·시작도 이 어휘를 쓴다. [SKILL-BETTER-LAYOUT]
|
||||
|
||||
## 4-k. 가짜 현지화 검사
|
||||
|
||||
언제: 프로젝트가 다국어(번역)를 지원할 때.
|
||||
|
||||
**프로젝트 계약(조건부).** 번역된 문자열은 늘어나고, **짧은 문자열일수록 비율적으로 더 늘어난다** — 그래서 한 단어짜리 버튼 라벨이 화면에서 가장 위험하다. 문자열 확장 폭은 언어와 원문 길이별로 다르므로 하나의 퍼센트 예산에 의존하지 않는다. 배포 전 가짜 현지화(pseudo-localization, 문자를 늘리고 특수문자로 감싸 실제 번역 없이 레이아웃 반응을 시험하는 기법)나 대표 로케일 1개로 실제 테스트한다.
|
||||
|
||||
- 텍스트 컨테이너에 고정 폭·고정 높이를 두지 않는다(줄바꿈을 허용하거나 `min-height`를 쓴다).
|
||||
- 버튼은 라벨 길이에서 스스로 크기를 얻는다(`padding-inline`, 하드코딩된 폭 금지).
|
||||
|
||||
```css
|
||||
/* 좋음 */
|
||||
.button { padding-inline: var(--space-3); white-space: nowrap; }
|
||||
|
||||
/* 나쁨 — 독일어 등 긴 번역에서 잘린다 */
|
||||
.button { width: 96px; overflow: hidden; }
|
||||
```
|
||||
|
||||
[SKILL-BETTER-LAYOUT]
|
||||
|
||||
---
|
||||
|
||||
## 5. 이 단계의 통과 조건
|
||||
|
||||
- [ ] CSS/HTML만으로 페이지가 완성됐다. JS를 꺼도 읽힌다
|
||||
|
|
|
|||
|
|
@ -17,13 +17,35 @@
|
|||
| 축 | iOS (HIG) | Android (M3) | 웹 구현 |
|
||||
|---|---|---|---|
|
||||
| 하단 내비 목적지 | 탭바 **5~6 이하** | 바텀내비 **3~5** | 초과분은 "더보기" 시트로 |
|
||||
| 터치 타깃 | 44×44pt | 48×48dp | coarse 포인터에서 44px 최소, 인라인 링크는 24px |
|
||||
| 터치 타깃 | 44×44pt[PLATFORM-HIG-TARGET] | 48×48dp[PLATFORM-M3-TARGET] | coarse 포인터에서 44px 최소, 인라인 링크는 24px |
|
||||
| safe area | 노치·홈 인디케이터 회피 | 제스처 바 회피 | `viewport-fit=cover` + `env(safe-area-inset-*)` |
|
||||
| 입력 확대 | 포커스 시 16px 미만이면 확대 | — | `@media (max-width:680px)` 입력 16px |
|
||||
| 뒤로가기 | 스와이프 백 = 히스토리 | **하드웨어 백 = 히스토리** | `pushState`/`popstate` (아래 패턴) |
|
||||
|
||||
두 OS가 같은 것: 하단 시트는 그래버가 있고 썸존(화면 하단 35%)에서 열린다. 제스처 내비 예측 영역(가장자리)에 컨트롤을 두지 않는다.
|
||||
|
||||
### 엄지 우선 배치의 일반화
|
||||
|
||||
**프로젝트 계약**: 위 썸존(하단 시트가 열리는 위치)은 한 사례일 뿐이다. 일반 원칙으로 넓히면, 모바일 화면의 1차 조작(주요 CTA·자주 쓰는 토글·플로팅 버튼)은 화면 하단 1/3을 엄지의 자연스러운 도달권으로 보고 먼저 검토한다. 상단이 엄지보다 멀다는 사실 자체가 항상 이긴다는 뜻은 아니다 — 스캔 순서·기존 플랫폼 관습이 상단 배치를 요구할 수 있다. 상단에 둘 때는 그 근거를 design.md에 기록한다[SKILL-IMPECCABLE].
|
||||
|
||||
썸존 35%와 하단 1/3은 다른 두 값이다 — 바텀시트는 하단 약 35%에서 열리고, 1차 조작의 일반 도달권은 하단 1/3로 본다.
|
||||
|
||||
### 플랫폼 관용구 대응 (iOS ↔ Android)
|
||||
|
||||
웹은 두 플랫폼 중 하나를 흉내 내지 않는다. 다만 "iOS 느낌"·"Android 느낌"을 브리프가 언급하면 아래 대응표로 관용구를 옮겨라 — 한쪽 이름을 다른 쪽에 그대로 이식(transplant)하지 않는다[SKILL-IMPECCABLE].
|
||||
|
||||
| iOS | Android |
|
||||
|---|---|
|
||||
| 탭바(Tab bar) | 내비게이션 바 / 레일 / 드로어 |
|
||||
| 가장자리 스와이프 백, 뒤로가기 셰브런 | Predictive Back 제스처 / 버튼 |
|
||||
| 스위치, 세그먼트 컨트롤, 시스템 피커 | Material 스위치, 칩, Material 피커 |
|
||||
| 액션 시트 | 바텀 시트 / Material 다이얼로그 |
|
||||
| SF Symbols, SF Pro, Dynamic Type | Material Symbols, Roboto, sp 스케일링 |
|
||||
| 시맨틱 시스템 색·머티리얼 | Material 색 역할, tonal elevation |
|
||||
| 시스템 push/sheet 전환 | container transform, shared-axis, fade-through |
|
||||
|
||||
웹 구현은 두 관용구 중 하나를 그대로 베끼지 않고, 이 문서의 5축 요약(터치 타깃·safe area·뒤로가기)처럼 웹 네이티브 수단으로 옮긴 값을 쓴다.
|
||||
|
||||
## 구현 패턴
|
||||
|
||||
### safe area
|
||||
|
|
@ -40,6 +62,29 @@
|
|||
|
||||
`env()` 는 `viewport-fit=cover` 없이는 0으로 굳는다. 패딩에 더할 때는 `max(기존값, safe)` — safe가 0인 기기에서 기존 리듬이 살아야 한다. **주의: 토큰 블록(`:root {}`) 안에 넣어야 한다.** 블록 밖에 부착하는 실수가 있었다(무시되고 조용히 죽는다).
|
||||
|
||||
### 콘텐츠는 bleed, 컨트롤은 float — 2층 모델
|
||||
|
||||
**프로젝트 계약**: 모바일 앱 수준 화면은 콘텐츠 레이어와 컨트롤 레이어를 다른 규칙으로 다룬다. 배경·히어로 미디어·스크롤 가능한 목록 같은 콘텐츠 레이어는 뷰포트 가장자리까지 채워도 된다(bleed). 텍스트와 상호작용 컨트롤은 레이아웃 마진과 safe area 안에 머물며 콘텐츠 위에 떠 있는다(float) — 콘텐츠 레이어가 뷰포트를 채운다고 컨트롤까지 가장자리에 붙이지 않는다[SKILL-BETTER-LAYOUT].
|
||||
|
||||
```css
|
||||
.article {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr min(65ch, calc(100% - 48px)) 1fr;
|
||||
}
|
||||
.article > * { grid-column: 2; }
|
||||
.article > .full-bleed { grid-column: 1 / -1; } /* 콘텐츠만 bleed */
|
||||
|
||||
.fab {
|
||||
position: fixed;
|
||||
inset-inline-end: calc(16px + env(safe-area-inset-right, 0px));
|
||||
bottom: calc(16px + env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
/* safe-area env() 값은 물리 방향이다. RTL 에서 inline-end 는 왼쪽이므로 왼쪽 inset 을 더한다 */
|
||||
.fab:dir(rtl) { inset-inline-end: calc(16px + env(safe-area-inset-left, 0px)); }
|
||||
```
|
||||
|
||||
FAB(플로팅 액션 버튼)는 safe area와 논리 속성(`inset-inline-end`)을 함께 쓴다 — 물리적 `right`만 쓰면 RTL 로케일에서 반대편에 붙는다.
|
||||
|
||||
### iOS 입력 확대 방지
|
||||
|
||||
포커스된 입력이 16px 미만이면 iOS 사파리가 자동 확대한다. 확대는 컨트롤러가 아니라 뷰포트를 밀어 레이아웃을 흔든다.
|
||||
|
|
@ -52,6 +97,33 @@
|
|||
|
||||
**`maximum-scale=1` 로 잡지 마라.** 확대를 막는 건 WCAG 1.4.4 위반이다. 폰트 크기로만 잡는다. ID 선택자가 폰트를 `font: inherit`으로 정의한 경우 같은 명시도를 파일 뒤에 붙여 이겨야 한다(실측: `#q`가 미디어쿼리를 이겨 확대가 살아있었다).
|
||||
|
||||
**두 해법 중 하나를 고른다. 둘 다 정답이고 무엇을 디자인이 원하는지가 다르다**[SKILL-BETTER-TYPE].
|
||||
|
||||
1. **모바일에서 키운다.** 위 코드처럼 입력을 16px로 렌더하고, 넓은 화면부터 디자인 크기로 되돌린다. 보정이 필요 없지만 모바일 입력이 데스크톱과 시각적으로 달라진다.
|
||||
2. **16px를 유지하고 시각적으로 축소한다.** `font-size`는 항상 16px로 둬 Safari가 확대하지 않게 하고, `transform: scale()`로 의도한 크기로 보여준다. 모든 뷰포트에서 디자인이 그대로지만 보정 계산이 늘어난다 — 축소 비율의 역수로 폭을 넓힌다. `transform-origin: left`(RTL은 `right`)로 텍스트를 시작 가장자리에 고정한다.
|
||||
|
||||
```css
|
||||
/* 16px 폰트를 13px로 보여주는 예: 13 / 16 = 0.8125 */
|
||||
.input-shell { display: flex; align-items: center; border-radius: 10px; padding-inline: 10px; }
|
||||
.input-shell input {
|
||||
width: calc(100% / 0.8125);
|
||||
transform: scale(0.8125);
|
||||
transform-origin: left;
|
||||
font-size: 16px;
|
||||
line-height: 1.125;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
@media (min-width: 640px) {
|
||||
.input-shell input { width: 100%; transform: none; font-size: 13px; }
|
||||
}
|
||||
```
|
||||
|
||||
unitless `line-height`(위 예의 `1.125`처럼 배수로 쓴 값)는 `scale()`과 함께 줄어들어도 폰트 크기와의 비율이 유지되므로 나눗셈 보정이 필요 없다. px 같은 절대 길이 행간을 쓸 때만 나눗셈 보정이 필요하다.
|
||||
|
||||
트랜스폼은 글자만이 아니라 입력 박스 전체를 줄인다. 배경·테두리·포커스 링은 감싸는 `.input-shell`에 그리고 `input` 자체는 투명하게 둬라 — 축소된 요소에 테두리를 직접 걸면 의도한 히트 영역을 놓친다. 어느 쪽을 쓸지 브리프가 정하지 않았으면 0단계에서 묻는다.
|
||||
|
||||
### 하단 내비 + 더보기 시트
|
||||
|
||||
목적지가 5개 이하면 바텀내비에 전부 노출한다. 넘으면: **4개 고정 + 더보기**로 나누고, 나머지는 바텀 시트에 그룹 헤더를 달아 노출한다.
|
||||
|
|
@ -117,6 +189,87 @@ body { overscroll-behavior-y: none; } /* 페이지 전체가 통째로
|
|||
:is(button, a, input, select, textarea) { touch-action: manipulation; } /* 더블탭 줌 지연 제거 */
|
||||
```
|
||||
|
||||
자체 드래그·줌 표면(캔버스·커스텀 슬라이더)은 `touch-action: none`을 그 요소에만 스코프해 건다 — 전역에 걸면 페이지 스크롤까지 막힌다. 호버 전용 효과(카드 확대 등)는 `@media (hover: hover) and (pointer: fine)`로 게이트한다. 터치 기기는 탭이 호버를 일으켜 오탐을 만든다.
|
||||
|
||||
## 촉감 피드백(햅틱)
|
||||
|
||||
언제: PWA·모바일 웹 앱 브리프가 네이티브 앱 감각을 요구할 때만 연다. 랜딩 페이지·포트폴리오·데스크톱 우선 프로젝트에는 적용하지 않는다.
|
||||
|
||||
**프로젝트 계약**: Vibration API(`navigator.vibrate()`)는 webstatus.dev 기준 Chromium 계열(Chrome·Edge, Android 포함)만 구현했고 Safari(iOS 포함) 구현 기록은 없다[WEB-BASELINE]. 그래서 햅틱은 "있으면 더 좋은" 보강 신호로만 쓰고, 성공·오류·완료를 햅틱 단독으로 전달하지 않는다 — 시각 신호(토스트·상태 변화, [interaction-feel.md](interaction-feel.md) 참고)를 항상 함께 둔다.
|
||||
|
||||
멀티모달 피드백은 세 원칙을 따른다: **Causality(인과성)** — 실제 인과 사건(토글이 뒤집히는 순간, 아이템이 제자리에 스냅되는 순간)에서만 트리거한다. **Harmony(조화)** — 비주얼·사운드·햅틱이 같은 프레임에서 발화해야 한다. CSS 트랜지션이 `navigator.vibrate()` 호출을 지연시키지 않게 한다. **Utility(유용성)** — 의미 있는 순간(성공·오류·커밋)에만 아껴 쓴다. 과잉 피드백은 사용자가 전부 무시하도록 학습시킨다[SKILL-APPLE-DESIGN].
|
||||
|
||||
```js
|
||||
function hapticTap(pattern = 10) {
|
||||
if (!('vibrate' in navigator)) return; // iOS Safari 등 미지원 환경은 조용히 무시
|
||||
navigator.vibrate(pattern);
|
||||
}
|
||||
```
|
||||
|
||||
## 제스처 대체 조작 — 하드 게이트
|
||||
|
||||
### 스와이프 삭제·재정렬의 대체 조작
|
||||
|
||||
**하드 게이트**: 드래그로만 되는 기능은 단일 포인터의 드래그 없는 방법으로도 가능해야 한다 — 브라우저가 정하는 스크롤 등은 예외다(WCAG 2.5.7)[WCAG-DRAG]. 카드 스와이프 삭제·리스트 드래그 재정렬을 만들 때는 같은 결과를 내는 버튼(항목 메뉴의 "삭제"·"위로 이동" 등)을 항상 함께 둔다.
|
||||
|
||||
```js
|
||||
function bindSwipeDismiss(card, onDismiss) {
|
||||
let startX = 0, dx = 0, dragging = false;
|
||||
card.addEventListener('pointerdown', (e) => {
|
||||
dragging = true; startX = e.clientX; card.setPointerCapture(e.pointerId);
|
||||
});
|
||||
card.addEventListener('pointermove', (e) => {
|
||||
if (!dragging) return;
|
||||
dx = e.clientX - startX;
|
||||
card.style.transform = `translateX(${dx}px)`;
|
||||
});
|
||||
card.addEventListener('pointerup', () => {
|
||||
dragging = false;
|
||||
const threshold = card.offsetWidth * 0.28; // 시작값: 카드 폭의 25~30% — 실측 조정
|
||||
if (Math.abs(dx) > threshold) onDismiss();
|
||||
else card.style.transform = '';
|
||||
dx = 0;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**관찰 후보**: 임계값은 카드 폭의 25~30% 또는 100px을 시작값으로 두고, 프로젝트에서 실측해 조정한다[SKILL-INTERACTION-DESIGN].
|
||||
|
||||
### 당겨서 새로고침 — 조건부
|
||||
|
||||
언제: 네이티브 앱 감각이 브리프 요구일 때만 연다.
|
||||
|
||||
**하드 게이트**: 위 스와이프 항목과 같은 이유(WCAG 2.5.7)로, 당겨서 새로고침도 명시적 새로고침 버튼을 항상 병행한다 — 제스처 하나에만 기능을 의존시키지 않는다.
|
||||
|
||||
```js
|
||||
const PULL_THRESHOLD = 60; // 시작값 60px — 실측 조정
|
||||
let startY = 0, pullY = 0, pulling = false;
|
||||
document.addEventListener('touchstart', (e) => {
|
||||
if (window.scrollY === 0) { startY = e.touches[0].clientY; pullY = 0; pulling = true; }
|
||||
}, { passive: true });
|
||||
document.addEventListener('touchmove', (e) => {
|
||||
if (!pulling) return;
|
||||
pullY = Math.max(0, e.touches[0].clientY - startY);
|
||||
indicator.style.opacity = String(Math.min(pullY / PULL_THRESHOLD, 1));
|
||||
}, { passive: true });
|
||||
document.addEventListener('touchend', () => {
|
||||
if (pulling && pullY > PULL_THRESHOLD) triggerRefresh();
|
||||
pulling = false; pullY = 0; indicator.style.opacity = '0';
|
||||
});
|
||||
```
|
||||
|
||||
임계값 60px는 근거가 약한 시작값이다[SKILL-INTERACTION-DESIGN] — 프로젝트에서 실측해 조정한다.
|
||||
|
||||
### 드래그형 커스텀 컨트롤의 QA
|
||||
|
||||
슬라이더·드래그 표면·스크롤형 컨트롤 스트립은 레이아웃 검증(너비·배치)과 제스처 검증이 별개다 — 레이아웃이 통과해도 제스처는 조용히 실패할 수 있다. 최소한 다음을 확인한다.
|
||||
|
||||
1. 탭 응답뿐 아니라 대상 입력방식으로 드래그를 끝까지 완주해 본다(시작만이 아니라).
|
||||
2. 컨트롤을 가로지르는 스와이프(페이지·컨테이너가 스크롤돼야 한다)와 컨트롤 축을 따르는 드래그(컨트롤이 움직여야 한다)를 둘 다 시험한다.
|
||||
3. 증거 출처(에뮬레이션·합성 터치·실기기, 엔진명 Chromium/Safari)를 보고에 명시한다. 접근 불가한 하드웨어는 "보고된 갭"으로 정직하게 남긴다 — 차단 사유로 쓰지 않는다.
|
||||
|
||||
제스처 인식 알고리즘(히스테리시스·포인터 캡처·그랩 오프셋)과 속도 기반 dismiss 판정은 [interaction-feel.md](interaction-feel.md)를 참조한다.
|
||||
|
||||
## PWA 최소 세트
|
||||
|
||||
"앱처럼"의 최소 기준: `manifest.webmanifest`(name·short_name·display: standalone·theme_color·아이콘 192/512) + `apple-touch-icon` + `mobile-web-app-capable` 메타 2종. 아이콘은 SVG에서 sharp로 PNG를 뽑는다(브랜드 문양 — 장부 문항, 시간표 블록 같은 **도메인 은유**). 홈 화면에 설치하면 아이콘이 곧 브랜드다 — 파비콘 재사용으로 땜빵하지 않는다.
|
||||
|
|
@ -150,3 +303,5 @@ body { overscroll-behavior-y: none; } /* 페이지 전체가 통째로
|
|||
- Nielsen Norman Group — Bottom Sheets UX, Touchscreen Touch Targets (nngroup.com/articles)
|
||||
- MDN — `env(safe-area-inset-*)`, `viewport-fit`, `overscroll-behavior`, `touch-action`
|
||||
- WebAIM/WCAG 1.4.4 — 확대 금지(`maximum-scale=1`)가 접근성 위반인 이유
|
||||
- WCAG 2.2 SC 2.5.7 Dragging Movements — 드래그 전용 기능은 단일 포인터의 비드래그 대안이 있어야 한다[WCAG-DRAG]
|
||||
- webstatus.dev — Vibration API 지원 범위(Chromium 계열만 구현, Safari(iOS 포함) 구현 기록 없음), 확인 2026-09-24[WEB-BASELINE]
|
||||
|
|
|
|||
|
|
@ -7,12 +7,16 @@
|
|||
| 상황 | 읽을 곳 |
|
||||
|---|---|
|
||||
| **모션을 넣을지 아직 안 정했다** | **§0만.** 절반은 여기서 "안 넣는다"로 끝나고 그게 정답이다 |
|
||||
| 하루에 몇 번 쓰는 동작인지로 판단하고 싶다 | §0 사용 빈도 표 |
|
||||
| duration·이징만 고르면 된다 | §1 결정 표. 여기서 끝내라 |
|
||||
| 하고 싶은 게 게이트 #8에 걸린다 | §2 우회표 |
|
||||
| 스크롤에 뭔가 물려야 한다 | §3 코드 B·C — 게이트 #10의 정답이 여기 있다 |
|
||||
| 하고 싶은 게 저비용 속성(transform/opacity) 밖이다 | §2 우회표 |
|
||||
| 스크롤에 뭔가 물려야 한다 | §3 코드 B·C — '애니메이션·스크롤 로직이 입력을 방해하지 않는다' 하드 게이트의 정답이 여기 있다 |
|
||||
| 모달·페이지 전환 / 텍스트·숫자 연출 | §3 코드 D·E / F·G |
|
||||
| 라이브러리를 깔지 말지 | §4 |
|
||||
| 형태 자체가 잘려야 한다(clip-path) | §3 코드 C-1 |
|
||||
| 테마(라이트·다크) 전환이 번져 보인다 | §3 코드 I |
|
||||
| 라이브러리를 깔지 말지 / 정리 규율 | §4 |
|
||||
| 감사 직전 | §5 체크리스트 |
|
||||
| 드래그·제스처·촉감 피드백을 만들어야 한다 | 이 문서가 아니라 [interaction-feel.md](interaction-feel.md) — 입력 축은 전부 거기서 다룬다 |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -29,6 +33,25 @@
|
|||
|
||||
모든 인터랙션에는 입력 결과를 알 수 있는 피드백이 필요하다. 다만 모션은 상태 변화의 이해를 도울 때만 쓰며, 즉시 바뀌는 값·텍스트·포커스도 피드백이 될 수 있다.
|
||||
|
||||
### 사용 빈도가 답을 정할 때도 있다
|
||||
|
||||
빈도를 모르면 넣지 말고, 브리프에서 확인하거나 가정으로 남긴다.
|
||||
|
||||
| 빈도 | 위계 | 예 | 처방 |
|
||||
|---|---|---|---|
|
||||
| 하루 100회 이상, 키보드 단축키·커맨드 팔레트로 실행 | 관찰 후보 — 시작값 | 커맨드 팔레트 열기, 단축키 토글 | 애니메이션을 넣지 않는다. 반복 마찰이 속도보다 크다 |
|
||||
| 하루 수십 회, 반복 목록·호버 내비 | 관찰 후보 — 시작값 | 리스트 항목 호버, 필터 전환 | 넣더라도 `--dur-instant` 이하로 줄이고 상태는 즉시 반영한다 |
|
||||
| 가끔, 세션당 몇 번 | 관찰 후보 | 모달·드로어·토스트 | §1 결정 표의 표준값을 쓴다 |
|
||||
| 드물게, 최초 1회 | 관찰 후보 | 온보딩, 첫 방문 히어로 | 딜라이트 여지가 가장 크다 |
|
||||
|
||||
첫 두 행의 임계값(100회, 수십 회)은 근거 출처가 없는 매직넘버다. 실측해 조정할 시작값으로만 써라. [SKILL-EMIL-DESIGN-ENG]
|
||||
|
||||
**관찰 후보 — 체감 성능은 실제 처리 시간과 다른 축이다.** 같은 대기 시간이라도 회전이 빠르거나 duration이 짧으면 더 빠르게 느껴진다. 이징도 이 지각에 관여한다 — `--ease-out`은 도착이 이르게 느껴지고 `linear`는 기계적으로 느껴진다. 같은 그룹 안에서 반복되는 인터랙션(예: 툴팁 그룹, §3 코드 D 인근)은 두 번째부터 지연·연출을 줄이면 전체가 더 빠르게 느껴진다. [SKILL-EMIL-DESIGN-ENG]
|
||||
|
||||
**프로젝트 계약 — 흩어진 효과보다 오케스트레이션된 한 순간.** 섹션마다 다른 등장 효과를 걸거나 모든 카드에 같은 호버 트랜지션을 반복해 붙이는 것은 AI가 만든 티가 가장 잘 나는 패턴 중 하나다. 페이지 로드 시퀀스 하나, 리빌 하나로 정한 순간이 흩어진 열 개의 작은 효과보다 낫다. [SKILL-FRONTEND-DESIGN][SKILL-IMPECCABLE]
|
||||
|
||||
**프로젝트 계약 — 섹션마다 스크롤 리빌을 거는 것은 기본값이 아니다.** §3 코드 B는 "리빌을 쓰기로 했을 때"의 구현 기본값이지, 모든 섹션에 리빌을 걸라는 뜻이 아니다. 뒤의 "패럴랙스는 섹션마다 다른 말을 해야 한다"의 마지막 줄(움직이지 않는 것도 결정이다)과 같은 원칙이다.
|
||||
|
||||
다섯 번째 **"멋있어서"는 이유가 아니다.** 예외는 브랜드 표현이 브리프의 명시적 요구일 때뿐이고, 그때도 (a) 사용자가 스크롤·호버로 통제하거나 (b) 1회성이며 (c) `prefers-reduced-motion`에서 완전히 사라져야 한다.
|
||||
|
||||
**넣지 말아야 할 곳**: 고빈도 반복 작업(폼·표·필터) · 오류 복구 경로 · 결과가 이미 예측되는 전환(탭) · **첫 화면**(콘텐츠는 즉시 읽혀야 한다) · 숫자가 계속 바뀌는 곳.
|
||||
|
|
@ -79,6 +102,13 @@
|
|||
|
||||
**stagger 시작값 예시**: 이 프로젝트에서는 `(항목수 − 1) × --stagger + duration ≤ 800ms`, 12개 이하를 초기 예산으로 둘 수 있다. 목적·입력 지연·전체 소요·감소 모션에 맞춰 계약을 정하고 실제 렌더에서 확인한다. 방향은 읽기 방향(좌→우, 상→하)과 일치시킨다.
|
||||
|
||||
**각주 넷.**
|
||||
|
||||
1. **프로젝트 계약 — "UI에 ease-in을 절대 쓰지 않는다"는 통념은 진입(entrance)에만 해당한다.** 진입에 감속 없는 가속 곡선을 쓰면 도착이 급정거처럼 보이기 때문이다. 위 표의 퇴장 `--ease-in`(화면 밖으로 나가는 것)은 의도적으로 유지한다 — 나가는 것은 가속해도 자연스럽다. [SKILL-EMIL-DESIGN-ENG]
|
||||
2. **프로젝트 계약 — 트랜지션과 키프레임은 재트리거 가능성으로 고른다.** CSS `transition`은 중간에 끊겨도 현재 값에서 다시 조준(retarget)되지만 `@keyframes`는 중단되면 처음부터 다시 재생된다. 토스트 스태킹, 호버 상태 깜빡임처럼 짧은 간격으로 다시 발생할 수 있는 요소는 `transition`을 기본으로 쓰고, `@keyframes`는 1회성·비재트리거 애니메이션(코드 B의 리빌, 코드 G의 카운트업)에만 쓴다. [SKILL-EMIL-DESIGN-ENG]
|
||||
3. **관찰 후보 — 크로스페이드가 이징·duration 조정으로도 어색하면 아주 작은 블러를 겹친다.** `filter: blur(2px)` 정도를 크로스페이드 구간에만 더하면 전환이 매끄러워 보인다. 블러는 20px 미만으로 제한하고, Safari에서 `filter`가 비싼 속성이라는 점을 §2와 함께 확인한다. [SKILL-EMIL-DESIGN-ENG]
|
||||
4. **관찰 후보 — 시작값. 드로어류에는 별도 이징 곡선을 조건부로 둘 수 있다.** iOS 스타일 드로어가 브리프의 요구일 때만, 드로어 컴포넌트 스코프에 로컬로 `--ease-drawer: cubic-bezier(0.32, 0.72, 0, 1)`을 정의해 쓴다(전역 3단계 토큰에는 추가하지 않는다, 실측 조정). [SKILL-EMIL-DESIGN-ENG]
|
||||
|
||||
---
|
||||
|
||||
## 2. 저비용 기본 선택으로 만들기
|
||||
|
|
@ -110,6 +140,21 @@
|
|||
- `display`·`overlay`의 `transition-behavior: allow-discrete`는 보간하지 않는 선택지다. 다른 속성도 자동 금지가 아니며, 정적 게이트 검출은 측정·계약 검토가 필요한 후보를 알릴 뿐이다
|
||||
- View Transitions의 저자 키프레임은 `opacity`와 transform 계열을 먼저 검토한다. 다른 속성은 목적·비용·입력 간섭·감소 모션·예산을 실제로 확인한 근거가 있을 때만 채택한다
|
||||
|
||||
**프로젝트 계약 — `transition: all`은 쓰지 않는다.** 전환할 속성을 `transition-property`로 명시한다(`transition: scale var(--dur-instant) var(--ease-out), opacity var(--dur-instant) var(--ease-out)`처럼 속성별로 쓰거나 `transition-property: scale, opacity`로 모아 쓴다). `all`은 의도하지 않은 속성(`height`·`box-shadow` 등)까지 전환에 끌어들여 위 표의 저비용 원칙을 조용히 깬다. Tailwind의 `transition-transform`은 `transform`·`translate`·`scale`·`rotate` 넷을 한 번에 잡으므로 그 넷만 전환할 때 쓰고, 다른 속성이 섞이면 대괄호 문법(`transition-[scale,opacity]`)으로 좁힌다. [SKILL-BETTER-UI]
|
||||
|
||||
**`will-change`가 실제로 GPU 합성을 만드는 속성은 셋뿐이다.**
|
||||
|
||||
| 속성 | 합성 가능 | 비고 |
|
||||
|---|---|---|
|
||||
| `transform` | O | 저비용 기본 |
|
||||
| `opacity` | O | 저비용 기본 |
|
||||
| `filter` | O | Safari에서 특히 체감 효과가 크다 |
|
||||
| `clip-path` | 불안정 | 신형 Chromium 한정. 신뢰하지 마라 |
|
||||
| `top`·`left`·`width`·`height` | X | 레이아웃 속성. `will-change`를 걸어도 합성되지 않는다 |
|
||||
| `background`·`border`·`color` | X | 페인트 속성. 합성되지 않는다 |
|
||||
|
||||
**관찰 후보 — 첫 프레임에서 실제로 끊김이 보일 때만 추가한다.** 상시 걸어두면 레이어가 늘어 오히려 느려진다(`preflight.md`·`svg-filters.md`의 경고와 같은 이유). [SKILL-BETTER-UI]
|
||||
|
||||
---
|
||||
|
||||
## 3. CSS 네이티브 우선 — 라이브러리를 끌어오기 전에
|
||||
|
|
@ -122,7 +167,7 @@
|
|||
| 페이지 전환 (View Transitions) | 레이아웃 변화 FLIP (그리드 → 리스트) |
|
||||
| 무한 루프, `linear()` 스프링 근사 | 텍스트 자동 분해, SVG 패스 모핑, 포인터 추종 |
|
||||
|
||||
**지원 현황(2026-08).** `prefers-reduced-motion`·`linear()`는 Baseline widely — 무조건 쓴다. `@starting-style`·`transition-behavior`(2024-08), same-document View Transitions(2025-10)는 Baseline newly — 폴백 두고 쓴다. **scroll-driven animations와 cross-document View Transitions는 Firefox 미지원이라 `@supports` 가드가 필수다.**
|
||||
**지원 현황(확인 2026-09-24, 1차 출처).** `prefers-reduced-motion`·`linear()`는 Baseline widely — 무조건 쓴다. `@starting-style`은 2024-08-06 Baseline newly가 됐고(Firefox 129), same-document View Transitions는 2025-10-14 Baseline newly가 됐다(Firefox 144가 마지막으로 지원) — 둘 다 아직 widely(저 시점부터 30개월)는 아니므로 폴백을 두고 쓴다. **scroll-driven animations(`animation-timeline`)는 Baseline Limited다 — Firefox는 안정판에서 아직 지원하지 않는다**(지원: Chrome/Chrome Android/Edge 115, Safari 26). cross-document View Transitions도 Baseline Limited(Firefox 미지원)다. 두 기능 모두 `@supports` 가드가 필수이고, 가드 **안의 초기 상태가 실제로 콘텐츠를 보이게 하는지**를 반드시 확인한다(아래 "가장 흔한 사고"). [WEB-BASELINE]
|
||||
|
||||
> **가장 흔한 사고**: 미지원 브라우저에서 `opacity: 0`이 남아 콘텐츠가 영영 안 보이는 것. **초기 상태를 `@supports` 블록 *안에* 넣어라.** 밖에 두면 Firefox에서 백지가 된다.
|
||||
|
||||
|
|
@ -167,7 +212,7 @@
|
|||
|
||||
### 코드 B — 스크롤 진입 리빌 (scroll-driven animation)
|
||||
|
||||
> 언제: **기본값.** 리빌·진행 바·시차·헤더 축소. 컴포지터 스레드에서 돌아 메인 스레드가 막혀도 끊기지 않는다. 게이트 #10의 정답이 이것이다.
|
||||
> 언제: **스크롤 리빌을 쓰기로 했을 때의 구현 기본값.** 리빌 자체를 걸지는 §0("섹션마다 스크롤 리빌을 거는 것은 기본값이 아니다")에서 먼저 정한다. 쓰기로 했다면 진행 바·시차·헤더 축소를 포함해 이 방식이 기본이다 — 컴포지터 스레드에서 돌아 메인 스레드가 막혀도 끊기지 않는다. **scroll-driven animations는 Baseline Limited다. Firefox 안정판이 지원하지 않으므로**(§3 도입부) `@supports` 가드 없이 쓰면 Firefox에서 리빌이 영영 실행되지 않고, 초기 상태를 가드 밖에 두면 콘텐츠가 사라진 채로 남는다. '애니메이션·스크롤 로직이 입력을 방해하지 않는다' 하드 게이트의 정답이 이것이다. [WEB-BASELINE]
|
||||
|
||||
```css
|
||||
/* 초기 상태를 @supports 안에 둔다 — 미지원 브라우저에서는 그냥 보인다 */
|
||||
|
|
@ -184,7 +229,7 @@
|
|||
to { opacity: 1; translate: 0 0; }
|
||||
}
|
||||
}
|
||||
/* 읽기 진행 바 — width가 아니라 scaleX (게이트 #8) */
|
||||
/* 읽기 진행 바 — width가 아니라 scaleX (§2 저비용 속성 원칙) */
|
||||
@supports (animation-timeline: scroll()) {
|
||||
.progress {
|
||||
position: fixed; inset-block-start: 0; inset-inline: 0; height: 3px;
|
||||
|
|
@ -237,9 +282,48 @@ if (matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
|||
}
|
||||
```
|
||||
|
||||
### 코드 C-1 — clip-path를 애니메이션할 때
|
||||
|
||||
> 언제: 형태 자체가 잘려야 하는 경우만. 단순 리빌(텍스트·카드 등장)은 여전히 `overflow: hidden` 래퍼 안 자식의 `translate`가 저비용 기본이다(§2 표, 코드 F의 각주). 이 소절은 그 대안이 아니라 **다른 문제**를 푼다 — 탭 배경이 선택된 탭의 모양으로 바뀌거나, 눌러서 채우는 확인, 두 이미지를 가르는 슬라이더처럼 **경계선 자체가 움직여야** 할 때다. [SKILL-EMIL-DESIGN-ENG]
|
||||
|
||||
`clip-path`는 GPU 합성이 불안정하다(§2 will-change 표). 아래 레시피는 실제 렌더에서 프레임을 재보고, 끊기면 `mask-image`나 두 겹 이미지 + `overflow: hidden`으로 되돌린다.
|
||||
|
||||
```css
|
||||
/* 탭 배경 전환 — 선택된 탭 아래로 배경이 미끄러져 들어온다.
|
||||
JS가 선택된 탭의 실제 rect를 읽어 --tab-x/--tab-w를 갱신한다 */
|
||||
.tabs { position: relative; }
|
||||
.tabs__bg {
|
||||
position: absolute; inset: 0; background: var(--accent);
|
||||
clip-path: inset(0 calc(100% - var(--tab-x) - var(--tab-w)) 0 var(--tab-x) round var(--radius-pill));
|
||||
transition: clip-path var(--dur-normal) var(--ease-soft);
|
||||
}
|
||||
|
||||
/* hold-to-confirm — 누르는 동안(결정)은 느리게 채우고, 놓으면(해제)은 항상 빠르게 되감는다.
|
||||
"누르는 동작은 느리게, 해제는 항상 빠르게"라는 비대칭 원칙의 구체 사례다(preflight.md의 되돌림 원칙과 연결) */
|
||||
.confirm { position: relative; }
|
||||
.confirm::after {
|
||||
content: ''; position: absolute; inset: 0; background: var(--ink);
|
||||
clip-path: inset(0 100% 0 0);
|
||||
transition: clip-path calc(var(--dur-quick) * 0.65) var(--ease-in); /* 해제 스냅백 */
|
||||
}
|
||||
.confirm:active::after { clip-path: inset(0 0 0 0); transition: clip-path 2s linear; } /* 실측 조정 */
|
||||
|
||||
/* 비교 슬라이더 — 두 이미지를 가르는 경계 자체가 움직인다. --split은 드래그로 갱신 */
|
||||
.compare__after { clip-path: inset(0 0 0 var(--split, 50%)); }
|
||||
|
||||
/* 이미지 리빌 — 사각형이 아니라 비정형 경계로 드러나야 할 때 */
|
||||
.reveal-shape {
|
||||
clip-path: polygon(0 0, 0 0, 0 100%, 0 100%);
|
||||
transition: clip-path var(--dur-slow) var(--ease-out);
|
||||
}
|
||||
.reveal-shape[data-shown] { clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%); }
|
||||
```
|
||||
|
||||
**관찰 후보.** 네 레시피 모두 §5 모션 QA로 실제 기기에서 프레임을 확인한다. [SKILL-EMIL-DESIGN-ENG]
|
||||
|
||||
### 코드 D — 모달·팝오버 진입/퇴장 (`@starting-style`)
|
||||
|
||||
> 언제: `display: none`에서 나타나거나 top layer에 올라가는 모든 것. 라이브러리가 필요 없다.
|
||||
> 언제: `display: none`에서 나타나거나 top layer에 올라가는 모든 것. 라이브러리가 필요 없다. (`@starting-style`은 2024-08-06 Baseline newly — §3 도입부. [WEB-BASELINE])
|
||||
|
||||
```html
|
||||
<button popovertarget="tip">도움말</button>
|
||||
|
|
@ -270,11 +354,38 @@ if (matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
|||
@media (prefers-reduced-motion: reduce) { .tip { scale: 1; } } /* 이동은 --shift-sm이 0이 되며 자동 처리 */
|
||||
```
|
||||
|
||||
**팝오버는 클릭한 곳에서 나와야 한다.** 화면 정중앙에서 페이드인하는 팝오버는 "어디서 왔는지"를 버리는 것이다. `transform-origin`을 트리거 위치로 잡아라.
|
||||
**프로젝트 계약 — `scale(0)`에서 시작하지 않는다.** 0에서 커지는 진입은 평평했다가 갑자기 부풀어 보인다. `scale(0.9)` 이상에서 시작하고 항상 `opacity`와 함께 쓴다 — 위 코드의 `scale: 0.96`이 그 예다. [SKILL-EMIL-DESIGN-ENG]
|
||||
|
||||
**팝오버는 클릭한 곳에서 나와야 한다.** 화면 정중앙에서 페이드인하는 팝오버는 "어디서 왔는지"를 버리는 것이다. `transform-origin`을 트리거 위치로 잡아라. **모달은 예외다** — 뷰포트 중앙에 고정되므로 `transform-origin: center`를 유지한다. [SKILL-EMIL-DESIGN-ENG]
|
||||
|
||||
**관찰 후보 — 툴팁 그룹은 첫 번째만 지연한다.** 같은 그룹의 인접 트리거 사이를 연속으로 오갈 때마다 매번 딜레이·트랜지션을 다시 타면 굼떠 보인다. 첫 툴팁이 열린 뒤 일정 시간 안에 그룹 내 다른 트리거로 옮기면 지연·애니메이션 없이 즉시 연다.
|
||||
|
||||
```js
|
||||
let groupTimer = null;
|
||||
group.addEventListener('pointerenter', ({ target }) => {
|
||||
if (!target.closest('[data-tip-trigger]')) return;
|
||||
if (groupTimer) group.dataset.tipInstant = ''; // 그룹이 방금 열려 있었으면 두 번째부터 즉시 연다
|
||||
clearTimeout(groupTimer);
|
||||
groupTimer = setTimeout(() => { // 1500ms 는 실측 조정 시작값
|
||||
delete group.dataset.tipInstant;
|
||||
groupTimer = null; // 창이 닫히면 다음 첫 툴팁은 다시 지연한다
|
||||
}, 1500);
|
||||
}, true); // pointerenter 는 버블링되지 않으므로 캡처 단계에서 그룹이 받는다
|
||||
```
|
||||
|
||||
```css
|
||||
[data-tip-instant] .tip { transition-delay: 0s; transition-duration: 0s; }
|
||||
```
|
||||
|
||||
[SKILL-EMIL-DESIGN-ENG]
|
||||
|
||||
**프로젝트 계약 — 모달 스크림·비차단 패널·스택 시트는 디밍이 다르다.** 모달성 과업은 배경을 어둡게 하는 스크림(`::backdrop` 또는 별도 레이어)과 함께 배경을 살짝 뒤로 후퇴시킨다. 사이드바처럼 흐름을 막지 않고 나란히 떠 있는 패널은 스크림 없이 반투명·오프셋만으로 존재감을 준다. 시트가 겹겹이 쌓이면 열릴 때마다 이전 레이어를 한 단계씩 더 어둡고 더 뒤로 민다. [SKILL-APPLE-DESIGN]
|
||||
|
||||
호버·포커스로 나타나는 콘텐츠(위 `.tip` 같은 툴팁)가 해제 가능·호버 유지·지속이라는 WCAG 1.4.13 조건을 만족하는지는 [accessibility.md](accessibility.md)에서 확인한다. [WCAG-HOVER]
|
||||
|
||||
### 코드 E — 페이지·뷰 전환 (View Transitions API)
|
||||
|
||||
> 언제: 라우트 이동, 리스트 필터링, 썸네일 → 상세. 미지원 브라우저에서는 즉시 바뀐다(점진 향상).
|
||||
> 언제: 라우트 이동, 리스트 필터링, 썸네일 → 상세. 미지원 브라우저에서는 즉시 바뀐다(점진 향상). same-document View Transitions는 2025-10-14 Baseline newly가 됐다(§3 도입부) — 아직 widely는 아니므로 아래 `transition()` 래퍼의 폴백이 실제 경로다. [WEB-BASELINE]
|
||||
|
||||
```js
|
||||
/** DOM을 바꾸는 함수를 감싼다. 지원·모션감소 판정을 여기 한 곳에 모은다 */
|
||||
|
|
@ -332,7 +443,8 @@ filterBtn.addEventListener('click', () => transition(() => renderList(filterBtn.
|
|||
```
|
||||
|
||||
```css
|
||||
.split__line { display: block; overflow: hidden; } /* 마스크. clip-path를 애니메이션하지 않는다 */
|
||||
.split__line { display: block; overflow: hidden; } /* 마스크. 줄 리빌은 overflow 마스크 + translate가 기본이다.
|
||||
형태 자체를 잘라야 하는 경우만 코드 C-1(clip-path)을 쓴다 */
|
||||
.split__line > span { display: block; translate: 0 0; }
|
||||
|
||||
@supports (animation-timeline: view()) {
|
||||
|
|
@ -352,10 +464,12 @@ filterBtn.addEventListener('click', () => transition(() => renderList(filterBtn.
|
|||
|
||||
**자동 분해가 꼭 필요하면** GSAP SplitText를 쓴다(§4, 무료). `type: 'lines,words'`까지만, `mask: 'lines'`, `autoSplit: true`. 글자 단위가 브리프의 요구라면 원문을 `.sr-only` 사본으로 남기고 시각 요소에 `aria-hidden="true"`를 건다.
|
||||
|
||||
**관찰 후보 — 시작값. 단어 단위 스플릿도 옵션이다.** 줄 단위 대신 단어 단위(스태거 약 80ms 시작값)로 쪼개는 스타일도 있다 — 제목처럼 짧고 리듬을 강조하고 싶을 때다. `--stagger` 토큰(60ms, tokens.md §4)은 그대로 유지하고, 단어 개수가 많으면 위 §1의 스태거 예산식으로 총 소요 상한을 확인한다. [SKILL-BETTER-UI]
|
||||
|
||||
### 코드 G — 숫자 카운트업 (자릿수 스트립, transform만)
|
||||
|
||||
> 언제: 실적 숫자 하나. **사용자가 준 진짜 숫자에만 쓴다** — 지어낸 숫자는 게이트 #11이다.
|
||||
> `@property --count` 방식은 커스텀 속성을 매 프레임 바꿔 게이트 #8에 걸린다. 자릿수를 굴려라.
|
||||
> 언제: 실적 숫자 하나. **사용자가 준 진짜 숫자에만 쓴다** — 지어낸 숫자는 '사용자가 주지 않은 수치를 그럴듯한 값으로 넣지 않는다' 하드 게이트에 걸린다.
|
||||
> `@property --count` 방식은 커스텀 속성을 매 프레임 바꿔 저비용 속성(transform/opacity) 밖의 속성을 애니메이션하게 된다. 자릿수를 굴려라.
|
||||
|
||||
```html
|
||||
<p class="stat">
|
||||
|
|
@ -428,6 +542,29 @@ WCAG 2.3.3은 **인터랙션으로 촉발된 모션 애니메이션을 끌 수
|
|||
|
||||
JS 쪽 판정도 한 곳에 모은다. 설정이 도중에 바뀌면 반영한다: `matchMedia('(prefers-reduced-motion: reduce)').addEventListener('change', () => location.reload())`.
|
||||
|
||||
### 코드 I — 테마 전환 시 트랜지션 억제
|
||||
|
||||
> 언제: 라이트·다크를 런타임에 토글하는 프로젝트 전부. 넣지 않으면 테마가 바뀌는 순간 색·그림자·테두리 트랜지션이 동시에 발화해 화면 전체가 번져 보인다.
|
||||
|
||||
OS 설정이나 인앱 토글로 테마가 바뀔 때, 전역에 `transition: none`을 순간적으로 주입하고 강제 리플로우한 뒤 다음 프레임에 제거한다.
|
||||
|
||||
```js
|
||||
function setTheme(next) {
|
||||
const css = document.createElement('style');
|
||||
css.textContent = '*,*::before,*::after{transition:none!important}';
|
||||
document.head.appendChild(css);
|
||||
|
||||
document.documentElement.dataset.theme = next; // 실제 토큰 전환
|
||||
|
||||
document.body.offsetHeight; // 강제 리플로우 — 주입한 스타일을 확정시킨다
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => css.remove()));
|
||||
}
|
||||
```
|
||||
|
||||
`next-themes` 같은 라이브러리를 쓴다면 `disableTransitionOnChange` 옵션이 같은 일을 기본 제공한다.
|
||||
|
||||
**프로젝트 계약.** [SKILL-BETTER-UI]
|
||||
|
||||
---
|
||||
|
||||
## 패럴랙스는 섹션마다 다른 말을 해야 한다
|
||||
|
|
@ -717,6 +854,97 @@ const progress = clamp01((start - rect.top) / travel);
|
|||
|
||||
> 판단: GSAP은 더 이상 "돈 때문에 못 쓰는 라이브러리"가 아니다. 스크롤 시퀀스나 텍스트 분해가 필요하면 주저 없이 쓴다. 다만 **필요 없으면 여전히 깔지 않는다.**
|
||||
|
||||
### 정리 규율 — 라이브러리를 쓰면 반드시 해제한다
|
||||
|
||||
**프로젝트 계약.** 라우트를 떠나거나 컴포넌트가 언마운트될 때 인스턴스를 정리하지 않으면 리스너와 타임라인이 누적돼 메모리와 프레임이 새어나간다.
|
||||
|
||||
| 라이브러리 | 정리 호출 |
|
||||
|---|---|
|
||||
| GSAP 타임라인·ScrollTrigger | `gsap.context(fn, scope)`로 스코프를 잡고 언마운트 시 `ctx.revert()` |
|
||||
| GSAP SplitText | `split.revert()` — 원본 DOM 구조로 되돌린다 |
|
||||
| WAAPI | `animation.cancel()` |
|
||||
| IntersectionObserver | `observer.disconnect()`(§3 코드 C·G에 이미 적용) |
|
||||
| Lenis | `lenis.destroy()` |
|
||||
|
||||
[SKILL-INTERACTION-DESIGN][SKILL-EMIL-DESIGN-ENG]
|
||||
|
||||
### WAAPI — 번들 없이 명령형 제어
|
||||
|
||||
```js
|
||||
const anim = el.animate(
|
||||
[
|
||||
{ opacity: 0, translate: '0 var(--shift-lg)' },
|
||||
{ opacity: 1, translate: '0 0' },
|
||||
],
|
||||
{ duration: 350, easing: 'cubic-bezier(0.22, 1, 0.36, 1)', fill: 'both' }
|
||||
);
|
||||
anim.finished.then(() => { /* 완료 후 처리 */ }).catch(() => {});
|
||||
|
||||
// 언마운트하거나 다시 요청받으면 반드시 취소한다
|
||||
anim.cancel();
|
||||
```
|
||||
|
||||
### GSAP 타임라인 + ScrollTrigger 최소 골격 (바닐라 JS)
|
||||
|
||||
React를 전제하지 않아 어떤 프레임워크에도 옮길 수 있는 최소 형태다. `gsap.context()`로 스코프를 잡고 정리 함수를 반환한다.
|
||||
|
||||
```js
|
||||
import gsap from 'gsap';
|
||||
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
function mountHero(root) {
|
||||
const ctx = gsap.context(() => {
|
||||
const tl = gsap.timeline({
|
||||
scrollTrigger: { trigger: root, start: 'top 80%', once: true },
|
||||
});
|
||||
// GSAP은 CSS 변수를 직접 읽지 않으므로 토큰 값과 같은 숫자를 쓴다
|
||||
tl.from('.hero__title', { opacity: 0, y: 24, duration: 0.6, ease: 'power2.out' }) // 0.6 = --dur-slow, 'power2.out' = --ease-out에 대응하는 GSAP 이징
|
||||
.from('.hero__sub', { opacity: 0, y: 16, duration: 0.35, ease: 'power2.out' }, '-=0.3'); // 0.35 = --dur-normal
|
||||
}, root);
|
||||
|
||||
return () => ctx.revert(); // 언마운트 시 반드시 호출
|
||||
}
|
||||
```
|
||||
|
||||
[SKILL-INTERACTION-DESIGN]
|
||||
|
||||
### Motion(Framer) 쓸 때 주의 둘
|
||||
|
||||
- **관찰 후보 — `x`·`y`·`scale` 축약 속성은 그 자체로 하드웨어 가속이 아니다.** Motion은 메인 스레드의 `requestAnimationFrame`으로 이 값들을 계산해 매 프레임 `transform` 문자열로 합성한다. 실제 GPU 합성 여부는 최종적으로 만들어지는 `transform` 값과 `will-change`(§2 표) 조합에 달려 있지, 축약 속성 문법 자체가 보장하지 않는다. [SKILL-EMIL-DESIGN-ENG]
|
||||
- **관찰 후보 — `AnimatePresence`는 `initial={false}`로 첫 렌더 진입 애니메이션을 끈다.** 마운트마다 진입 연출이 발화하면 페이지를 새로고침할 때마다 불필요하게 움직인다. 단, 스태거드 히어로처럼 `initial` 자체가 최초 1회 연출을 담당하는 곳에는 적용하지 않는다 — 새로고침에서도 올바르게 보이는지 확인한다. [SKILL-BETTER-UI]
|
||||
|
||||
### 경량 CSS 3D — WebGL 게이트를 넘지 않는 작은 회전
|
||||
|
||||
로고 궤도, 카드 뒤집기처럼 작은 3D 연출은 `three.md`의 WebGL 채택 게이트를 넘지 않고도 만들 수 있다.
|
||||
|
||||
```css
|
||||
.flip-card { perspective: 800px; }
|
||||
.flip-card__inner {
|
||||
transform-style: preserve-3d;
|
||||
transition: transform var(--dur-normal) var(--ease-soft);
|
||||
}
|
||||
.flip-card[data-flipped] .flip-card__inner { transform: rotateY(180deg); }
|
||||
.flip-card__face--back { transform: rotateY(180deg); backface-visibility: hidden; }
|
||||
```
|
||||
|
||||
[SKILL-EMIL-DESIGN-ENG]
|
||||
|
||||
### 스크롤 핸들러는 passive + 일정화한다
|
||||
|
||||
`addEventListener('scroll', ...)`이 §3 코드 C처럼 정말 필요할 때(IntersectionObserver나 scroll-driven animation으로 안 풀릴 때)만 쓴다. 매 스크롤 이벤트가 아니라 다음 페인트 한 번으로 묶는다.
|
||||
|
||||
```js
|
||||
let ticking = false;
|
||||
window.addEventListener('scroll', () => {
|
||||
if (ticking) return;
|
||||
ticking = true;
|
||||
requestAnimationFrame(() => { handleScroll(); ticking = false; });
|
||||
}, { passive: true });
|
||||
```
|
||||
|
||||
**프로젝트 계약.** [SKILL-INTERACTION-DESIGN]
|
||||
|
||||
### Lenis (스무스 스크롤) — 논쟁이 있다. 양쪽을 알고 결정해라
|
||||
|
||||
**반대**: 스크롤은 사용자가 기대하는 기기 고유의 물리다. 바꾸는 건 시스템 관습 침해다. 관성이 붙으면 **정확한 위치에 멈추기 어렵고** 운동 장애가 있는 사용자에게 치명적이다. 지연은 모든 사용자에게 인지 비용이다.
|
||||
|
|
@ -747,6 +975,8 @@ const progress = clamp01((start - rect.top) / travel);
|
|||
- [ ] 첫 화면 콘텐츠가 애니메이션 없이 즉시 읽힌다
|
||||
- [ ] 퇴장 duration과 stagger 총 소요가 프로젝트 계약에 맞고, 입력 지연·감소 모션을 실제로 확인했다
|
||||
- [ ] 대표 과업에서 동시 모션이 시선·클릭·키 입력을 방해하지 않는지 실제로 확인했다
|
||||
- [ ] 모션 QA를 실제로 돌렸다 — duration을 2~5배로 늘리거나 DevTools Animations 패널에서 프레임 단위로 재생해 색 전환·이징·transform-origin·속성 동기화 4항목을 확인했다 [SKILL-APPLE-DESIGN][SKILL-EMIL-DESIGN-ENG][SKILL-IMPECCABLE]
|
||||
- [ ] 실기기 또는 CPU 스로틀링(DevTools Performance)에서 한 번은 확인했다. 증거 출처(에뮬레이션/실기기, 엔진명 — Chromium≠Safari)를 `design.md`에 남겼다 [SKILL-IMPECCABLE]
|
||||
|
||||
**접근성 — 타협 없음**
|
||||
- [ ] `prefers-reduced-motion: reduce`를 켜고 실제로 확인했다. **전부 꺼지지 않고** 위치 이동·시차·루프만 죽고 페이드·상태 변화·진행 표시는 남는다
|
||||
|
|
@ -756,10 +986,13 @@ const progress = clamp01((start - rect.top) / travel);
|
|||
- [ ] 카운트업의 최종값이 스크린리더에 노출된다
|
||||
- [ ] 포커스 링이 애니메이션되지 않고 즉시 보인다. Tab 순서가 시각 순서와 일치하고, 전환 후 포커스가 새 콘텐츠를 따라간다
|
||||
- [ ] 호버에만 있는 정보가 없다 (터치·키보드에서 접근 불가)
|
||||
- [ ] 자동 재생이든 인터랙션 트리거든 초당 3회 넘게 깜빡이지 않는다 (WCAG 2.3.1). 세부 판정은 [accessibility.md](accessibility.md) [WCAG-FLASH]
|
||||
|
||||
**성능**
|
||||
- [ ] 모션 라이브러리 추가분이 `tokens.md` §5 예산 안이다. 넘겼으면 올린 이유를 명시했다
|
||||
- [ ] 상시 `will-change` 요소의 레이어 메모리와 실제 기기 성능이 프로젝트 예산 안임을 확인했다
|
||||
- [ ] 상시 `will-change` 요소의 레이어 메모리와 실제 기기 성능이 프로젝트 예산 안임을 확인했다(§2 will-change 표)
|
||||
- [ ] 드래그·제스처가 있다면 입력 경로 지연을 감사했다 — 기준과 방법은 [interaction-feel.md](interaction-feel.md)
|
||||
- [ ] GSAP·Motion·Lenis·SplitText·IntersectionObserver 등 이 문서에서 쓴 라이브러리 인스턴스가 이탈 시 정리된다(§4 정리 규율 표)
|
||||
- [ ] Lenis를 썼다면 §4의 입력·키보드·포커스·앵커·감소 모션·예산 검증을 실제로 마쳤다
|
||||
- [ ] 6단계 `design.md`에 **채택한 모션 · 안 쓰기로 한 모션과 그 이유**를 적었다
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
- **React·Vue·Astro 등 프레임워크 프로젝트 → 프로덕션 빌드 결과에 돌린다.** 소스만 보면 렌더된 DOM 이 없어 대비·가로 스크롤·LCP 를 잴 수 없다. `pnpm build && pnpm preview` 로 띄우고 그 URL 을 검사해라
|
||||
- **소스만 보고 "측정 불가"로 남기지 마라.** 미측정은 통과가 아니다
|
||||
|
||||
grep 검사는 **주석과 문자열을 제외**해라. "100vh 금지"라고 쓴 주석이 게이트 #9 에 걸리는 오탐이 실제로 나왔다.
|
||||
grep 검사는 **주석과 문자열을 제외**해라. "100vh 금지"라고 쓴 주석이 '지원 범위의 뷰포트에서 의도하지 않은 가로 스크롤·겹침·조작 불능이 없다' 하드 게이트 검사에 걸리는 오탐이 실제로 나왔다.
|
||||
|
||||
사람 눈보다 정확하고 빠르다. 셋 다 돌려라.
|
||||
|
||||
|
|
@ -65,6 +65,22 @@ canvas { display: none !important; }
|
|||
| 모션 감소 | `prefers-reduced-motion` 켜고 확인 | **전부 끄는 게 정답이 아니다** — 위치 이동·시차·자동재생을 죽이고, 페이드·상태 변화는 남긴다 |
|
||||
| 자동재생 | 확인 | 5초 이상 반복되는 애니메이션에는 정지 수단이 있다 |
|
||||
| 캔버스/WebGL 실패 | JS 끄고 확인 | 콘텐츠가 여전히 보인다 |
|
||||
| 네이티브 요소 | 소스 확인 | 이동은 `<a href>`, 액션은 `<button>`, 상태 없는 클릭 영역에 `<div onClick>`을 쓰지 않는다. 근거·예외는 [accessibility.md](accessibility.md) [SKILL-BETTER-A11Y] |
|
||||
| 아이콘 버튼 이름 · Label in Name | 소스 확인 | 텍스트 라벨 없는 버튼·링크는 `aria-label` 또는 숨김 텍스트 필수, 장식 아이콘은 `aria-hidden="true"`. 보이는 라벨이 있으면 접근 가능한 이름이 그 텍스트를 포함한다 [WCAG-LABEL-NAME] [SKILL-WEB-A11Y] |
|
||||
| 포커스 가림 | Tab 순회, 스티키 헤더·CTA 바 확인 | 포커스된 요소가 고정 바에 완전히 가려지지 않는다. `scroll-margin-top`을 헤더 높이 토큰에 연결 [WCAG-FOCUS-OBSCURED] |
|
||||
| forced-colors | Windows 고대비 모드로 확인 | 강제 색상 모드에서도 컨트롤 경계·상태가 구분된다(Baseline: 2022-09~ 전 주요 브라우저 [WEB-BASELINE]). `forced-color-adjust: none`은 검증된 곳에서만 [SKILL-BETTER-A11Y] |
|
||||
| 스킵 링크 · 랜드마크 | 첫 Tab, 소스 확인 | 반복 내비를 건너뛰는 링크가 첫 포커스 요소, 보이는 `<main>` 랜드마크 하나. 세부는 [accessibility.md](accessibility.md) [SKILL-WEB-A11Y] [SKILL-BETTER-A11Y] |
|
||||
| 키보드 위젯 패턴 | 실제 키보드 조작 | 탭·메뉴버튼·콤보박스·리스트박스·다이얼로그는 WAI-ARIA APG 패턴을 따른다. 세부는 [accessibility.md](accessibility.md) [ARIA-APG] |
|
||||
| 폼 — autocomplete·오류 | 소스·조작 확인 | autocomplete 값이 필드 의미와 맞고(무표시는 하드 게이트), 오류는 필드 옆에서 수정법을 말한다. 세부는 [accessibility.md](accessibility.md) [WCAG-INPUT-PURPOSE] [SKILL-BETTER-A11Y] |
|
||||
| **[프로젝트 계약]** 폼 — 제출 버튼 | 소스·조작 확인 | 제출 버튼은 요청 시작 전까지 비활성화하지 않으며, `disabled`와 `aria-disabled`를 구분해 쓴다. 세부는 [accessibility.md](accessibility.md) [SKILL-BETTER-A11Y] |
|
||||
| 상태 메시지 | 조작 후 확인 | 포커스 이동 없이 전달되는 상태 변화도 role·속성으로 보조기술에 전달된다. 라이브 리전 선택 기준은 [accessibility.md](accessibility.md) [WCAG-STATUS] [SKILL-BETTER-A11Y] |
|
||||
| 깜빡임 | 초당 프레임 확인 | 1초에 3회를 넘게 번쩍이거나 일반·적색 섬광 한계를 넘는 콘텐츠가 없다 [WCAG-FLASH] |
|
||||
| 호버 콘텐츠 | 마우스오버 확인 | 호버·포커스로 나타나는 콘텐츠는 해제 가능·호버 가능·계속 보임 세 조건을 만족한다 [WCAG-HOVER] |
|
||||
| 드래그 대안 | 조작 확인 | 드래그로만 되는 기능은 단일 포인터 조작(탭 등)으로도 된다. 예외는 필수 드래그·UA 기본 스크롤 [WCAG-DRAG] |
|
||||
| 잘린 텍스트 도달 수단 | 소스·렌더 확인 | `text-overflow: ellipsis` 등으로 시각적으로 잘린 텍스트·표 셀·태그에는 title·툴팁·상세보기 같은 전체 값 도달 수단이 있다 [SKILL-BETTER-INTERFACE] |
|
||||
| **[프로젝트 계약]** 비활성 · placeholder 대비 | 렌더 확인 | 비활성 컨트롤은 WCAG 대비 예외 대상이지만, 활성처럼 보여 사용자를 속이지 않을 만큼은 구분한다. placeholder는 라벨이 아니며 지나치게 옅어 읽을 수 없게 두지 않는다 [SKILL-BETTER-COLORS] |
|
||||
|
||||
이 표에 새로 더한 행은 비활성/placeholder 대비(별도 표시한 프로젝트 계약)를 빼면 모두 하드 게이트다. forced-colors 도 하드 게이트다 — 판정 근거는 [accessibility.md](accessibility.md) §12. 대비가 실패로 나오면 수정 절차(색상환 고정·명도부터 조정)는 [color.md](color.md)를 따른다.
|
||||
|
||||
> **대비를 스크립트로 잴 때 — 반투명 배경을 불투명으로 계산하지 마라.**
|
||||
> `getComputedStyle(el).backgroundColor` 로 조상을 거슬러 올라가며 "투명이 아닌 첫 값"을 배경으로 쓰면,
|
||||
|
|
@ -140,7 +156,7 @@ canvas { display: none !important; }
|
|||
- [ ] hero 검증만으로 text-media 분할 검증을 끝내지 않는다. 과정·추천·가맹처럼 DOM/여백 규칙이 다른 모든 분할 표면에서 rail·copy·media·제목의 실제 rect와 줄 수를 기록한다. wide override가 있다면 기존 `padding-left/right: calc(100vw …)`를 **양쪽 모두** 재설정했는지, 상위 `max-width`의 선택자 우선순위가 named stage를 다시 줄이지 않는지 확인한다
|
||||
- [ ] 중간 뷰포트(768~1024px)에서 레이아웃이 깨지지 않음 — **가장 자주 빠뜨리는 구간**
|
||||
- [ ] 터치 타깃: **버튼·아이콘·카드 등 독립 컨트롤은 44×44px 이상**(Apple/Google 권고)
|
||||
- [ ] 터치 타깃: **본문 안 인라인 텍스트 링크는 24×24px 이상**(WCAG 2.5.8 AA). 여기에 44 를 요구하면 정상적인 내비 링크가 오탐된다
|
||||
- [ ] 터치 타깃: **본문 안 인라인 텍스트 링크는 24×24px 이상** — 이 스킬의 프로젝트 계약(`inlineTargetMin` 24px)이다. WCAG 2.5.8은 문장 속 인라인 링크를 예외로 둔다. 여기에 44 를 요구하면 정상적인 내비 링크가 오탐된다
|
||||
- [ ] **컨트롤의 내용 여백**: 버튼 라벨·입력값·select 표시·표 셀의 실제 렌더 글자가 테두리에 닿지 않는다. 이 저장소의 실렌더 기본값은 좌우 **8 CSS px** 이상이다. 이는 WCAG 적합성 수치가 아니라 검증 가능한 시각 품질 기본값이며, 아이콘 전용·체크박스·의도적인 데이터 정렬은 별도 근거를 적는다
|
||||
- [ ] **폼 기하**: 연결된 label/name/autocomplete를 확인한 뒤 실제 렌더 rect로 입력끼리 겹치지 않는지 잰다. 좁은 폭에서는 필드를 세로로 쌓고, label·입력·오류 문장이 서로의 클릭/읽기 영역을 침범하지 않게 한다
|
||||
- [ ] **표와 표처럼 보이는 flex/grid**: 데이터 관계에는 `th`/`td`와 `scope`를 쓰고, 모든 가시 셀에 좌우 읽기 여백을 둔다. 좁은 폭에서 스크롤·카드화로 형식이 바뀌어도 헤더-값 관계가 남아야 한다
|
||||
|
|
@ -172,7 +188,12 @@ canvas { display: none !important; }
|
|||
- [ ] **파괴적 행동에는 되돌림이 있다** — 삭제·취소는 실행취소 또는 확인. 되돌리기가 확인보다 낫다(흐름이 끊기지 않는다). 재신청·복원은 원래 판정 함수로 다시 심사한다 — 사이에 끼어든 다른 변화가 있으면 정당하게 막혀야 한다
|
||||
- [ ] **열리는 것은 Esc 로 닫힌다** — 모달만이 아니다. 드롭다운·팝오버·알림 메뉴도. 닫힐 때 포커스는 연 요소로 돌아간다
|
||||
- [ ] native `<dialog>` 또는 동등 모달은 전역 reset 뒤에도 `position: fixed; inset: 0; margin: auto`와 viewport 상한/내부 scroll owner를 가진다. 390·1440·2560에서 중심점, 화면 안 rect, 배경 scroll lock을 실제로 단언한다. W3C의 modal dialog 키보드·포커스·닫기 계약을 따른다: [WAI-ARIA APG](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/), [MDN dialog](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/dialog)
|
||||
- [ ] **[프로젝트 계약]** 모달 액션 행은 스크롤 영역과 분리된 스티키 푸터에 둔다 — 콘텐츠가 스크롤되어도 확인·취소 같은 핵심 액션은 항상 보인다. 리사이즈 가능한 패널 바닥이나 고정 높이 모달의 폴드 아래처럼 잘릴 수 있는 자리에 크리티컬 액션을 두지 않는다 [SKILL-BETTER-LAYOUT]
|
||||
- [ ] **로딩 상태는 실제 비동기에만** — 동기 인라인 데이터에 스켈레톤을 붙이는 건 저장을 흉내내는 것이다. 없는 지연을 만들지 마라
|
||||
- [ ] **[프로젝트 계약]** 변형 상태 매트릭스 — 새 컴포넌트·variant는 hover·focus·active·disabled·loading·selected 여섯 상태를 갖추었는지 표로 확인한다. 일부 상태만 구현하고 넘어가는 것이 가장 흔한 누락이다 [SKILL-INTERFACE-REVIEW]
|
||||
- [ ] **[프로젝트 계약]** 오류·빈 상태 문구는 "존재"만으로 충분하지 않다 — 무엇이 잘못됐는지·어떻게 고치는지를 비난하지 않는 톤으로 말하는 내용 기준은 [product-copy.md](product-copy.md)를 따른다 [SKILL-BETTER-WRITING]
|
||||
- [ ] **[하드 게이트]** 낙관적 업데이트는 실패하면 되돌린다 — 즉시 반응이 필요한 저위험 토글(좋아요·북마크 등)에 낙관적 업데이트를 쓸 때는 실패 시 같은 판정 함수로 원상 롤백하고, 실패를 토스트·인라인 오류로 알린다. "판정은 순수 함수다"(위) 원칙과 연결된다 [SKILL-INTERACTION-DESIGN](근거: SKILL.md 하드 게이트 5 — 실패를 성공으로 남기지 않는다)
|
||||
- [ ] **[프로젝트 계약]** 제스처·모션은 실제로 조작해 QA한다 — 커스텀 컨트롤의 제스처 QA는 [interaction-feel.md](interaction-feel.md) §11, 모션 QA(배속 재생·프레임 단위 점검)는 [motion.md](motion.md) §5를 따른다 [SKILL-EMIL-DESIGN-ENG] [SKILL-IMPECCABLE]
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -216,6 +237,9 @@ canvas { display: none !important; }
|
|||
4. **사용한 레퍼런스의 원칙이 브리프와 렌더 결과에 맞게 번역됐는가?** → 슬롯 수나 업종 차이 자체로 판단하지 않는다
|
||||
5. **강조색·radius·정렬·반복이 프로젝트 토큰과 과업에 맞는가?**
|
||||
6. **여백이 의도적인가, 남은 것인가?**
|
||||
7. **감수하기로 한 리스크는 몇 개이고, 하나로 압축할 수 있는가?** → 여러 개를 동시에 감수했다면 서로 상쇄해 사실상 아무 것도 감수하지 않은 것과 같다 [SKILL-FRONTEND-DESIGN]
|
||||
|
||||
반사실 제네릭 점검("이 선택이 다른 업종의 브리프에도 그대로 나왔을 것인가")은 2단계(방향 결정)에서 먼저 하고 design.md에 기록한다. 여기서는 그 결정이 구현 중 안전한 쪽으로 후퇴하지 않았는지만 다시 본다.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@
|
|||
|
||||
1. **세리프 디스플레이를 쓰고 여백이 좁다** → 세리프는 숨 쉴 공간이 없으면 답답할 뿐이다
|
||||
2. **CTA 가 둘로 갈라진다** ("구매하기" + "구독하기") → 레퍼런스가 그렇더라도 베끼지 마라. 브리프가 정한 목표 하나로 통합해라
|
||||
3. **수치를 지표 배너로 만든다** → 근거 없는 숫자는 신뢰를 깎는다. 하드 게이트 #11에 걸린다
|
||||
3. **수치를 지표 배너로 만든다** → 근거 없는 숫자는 신뢰를 깎는다. '사용자가 주지 않은 수치를 그럴듯한 값으로 넣지 않는다' 하드 게이트에 걸린다
|
||||
4. **한글에 라틴 세리프만 지정** → 한글이 시스템 기본으로 떨어져 전부 무너진다
|
||||
5. **사진이 없다고 일러스트로 때운다** → 사양·수치·타이포로 채우는 쪽이 이 대상에게 더 강하다
|
||||
|
||||
|
|
|
|||
233
packages/skill/references/print-email.md
Normal file
233
packages/skill/references/print-email.md
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
# print-email — 인쇄와 이메일
|
||||
|
||||
브리프가 인쇄 가능한 문서(출력·PDF 저장·서명용 서류)나 이메일(뉴스레터·트랜잭션 메일·초대장)을 명시할 때만 연다. 둘 중 하나만 필요하면 해당 절만 읽는다.
|
||||
|
||||
이 문서는 `SKILL.md` 4-5(서류로서의 완성)의 인쇄 규칙을 일반화한다. 4-5는 프리셋이 서류·원장·콘솔 계열(장부, 시간표, 관리 화면)일 때만 도는 좁은 규칙이고, 여기서는 **그 밖의 어떤 화면이든 브리프가 인쇄를 요구하면** 적용할 수 있게 넓힌다. 4-5의 규칙(모달이 열려 있으면 그 서류만, `print-color-adjust: exact`)과 모순되지 않으며, 겹치는 부분은 새로 쓰지 않고 그대로 가져와 쓴다.
|
||||
|
||||
## 이 문서를 읽는 법
|
||||
|
||||
| 상황 | 읽을 곳 |
|
||||
|---|---|
|
||||
| 페이지를 인쇄·PDF 저장 가능하게 만든다 | §1 인쇄 |
|
||||
| 인쇄 결과가 맞는지 확인한다 | §2 인쇄 검증 |
|
||||
| 뉴스레터·트랜잭션 메일·초대장을 만든다 | §3 이메일 |
|
||||
| 이메일 클라이언트에서 실제로 확인한다 | §4 이메일 검증 |
|
||||
| 출시 전 한 번에 훑는다 | §5 체크리스트 |
|
||||
|
||||
---
|
||||
|
||||
## 0. 언제 여는가
|
||||
|
||||
- 브리프에 "인쇄", "PDF로 저장", "출력해서 제출", "서명 후 인쇄" 같은 말이 있으면 §1·§2.
|
||||
- 브리프에 "뉴스레터", "이메일로 보낸다", "가입 확인 메일", "영수증 메일", "초대 메일" 같은 말이 있으면 §3·§4.
|
||||
- 둘 다 아니면 이 문서는 읽지 않는다. 모든 웹 페이지를 기본적으로 인쇄·이메일 대응시키지 않는다 — 그것은 이 문서가 조건부로 존재하는 이유 자체를 없앤다.
|
||||
|
||||
---
|
||||
|
||||
## 1. 인쇄
|
||||
|
||||
언제: 브리프가 인쇄 가능한 문서를 요구할 때. 프리셋이 서류·원장·콘솔이면 `SKILL.md` 4-5를 먼저 따르고, 이 절은 그 규칙이 없는 일반 페이지(랜딩, 포트폴리오, 안내문 등)에 인쇄 대응을 추가할 때 쓴다.
|
||||
|
||||
### 1-1. `@media print` 뼈대
|
||||
|
||||
**프로젝트 계약**: 인쇄가 요구되면 `@media print` 블록으로 화면과 서류를 분리한다. 화면에서 필요한 앱 크롬(내비, 툴바, 플로팅 버튼, 사이드바)은 인쇄에서 의미가 없다 — 사용자는 종이 위에서 그것을 누를 수 없다.
|
||||
|
||||
```css
|
||||
@media print {
|
||||
header.app-nav,
|
||||
[role="toolbar"],
|
||||
.floating-action,
|
||||
.cookie-banner,
|
||||
button:not(.print-keep) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**프로젝트 계약**: 모달이 열린 채로 인쇄가 시작되면 배경의 앱 화면이 아니라 **그 모달 내용만** 서류가 되어야 한다. `SKILL.md` 4-5의 기존 규칙을 그대로 쓴다.
|
||||
|
||||
```css
|
||||
@media print {
|
||||
body:has(dialog[open]) > *:not(:has(dialog[open])) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**관찰 후보(시작값)**: `@page`의 여백은 브라우저 기본값에서 시작한다. 제본이나 클립이 필요한 서류만 여백을 늘린다. 정확히 몇 cm가 맞는지는 프로젝트마다 다르므로 숫자를 단정하지 않고, 실제 출력물로 확인한다(§2).
|
||||
|
||||
```css
|
||||
@page {
|
||||
margin: 2cm 1.5cm; /* 제본 쪽에 더 필요하면 margin-left 만 늘린다 — 실측 조정 */
|
||||
}
|
||||
```
|
||||
|
||||
### 1-2. 콘텐츠를 서류에 맞게 바꾼다
|
||||
|
||||
**프로젝트 계약**: 카드·표 행·영수증 구획처럼 한 덩어리로 읽혀야 하는 블록은 페이지 경계에서 잘리지 않게 한다.
|
||||
|
||||
```css
|
||||
@media print {
|
||||
.card,
|
||||
tr,
|
||||
.receipt-line,
|
||||
figure {
|
||||
break-inside: avoid;
|
||||
}
|
||||
h2, h3 {
|
||||
break-after: avoid; /* 제목만 페이지 끝에 남고 본문이 다음 장으로 넘어가는 것을 막는다 */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**프로젝트 계약**: 상태색처럼 의미를 나르는 색(경고·성공·실패 배지 등)만 `print-color-adjust: exact`로 보존한다. 나머지는 브라우저의 기본 인쇄 설정(그레이스케일·절약 모드 포함)에 맡긴다. 전체를 강제 컬러로 찍으면 사용자의 잉크 절약 설정을 무시하게 된다.
|
||||
|
||||
```css
|
||||
@media print {
|
||||
.status-badge,
|
||||
.status-dot {
|
||||
print-color-adjust: exact;
|
||||
-webkit-print-color-adjust: exact;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**하드 게이트**: 화면에서 잘라 보여주던 것(짧은 링크 텍스트, 접힌 섹션)은 종이 위에서 클릭이나 hover로 나머지에 닿을 수 없다. [typography.md](typography.md) §13의 "잘린 텍스트 — 전체 값에 도달하는 수단" 원칙과 같은 이유로, 인쇄에서는 그 값 자체를 펼쳐 보여줘야 정보 손실이 없다.
|
||||
|
||||
```css
|
||||
@media print {
|
||||
a[href^="http"]:not([href*="#"])::after {
|
||||
content: " (" attr(href) ")";
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
닫힌 `<details>`의 내용은 브라우저가 내부 슬롯으로 숨기므로, 자식 요소에 `display`를 주는 CSS로는 확실히 펼쳐지지 않는다. 인쇄 직전에 `open`을 켜고 끝나면 되돌린다.
|
||||
|
||||
```js
|
||||
let reopened = [];
|
||||
addEventListener('beforeprint', () => {
|
||||
reopened = [...document.querySelectorAll('details:not([open])')];
|
||||
reopened.forEach((d) => { d.open = true; });
|
||||
});
|
||||
addEventListener('afterprint', () => {
|
||||
reopened.forEach((d) => { d.open = false; });
|
||||
reopened = [];
|
||||
});
|
||||
```
|
||||
|
||||
### 1-3. 서류의 신원 — 머리말·바닥말·출력일
|
||||
|
||||
**관찰 후보**: 여러 장으로 인쇄될 문서(보고서, 명세서)는 페이지 번호와 문서 제목을 각 장에 남긴다. 독자가 순서를 잃어버리지 않게 하는 목적이므로, 한 장짜리 인쇄물(영수증, 초대장)에는 강제하지 않는다.
|
||||
|
||||
```css
|
||||
@media print {
|
||||
@page {
|
||||
@bottom-right { content: counter(page) " / " counter(pages); }
|
||||
}
|
||||
.print-footer::after {
|
||||
content: "출력일 " attr(data-print-date);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`@page` 안의 여백 상자(`@bottom-right` 등)는 브라우저 지원이 고르지 않으므로 점진적 향상으로 둔다 — 지원하지 않는 브라우저에서는 페이지 번호가 빠질 뿐이다. 페이지 번호가 계약상 반드시 필요하면 인쇄 미리보기에서 대상 브라우저로 확인하고, 안 되면 PDF 생성 도구 쪽에서 넣는다. `attr(data-print-date)`는 CSS만으로 오늘 날짜를 넣을 수 없으므로, 인쇄 전에 JS로 `document.querySelector('.print-footer').dataset.printDate = new Date().toLocaleDateString('ko-KR')` 같은 식으로 채워 둔다.
|
||||
|
||||
---
|
||||
|
||||
## 2. 인쇄 검증
|
||||
|
||||
인쇄는 화면 렌더링과 다른 엔진 경로를 타기 때문에, 화면에서 괜찮아 보인다고 인쇄도 괜찮다고 결론 내리지 않는다.
|
||||
|
||||
1. **인쇄 미리보기를 캡처한다.** 브라우저의 인쇄 미리보기(Ctrl/Cmd+P)를 열어 각 페이지를 실제로 넘겨 본다. 브라우저 자동화 도구가 있으면 인쇄 미디어를 에뮬레이트해 스크린샷으로 남긴다 — 쓸 수 있는 도구는 [harness.md](harness.md)를 따른다.
|
||||
2. **PDF로 저장해 연다.** "PDF로 저장"으로 실제 파일을 만들고 별도 뷰어로 열어, 브라우저 미리보기와 최종 PDF가 다르게 나오는 경우(특히 폰트·배경색)가 없는지 확인한다.
|
||||
3. **경계에서 확인할 것**: 카드·표·영수증 구획이 페이지 경계에서 잘리지 않는지, 앱 크롬이 실제로 사라졌는지, 모달만 인쇄될 상황에서 배경 화면이 섞여 나오지 않는지, 링크 URL과 펼친 `details`가 실제로 종이에 보이는지, 상태색이 흑백 인쇄에서도 배지 모양(테두리·아이콘)만으로 구분되는지(색만으로 구분되면 흑백에서 정보가 사라진다).
|
||||
4. **확인하지 못했으면 "미검증"이라고 적는다.** 소스만 보고 통과로 추정하지 않는다 — `audit-gate.md`의 미검증 관례를 그대로 따른다.
|
||||
|
||||
---
|
||||
|
||||
## 3. 이메일
|
||||
|
||||
언제: 브리프가 뉴스레터, 트랜잭션 메일(가입 확인·영수증·비밀번호 재설정), 초대장처럼 **이메일 클라이언트 안에서 열리는 HTML**을 명시할 때. 웹사이트에 "문의하기 → 메일 발송" 버튼이 있는 정도는 해당하지 않는다.
|
||||
|
||||
이메일 클라이언트는 웹 브라우저가 아니다. **관찰 후보 — 이 문서에서 실측하지 않았다.** 일부 클라이언트는 데스크톱 문서 편집기의 렌더링 경로를 그대로 쓰거나, 외부 스타일시트·최신 레이아웃 CSS를 지원하지 않거나 부분적으로만 지원한다는 관찰이 있다. 정확히 어떤 클라이언트가 무엇을 지원하는지는 이 문서에서 확인하지 않았으므로 클라이언트별 지원 수치를 단정하지 않는다. 아래는 그런 제약을 가정했을 때 안전한 기본값이며, 실제 확인은 §4를 따른다.
|
||||
|
||||
### 3-1. 레이아웃
|
||||
|
||||
**프로젝트 계약**: 폭은 600px 안팎의 단일 컬럼을 기본값으로 한다. 화면이 넓어도 이메일 본문 폭을 늘리지 않는다 — 클라이언트의 미리보기 창, 모바일 메일 앱 폭이 그보다 좁은 경우가 흔하다[SKILL-IMPECCABLE].
|
||||
|
||||
**프로젝트 계약**: 레이아웃은 표(`<table>`) 기반으로 짜고, 스타일은 `<style>` 블록이 아니라 각 태그의 `style` 속성에 인라인으로 넣는다. `display: flex`나 `grid` 같은 최신 레이아웃 CSS, 외부 스타일시트 링크는 이메일에서 기대한 대로 렌더링되지 않을 수 있다는 것이 근거다[SKILL-IMPECCABLE].
|
||||
|
||||
```html
|
||||
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="margin:0 auto; max-width:600px;">
|
||||
<tr>
|
||||
<td style="padding:24px; font-family:Arial, sans-serif; font-size:16px; line-height:1.5; color:#1a1a1a;">
|
||||
본문 내용
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
```
|
||||
|
||||
### 3-2. 인터랙션
|
||||
|
||||
**프로젝트 계약**: 행동 유도는 텍스트 링크가 아니라 버튼처럼 보이는 요소로 만든다. 배경색을 채운 `<a>`를 표 셀 안에 넣는 방식이 흔한 구현이다[SKILL-IMPECCABLE](`<button>` 태그 자체는 이메일 클라이언트에서 일관되게 렌더링되지 않는 경우가 있어 피한다 — **관찰 후보, 이 문서에서 실측하지 않았다**).
|
||||
|
||||
```html
|
||||
<a href="https://example.com/confirm"
|
||||
style="display:inline-block; padding:12px 24px; background:#1a56db; color:#ffffff;
|
||||
text-decoration:none; border-radius:6px; font-weight:600;">
|
||||
가입 확인하기
|
||||
</a>
|
||||
```
|
||||
|
||||
**하드 게이트**: 대부분의 이메일 클라이언트는 hover 상태 자체가 없다(모바일 메일 앱, 미리보기 창). 핵심 동작(다음 단계로 가는 CTA, 정보 확인)이 hover에만 반응하도록 만들면 애초에 작동하지 않는다. 추가 정보나 보조 동작이 hover에 의존해서도 안 된다 — 항상 보이는 상태로 둔다[SKILL-IMPECCABLE].
|
||||
|
||||
**프로젝트 계약**: 폼 제출, 다단계 흐름, 필터처럼 복잡한 상호작용은 이메일 안에서 구현하지 않는다. 버튼으로 웹의 해당 화면에 딥링크한다[SKILL-IMPECCABLE].
|
||||
|
||||
### 3-3. 이미지가 막혀도, 어두운 테마에서도 읽힌다
|
||||
|
||||
**프로젝트 계약**: 많은 이메일 클라이언트가 기본적으로 이미지를 차단한 채 미리보기를 띄운다. 로고·CTA·핵심 문구를 이미지에만 담으면 이미지가 막힌 순간 그 내용이 통째로 사라진다. 핵심 텍스트(제목, 본문, 버튼 라벨)는 실제 HTML 텍스트로 쓰고, 이미지에는 내용을 설명하는 `alt`를 붙이며, 이미지가 있어야 할 자리에는 배경색을 지정해 이미지가 빠져도 빈 흰 칸이 아니라 브랜드 색 영역으로 보이게 한다.
|
||||
|
||||
```html
|
||||
<img src="logo.png" alt="회사 로고" width="120"
|
||||
style="display:block; background-color:#f3f4f6;">
|
||||
```
|
||||
|
||||
**관찰 후보**: 일부 클라이언트는 사용자가 어두운 테마를 켰을 때 메일 배경·글자색을 자동으로 반전하거나 재조정한다. 이 동작은 클라이언트마다 다르고 이 문서에서 실측하지 않았으므로 어떤 클라이언트가 어떻게 반전하는지는 단정하지 않는다. 실무적으로 안전한 방향은 배경색을 투명에 맡기지 않고 컨테이너마다 명시적으로 지정하는 것, 로고처럼 투명 배경을 가정한 자산을 흰 배경 전용으로 만들지 않는 것이다. 실제 확인은 §4를 따른다.
|
||||
|
||||
---
|
||||
|
||||
## 4. 이메일 검증
|
||||
|
||||
브라우저 렌더링과 실제 이메일 클라이언트 렌더링은 별개다. 로컬 브라우저에서 HTML 파일을 열어 봤다고 이메일에서도 같다고 보고하지 않는다.
|
||||
|
||||
1. **실제 클라이언트로 확인한 경우만 "검증됨"으로 보고한다.** 예를 들어 실제 계정으로 지메일·아웃룩·애플 메일 등에 발송해 열어 보고, 폭·표 레이아웃·버튼·이미지 차단 상태·어두운 테마에서 각각 확인한다.
|
||||
2. **확인하지 못했으면 "미검증"이라고 명시한다.** 코드가 규칙(§3)을 따랐다는 것과 실제 클라이언트에서 그대로 보인다는 것은 다른 주장이다. "표 레이아웃과 인라인 CSS를 썼으니 호환될 것이다"는 추정이지 검증이 아니다.
|
||||
3. **클라이언트별 지원 수치·점유율을 지어내지 않는다.** 이 스킬은 "아웃룩이 몇 퍼센트를 지원한다" 같은 구체 수치를 확인하지 않았다. 필요하면 사용자에게 발송 대상 클라이언트 범위를 물어 §3의 어떤 항목을 더 엄격하게 지킬지 좁힌다.
|
||||
|
||||
---
|
||||
|
||||
## 5. 체크리스트
|
||||
|
||||
**인쇄**
|
||||
|
||||
- [ ] `@media print`로 앱 크롬(내비·툴바·플로팅 버튼)을 숨겼다
|
||||
- [ ] 모달이 열린 채 인쇄되면 그 모달만 서류가 된다
|
||||
- [ ] 카드·표 행이 페이지 경계에서 잘리지 않는다(`break-inside: avoid`)
|
||||
- [ ] 상태색은 `print-color-adjust: exact`로 보존하되, 색만으로 상태를 구분하지 않는다(흑백에서도 구분 가능)
|
||||
- [ ] 짧은 링크 텍스트가 전체 URL로 펼쳐진다
|
||||
- [ ] 접힌 섹션(`details` 등)이 인쇄에서는 펼쳐진다
|
||||
- [ ] 여러 장 문서면 페이지 번호·문서 제목이 있다
|
||||
- [ ] 인쇄 미리보기와 PDF 출력을 실제로 열어 확인했다(§2). 못 했으면 미검증이라고 적었다
|
||||
|
||||
**이메일**
|
||||
|
||||
- [ ] 폭 600px 안팎, 단일 컬럼
|
||||
- [ ] 표 기반 레이아웃, 인라인 CSS
|
||||
- [ ] CTA가 버튼처럼 보이는 요소다(텍스트 링크가 아니다)
|
||||
- [ ] 핵심 동작·정보가 hover에 의존하지 않는다
|
||||
- [ ] 복잡한 상호작용은 웹으로 딥링크했다
|
||||
- [ ] 이미지가 막혀도 핵심 텍스트가 읽히고, `alt`와 배경색을 지정했다
|
||||
- [ ] 컨테이너 배경색을 명시해 다크모드 반전에 대비했다
|
||||
- [ ] 실제 클라이언트에서 확인했다(§4). 못 했으면 미검증이라고 적었다
|
||||
241
packages/skill/references/product-copy.md
Normal file
241
packages/skill/references/product-copy.md
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
# product-copy — 제품 UI 카피
|
||||
|
||||
4단계에서 버튼·폼 라벨·상태 메시지·오류·빈 상태·토글처럼 제품이 스스로 말하는 문구를 쓸 때 읽는다. 5단계 프리플라이트의 카피 검토에서 다시 연다.
|
||||
|
||||
브랜드 차별성을 주장하는 마케팅 헤드라인·브랜드 약속의 슬롭 지문(`so you can…` 테스트, 구문·어휘 지문)은 이 문서가 아니라 [antipatterns.md](antipatterns.md) 6절이 정본이다. 두 문서는 같은 대상을 다른 각도에서 본다 — 헤드라인이 버튼·토글 같은 제품 UI 라벨을 겸하면 두 문서를 모두 확인한다.
|
||||
|
||||
라벨은 셋 중 하나다. **하드 게이트**는 위반하면 통과시키지 않는다(WCAG 등 규범·기능 요건). **프로젝트 계약**은 이 프로젝트가 그 조건을 선택했을 때 지키는 약속이다(용어 체계, 어투 통일 같은 자체 결정). **관찰 후보**는 시작값이고 실제 렌더·독자 반응에서 확인해 조정한다. 절 제목에 주된 위계를 붙이고, 절 안에서 다른 위계가 섞이면 문장 앞에 굵게 라벨을 따로 단다.
|
||||
|
||||
## 이 문서를 읽는 법
|
||||
|
||||
| 상황 | 읽을 곳 |
|
||||
|---|---|
|
||||
| 새 카피를 쓰기 전에 뭘 먼저 봐야 하나 | §1 |
|
||||
| 같은 개념을 화면마다 다르게 부르고 있는 것 같다 | §2 |
|
||||
| 오류·삭제 확인·결제 화면의 톤이 다 같아 보인다 | §3 |
|
||||
| 한 화면에 해요체와 합니다체가 섞인다 | §4 |
|
||||
| 버튼 라벨을 어떻게 짓나 | §5 |
|
||||
| 오류 메시지를 쓴다 | §6 |
|
||||
| 빈 상태·빈 목록 문구를 쓴다 | §7 |
|
||||
| 내부 개발 용어가 라벨에 새어 나온 것 같다 | §8 |
|
||||
| `{name}을(를) 삭제했습니다` 같은 변수 삽입 문장 | §9 |
|
||||
| 토글·링크 텍스트·placeholder | §10 |
|
||||
| 물음표·느낌표·줄임표를 언제 쓰나 | §11 |
|
||||
| 영문 라벨의 대소문자 규칙 | §12 |
|
||||
| 실제 서비스 사례가 궁금하다 | §13 |
|
||||
| 카피 findings를 어떻게 검증·보고하나 | §14 |
|
||||
| 감사 직전 | §15 |
|
||||
|
||||
---
|
||||
|
||||
## 0. 언제·범위
|
||||
|
||||
이 문서는 4단계에서 폼·상태·오류·빈 상태·토글처럼 **제품이 스스로 말하는 문구**를 쓸 때 연다. 5단계 프리플라이트에서 카피만 따로 다시 훑을 때도 연다. 링크 텍스트·placeholder의 접근성 배선(스크린리더 순서, `aria-describedby`, 포커스 이동)은 이 문서가 아니라 [accessibility.md](accessibility.md)가 정본이며, 이 문서는 그 위에 얹히는 **문구 내용**만 다룬다.
|
||||
|
||||
---
|
||||
|
||||
## 1. 기존 보이스·용어 확인 — 프로젝트 계약
|
||||
|
||||
새 카피를 쓰기 전에 프로젝트에 이미 있는 카피를 읽는다. `design.md`, 기존 화면의 라벨·토스트·오류 문구, 저장소에 남은 스타일 가이드를 먼저 훑어 제품이 이미 쓰고 있는 용어·어투·로케일 관행을 파악한다[SKILL-BETTER-WRITING]. SKILL.md의 우선순위 원칙(사용자 지시 → design.md → 기존 토큰·코드 → designpaca 기본값)이 토큰·코드에 대해 요구하는 것과 같은 태도를 카피에도 적용한다 — 새로 지어내기 전에 이미 있는 답을 먼저 찾는다.
|
||||
|
||||
---
|
||||
|
||||
## 2. 용어 일관성 — 프로젝트 계약
|
||||
|
||||
한 개념에는 한 용어만 쓴다. 메뉴에서 `보관함`이라고 부른 것을 토스트에서 `저장소로 이동`이라고 부르면, 사용자는 같은 일을 두 번 배워야 한다[SKILL-BETTER-WRITING].
|
||||
|
||||
**grep 방법**
|
||||
|
||||
1. 화면에 쓰인 명사·동사를 후보로 뽑는다(예: `삭제`/`제거`/`지우기`, `보관`/`저장소`/`아카이브`).
|
||||
2. 저장소 전체에서 각 후보를 `grep`해 실제로 몇 군데서 어떤 말로 쓰이는지 센다.
|
||||
3. 가장 많이 쓰인 쪽을 기준으로 정하거나, 사용자에게 더 명확한 쪽을 고른다. 어느 쪽이든 design.md에 기록한다.
|
||||
4. 소수파 표현을 전부 grep으로 찾아 바꾼다. 코드 식별자(`archiveItem` 같은 함수명)까지 바꾸라는 뜻은 아니다 — 사용자가 보는 문자열만 대상이다.
|
||||
|
||||
---
|
||||
|
||||
## 3. 톤과 위험 매트릭스 — 관찰 후보
|
||||
|
||||
모든 문구를 같은 톤으로 쓰면, 정말 조심해야 하는 순간(삭제·결제)도 일상 안내와 구분되지 않는다. 상황의 위험 수위에 따라 톤 강도를 다르게 가져간다[SKILL-BETTER-WRITING].
|
||||
|
||||
| 위험 수위 | 상황 예 | 톤 | 이유 |
|
||||
|---|---|---|---|
|
||||
| 일상 | 목록 화면, 설정, 일반 안내 문구 | 중립·간결 | 매번 감정을 실으면 정말 중요한 순간의 톤이 묻힌다 |
|
||||
| 주의 | 로그아웃, 구독 변경, 여러 항목 일괄 처리 | 신중·결과를 분명히 말함 | 다음에 무슨 일이 벌어지는지 오해하면 안 된다 |
|
||||
| 파괴 | 삭제 확인, 탈퇴, 결제 취소 | 차분·군더더기 없음(장난기·이모지 배제) | 가벼운 톤은 행동의 무게와 어긋난다 |
|
||||
| 오류 | 폼 검증 실패, 저장 실패, 네트워크 오류 | 담백·비난하지 않음 | §6 참고 — 과한 사과·느낌표는 문제를 가리지 않는다 |
|
||||
| 금전·보안 | 결제, 개인정보 변경, 본인 인증 | 진지·조건을 흐리지 않고 명시 | 오해가 실제 손실로 이어질 수 있다 |
|
||||
|
||||
이 위험 수위는 0단계 인터뷰에서 정한 브리프의 톤 프리셋([brief-interview.md](brief-interview.md))과 다른 층이다 — 브리프의 톤이 전체 브랜드 인상을 정하고, 이 표는 그 톤 안에서 상황별 강도를 조절한다. 장난스러운 브랜드 톤이라도 결제·보안 화면까지 그 장난기를 그대로 옮기지 않는다.
|
||||
|
||||
---
|
||||
|
||||
## 4. 어투 일관성 — 프로젝트 계약
|
||||
|
||||
한 제품 안에서 해요체(`~해요`, `~돼요`)와 합니다체(`~합니다`, `~됩니다`)를 섞으면 어투가 흔들려 보인다. 어느 쪽을 쓸지는 브랜드 톤에 따라 프로젝트가 정하고, 정했으면 화면 전체에서 지킨다.
|
||||
|
||||
이 규칙의 근거는 1차 출처로 확인되지 않았다 — 실무에서 두 어체를 상황별로 섞어 쓰는 사례가 보고되긴 하나, 그 출처가 1차 공식 자료가 아니라서 이 문서는 규범이 아니라 **프로젝트 계약**으로만 둔다. 브리프나 기존 화면이 이미 특정 어체를 쓰고 있으면 그것을 따르고(§1), 새로 정해야 하면 0단계 인터뷰의 톤 질문과 함께 확정한다. 확정한 뒤에는 §2와 같은 방법으로 grep해 혼용된 곳을 찾는다.
|
||||
|
||||
---
|
||||
|
||||
## 5. 버튼 레이블 — 프로젝트 계약
|
||||
|
||||
행정안전부 KRDS(공공 디자인 시스템) 지침은 버튼 텍스트를 원칙적으로 동사형으로 쓰도록 규정한다. `완료`·`닫기`·`취소`·`추가`·`삭제`처럼 이미 관용적으로 쓰이는 명사형은 예외다[KO-KRDS]. 명사·형용사 라벨(`저장 버튼입니다` 같은 서술이 아니라 그냥 `저장 완료` 같은 애매한 상태명)은 버튼을 눌렀을 때 무슨 일이 일어나는지 예측하기 어렵게 만든다.
|
||||
|
||||
- **결과를 말한다.** 버튼을 누르면 정확히 무슨 일이 일어나는지 동사구로 쓴다 — `제출` 대신 `변경사항 저장`, `확인` 대신 `계정 삭제`[SKILL-FRONTEND-DESIGN].
|
||||
- **확인 대화상자 버튼은 결과를 반복한다.** `이 프로젝트를 삭제할까요?`라는 대화상자의 버튼은 `확인`/`취소`가 아니라 `삭제`/`취소`로 쓴다. `예`/`아니오`로 두면 사용자가 방금 읽은 질문을 다시 떠올려야 동의 대상을 알 수 있다[SKILL-BETTER-WRITING].
|
||||
- **같은 행동은 버튼 → 확인 → 토스트까지 같은 이름을 쓴다.** `게시` 버튼을 누르면 `게시됨` 토스트를 띄운다. 버튼은 `게시`인데 토스트가 `발행을 완료했습니다`로 바뀌면 같은 행동인지 다시 확인해야 한다[SKILL-FRONTEND-DESIGN].
|
||||
- **다단계 플로우의 어휘를 한 체계로 고정한다.** 진입은 `시작하기`, 진행은 `계속` 또는 `다음` 중 하나만(둘을 같은 플로우에서 섞지 않는다), 종료는 `완료`처럼 각 단계 역할에 한 단어만 배정하고 프로젝트 전체에서 유지한다[SKILL-BETTER-WRITING].
|
||||
|
||||
---
|
||||
|
||||
## 6. 오류 문구 — 프로젝트 계약(내용) / 관찰 후보(어휘)
|
||||
|
||||
오류 상태가 **존재**해야 한다는 요구는 [preflight.md](preflight.md) §4-1의 상태 완결성 검사가 이미 다룬다. 여기서는 그 오류 문구가 실제로 무엇을 말해야 하는지를 다룬다. 화면에서 문구가 나타나는 위치·타이밍(실시간 검증인지 제출 시인지)·`aria-live`·포커스 이동 같은 배선은 [accessibility.md](accessibility.md) §6(타이밍·포커스 이동)·§7(상태 알림)이 정본이다.
|
||||
|
||||
**프로젝트 계약 — 오류 문구는 항상 두 가지를 함께 말한다.** 무엇이 잘못됐는지, 그리고 어떻게 고치는지다. 하나만 있으면 사용자는 다음 행동을 스스로 추측해야 한다[SKILL-BETTER-WRITING].
|
||||
|
||||
**하드 게이트 — 입력 오류가 감지되고 고칠 방법을 알고 있으면 그 방법을 제시해야 한다**(WCAG 3.3.3 Error Suggestion, AA)[WCAG-22]. 그 밖의 오류 문구 기준(어휘·톤·비난 주어 회피 등)은 기존대로 프로젝트 계약이다.
|
||||
|
||||
**관찰 후보 — 어휘 선택.** 비난하는 주어를 쓰지 않는다(`당신이 잘못 입력했습니다` 대신 `형식이 올바르지 않습니다`처럼 상황을 주어로 둔다). 과한 사과와 느낌표를 쓰지 않는다 — §3의 `오류` 톤과 같은 이유다.
|
||||
|
||||
| 상황 | 나쁜 예 | 좋은 예 | 왜 |
|
||||
|---|---|---|---|
|
||||
| 필수 입력 누락 | 오류가 발생했습니다 | 이메일을 입력해 주세요 | 무엇이 비었는지 말하지 않음 → 말함 |
|
||||
| 형식 오류 | 잘못된 값입니다 | 전화번호는 숫자만 10~11자리로 입력해 주세요 | 고치는 법이 없음 → 있음 |
|
||||
| 저장 실패 | 죄송합니다ㅠㅠ 문제가 생겼어요! | 저장하지 못했습니다. 다시 시도해 주세요 | 과한 사과·느낌표 → 담백하게, 다음 행동 제시 |
|
||||
|
||||
같은 오류가 반복해서 발생한다면 문구를 더 다듬기보다 왜 반복되는지(입력 제약, 폼 흐름)를 4단계 인터랙션 설계에서 다시 본다. 이 문서는 문구 내용만 다루며, 인터랙션 재설계는 이 문서의 범위 밖이다.
|
||||
|
||||
---
|
||||
|
||||
## 7. 빈 상태 — 프로젝트 계약(내용) / 관찰 후보(어휘)
|
||||
|
||||
**프로젝트 계약 — 빈 상태는 세 가지를 한 번에 말한다.** 무엇이 비었는지(정체성), 왜 비었는지, 다음에 무엇을 할 수 있는지다. `항목이 없습니다` 한 줄만 있으면 사용자는 이 화면이 고장인지 정상인지부터 판단해야 한다[SKILL-BETTER-WRITING].
|
||||
|
||||
**관찰 후보 — 검색·필터 빈 상태는 사용자가 입력한 조건을 그대로 보여준다.** `검색 결과가 없습니다`보다 `'디자인'에 대한 검색 결과가 없습니다`가 사용자의 입력을 그대로 확인해 주고, 이어서 탈출구(필터 초기화, 다른 검색어 제안)를 함께 둔다.
|
||||
|
||||
| 상황 | 나쁜 예 | 좋은 예 |
|
||||
|---|---|---|
|
||||
| 첫 사용, 데이터 없음 | 항목이 없습니다 | 아직 등록한 프로젝트가 없습니다. 새 프로젝트를 만들어 보세요 |
|
||||
| 검색 결과 없음 | 검색 결과가 없습니다 | `'디자인'`에 대한 검색 결과가 없습니다. 다른 검색어를 시도하거나 필터를 초기화해 보세요 |
|
||||
| 필터로 전부 걸러짐 | 결과 없음 | 적용한 필터와 일치하는 항목이 없습니다. 필터 초기화 |
|
||||
|
||||
도움말 링크 같은 지속적인 정보를 빈 상태에만 있게 두지 않는다 — 데이터가 채워진 뒤에도 같은 도움말을 찾을 수 있어야 한다.
|
||||
|
||||
---
|
||||
|
||||
## 8. 평이한 말·기기 동사·독자 지칭·내부 용어 — 관찰 후보
|
||||
|
||||
- **최종 사용자 어휘로 쓴다.** 시스템 구현 방식이 아니라 사용자가 실제로 하는 일로 이름 붙인다 — `webhook`이 아니라 `알림`, `payload`가 아니라 `보낸 내용`. 이 검사는 [antipatterns.md](antipatterns.md) §6(진단)·§9(grep)의 내부 구현 용어 grep 후보(`webhook`·`config`·`payload`·`schema`)와 같은 검사이며, 여기서는 화면에 노출되는 **모든** 라벨·설정 이름에 같은 기준을 적용한다[SKILL-FRONTEND-DESIGN].
|
||||
- **입력 방식에 맞는 동사를 쓴다.** 터치 인터페이스 안내에 `클릭`을, 마우스 중심 안내에 `탭`을 쓰지 않는다. 입력 방식을 특정할 수 없는 라벨(아이콘 버튼 등)은 동사 없는 명사형도 대안이다[SKILL-BETTER-WRITING].
|
||||
- **독자 지칭과 소유격을 절제한다.** 안내문은 한국어의 주어 생략을 살려 `-하세요`체로 직접 쓰는 편이 자연스럽다. 오류 문구에서 `저희가`·`우리가` 같은 서비스 쪽 주어를 남용하지 않는다(§6의 `불러오지 못했습니다`처럼 상황 주어가 더 담백하다). 소유격도 필요할 때만 쓴다 — `내 즐겨찾기`가 다른 사용자의 즐겨찾기와 헷갈릴 우려가 없으면 `즐겨찾기`로 충분하다[SKILL-BETTER-WRITING].
|
||||
- **특정 집단을 전제하는 표현을 쓰지 않는다.** 성별·연령 등을 전제한 예시 문구나 관용구는 다른 독자를 배제할 수 있다.
|
||||
|
||||
---
|
||||
|
||||
## 9. 문장 조각 조립 금지와 한국어 조사 처리 — 관찰 후보
|
||||
|
||||
**관찰 후보(엔지니어링 관행).** 변수 주변에 고정 문구를 이어 붙여 문장을 조립하지 않는다. `${count} 개의 항목이 삭제되었습니다`처럼 숫자·이름 앞뒤에 고정된 조각을 붙이는 방식은 그 변수가 조사·복수형·문장 구조에 영향을 주는 언어에서 깨지기 쉽다[SKILL-BETTER-WRITING]. 완성된 문장을 통째로 템플릿화하거나, 조사가 필요 없는 문장 구조로 바꾼다.
|
||||
|
||||
한국어에서는 조사(을/를, 이/가, 은/는, 으로/로)가 변수의 마지막 글자 받침 유무에 따라 갈린다는 문제가 하나 더 있다. 완성형 한글 음절(유니코드 `U+AC00`~`U+D7A3`, 11,172자)은 마지막 글자의 코드값에서 `0xAC00`을 뺀 값을 28로 나눈 나머지가 0이면 받침이 없다.
|
||||
|
||||
```js
|
||||
// 받침 유무로 조사 짝을 고른다
|
||||
function hasBatchim(syllable) {
|
||||
const code = syllable.charCodeAt(syllable.length - 1) - 0xAC00;
|
||||
if (code < 0 || code > 11171) return null; // 완성형 한글 음절이 아님(로마자·숫자 등)
|
||||
return code % 28 !== 0;
|
||||
}
|
||||
|
||||
function pickJosa(word, withBatchim, withoutBatchim) {
|
||||
const has = hasBatchim(word);
|
||||
if (has === null) return null; // 판별 불가 — 아래 재구성 대안을 쓴다
|
||||
return has ? withBatchim : withoutBatchim;
|
||||
}
|
||||
|
||||
// "${name}을(를) 삭제했습니다" 대신
|
||||
const josa = pickJosa(name, '을', '를');
|
||||
`${name}${josa} 삭제했습니다`
|
||||
```
|
||||
|
||||
**로마자·숫자로 끝나는 변수는 코드로 판별할 수 없다.** `iPhone`, `3번` 처럼 완성형 한글 음절이 아닌 문자로 끝나면 받침 여부가 발음마다 갈려 규칙 하나로 못 맞힌다. 이런 값이 들어갈 자리는 조사가 아예 필요 없도록 문장을 다시 짠다 — 예: `${name}${josa} 삭제했습니다` 대신 `${name} 항목을 삭제했습니다`처럼 조사 앞에 고정 명사(`항목`)를 끼워 받침 문제를 피한다.
|
||||
|
||||
---
|
||||
|
||||
## 10. 토글 라벨·링크 텍스트·placeholder
|
||||
|
||||
### 10-1. 토글 라벨 — 관찰 후보
|
||||
|
||||
토글은 **켜졌을 때 일어나는 일**을 기준으로 라벨링한다. `알림 끄기`라는 라벨의 토글을 껐을 때 알림을 받는 상태가 되면(이중 부정), 사용자는 지금 상태가 무엇인지 헷갈린다. `알림 받기`로 쓰고 켜진 상태 = 알림을 받는 상태로 맞춘다[SKILL-BETTER-WRITING].
|
||||
|
||||
### 10-2. 링크 텍스트
|
||||
|
||||
**하드 게이트** — 링크의 목적은 링크 텍스트만으로, 또는 텍스트와 프로그램적으로 결정 가능한 맥락을 합쳐서 알 수 있어야 한다[WCAG-LINK].
|
||||
|
||||
**관찰 후보** — 스크린리더의 링크 목록 탐색 모드는 맥락 없이 링크 텍스트만 순서대로 읽으므로, 그런 상황에서도 통하도록 목적어를 넣는다 — `더보기` 단독 대신 `가격표 보기`[SKILL-BETTER-WRITING]. 한 화면에 `더보기` 링크가 여러 개면 각각 접미사로 구분한다(`사례 더보기`, `요금제 더보기`).
|
||||
|
||||
링크 존재 여부 자체는 이미 [preflight.md](preflight.md) §4-1이 확인한다 — 이 절은 그 링크의 문구 작성법만 더한다.
|
||||
|
||||
### 10-3. placeholder는 라벨이 아니다 — 하드 게이트
|
||||
|
||||
모든 입력에는 보이는 `<label>`이 있어야 하고, placeholder는 그 label을 대신할 수 없다. 이 요건의 정본은 [accessibility.md](accessibility.md)와 [preflight.md](preflight.md) §1이다. 이 문서가 더하는 내용은 카피뿐이다 — placeholder에는 예상 형식의 예시만 넣는다(`010-1234-5678`처럼 전화번호 형식 예시). 실제로 입력해야 할 값이나 지시문을 placeholder에만 넣지 않는다.
|
||||
|
||||
### 10-4. 아이콘 버튼의 라벨과 접근 가능한 이름 일치 — 하드 게이트
|
||||
|
||||
아이콘+텍스트 버튼에서 화면에 보이는 문구와 별도로 `aria-label`을 새로 지어 붙이면, 보이는 텍스트가 접근 가능한 이름 안에 포함되어야 한다 — 음성 입력 사용자가 화면에서 읽은 말을 그대로 불러 조작할 수 있어야 한다[WCAG-LABEL-NAME]. 아이콘 전용 버튼의 이름을 어떻게 배선하는지(`aria-label` vs 시각적으로 숨긴 텍스트)는 [accessibility.md](accessibility.md)와 [icons.md](icons.md) §8이 정본이다.
|
||||
|
||||
---
|
||||
|
||||
## 11. 문장 부호 — 관찰 후보
|
||||
|
||||
한국어 문장 부호는 2014년 12월 개정, 2015년 1월 시행된 한글 맞춤법 규정을 따른다. 마침표·물음표·느낌표·쉼표·가운뎃점·쌍점·빗금·따옴표(2종)·괄호(3종)·낫표(2종)·화살괄호(2종)·줄표·붙임표·물결표·드러냄표·숨김표·빠짐표·줄임표 등 약 20개 범주로 규정돼 있다[KO-PUNCT]. 개별 부호의 세부 적용 범위(가운뎃점의 정확한 용법, 따옴표 종류별 우선순위 등)는 이번 조사에서 1차 출처로 확인하지 못했다 — 세부 판정이 필요하면 국립국어원 어문 규정 원문을 프로젝트에서 직접 재확인한다.
|
||||
|
||||
제목·라벨에서 가운뎃점·em dash를 남용하는 구체적 지문은 이 문서가 아니라 [antipatterns.md](antipatterns.md) §4·§6이 맡는다(중복 규정하지 않는다). 이 문서가 더하는 것은 상태 문구 관행뿐이다.
|
||||
|
||||
- 진행 중 상태는 줄임표로 표시할 수 있다(`저장 중…`, `불러오는 중…`). 스피너·프로그레스 표시와 함께 써도 된다.
|
||||
- 물음표·느낌표는 §3의 톤과 위험 수위를 넘지 않는 범위에서만 쓴다. 오류·파괴적 확인에는 느낌표를 쓰지 않는다(§6과 같은 이유).
|
||||
|
||||
---
|
||||
|
||||
## 12. 라틴 문자 UI의 대소문자 정책 — 관찰 후보
|
||||
|
||||
언제: 프로젝트 UI에 영문 라벨이 있을 때 연다. 한국어는 대소문자 구분이 없어 이 절이 적용되지 않는다.
|
||||
|
||||
버튼·메뉴·제목 같은 요소 유형별로 타이틀 케이스(`Save Changes`)와 문장체(`Save changes`) 중 하나를 정해 그 유형 전체에서 통일한다. 같은 유형 안에서 섞이면(`Save Changes` 버튼 옆에 `Discard changes` 버튼) 비일관으로 읽힌다. 확신이 안 서면 문장체가 더 안전한 시작값이다[SKILL-BETTER-WRITING]. 군더더기 없는 평이한 동사와 문장체 톤을 기본으로 한다는 원칙도 같은 방향이다[SKILL-FRONTEND-DESIGN].
|
||||
|
||||
---
|
||||
|
||||
## 13. 사례 — 토스 글쓰기 원칙
|
||||
|
||||
토스 기술 블로그가 공개한 글쓰기 원칙은 핵심 가치 5가지(Clear·Concise·Casual·Respect·Emotional)와 실행 원칙 8가지로 구성된다[KO-TOSS-WRITING]. 예를 들어 Clear는 단어의 의미가 모호하지 않고 한 번에 이해되는 문장인지 점검하는 것이고, Casual은 금융·IT 업계에서 쓰던 어려운 용어와 딱딱한 뉘앙스를 쉽고 친절하게 고쳐 쓰는 것이다.
|
||||
|
||||
이 사례는 한 회사가 실제로 공개한 원칙의 예시이지, 모든 프로젝트가 그대로 따라야 할 규범이 아니다. 공공기관·의료·B2B처럼 브리프의 톤이 다르면 그대로 옮기지 않는다. §1의 원칙대로, 새 프로젝트의 어투는 그 프로젝트의 브리프와 기존 카피에서 먼저 확인한다.
|
||||
|
||||
---
|
||||
|
||||
## 14. 검증
|
||||
|
||||
카피 findings는 1차로 소스(코드·design.md) 대조만으로 판정할 수 있다 — 용어 일관성(§2), 버튼 동사(§5), 오류·빈 상태 문구 내용(§6·§7)은 렌더를 새로 켜지 않아도 확인 가능하다[SKILL-BETTER-WRITING]. 다만 잘림·줄바꿈의 영향을 받는 카피(버튼 라벨, 제목, 좁은 카드 안 문구)는 소스만으로 판정하지 않는다 — 실제 렌더에서 줄바꿈·말줄임(`text-overflow: ellipsis`)·좁은 폭 재배치를 확인한다.
|
||||
|
||||
카피 결함에도 별도 심각도 체계를 두지 않는다. 이 문서 각 절에 붙은 하드 게이트/프로젝트 계약/관찰 후보 라벨을 그대로 finding의 위계로 쓴다 — 예: 링크 텍스트 위반(§10-2)은 하드 게이트, 오류 문구에 수정법이 없는 것(§6)은 프로젝트 계약, 어휘 선택(비난 주어, 과한 사과)은 관찰 후보다.
|
||||
|
||||
---
|
||||
|
||||
## 15. 체크리스트
|
||||
|
||||
- [ ] 새 카피를 쓰기 전에 기존 화면·design.md의 용어를 확인했다(§1)
|
||||
- [ ] 같은 개념을 화면마다 다른 말로 부르지 않는다 — grep으로 소수파 표현을 찾아 통일했다(§2)
|
||||
- [ ] 일상·주의·파괴·오류·금전 상황의 톤 강도가 다르다(§3)
|
||||
- [ ] 한 화면·한 제품 안에서 해요체·합니다체가 섞이지 않는다(§4)
|
||||
- [ ] 버튼 라벨이 동사형이고 결과를 말한다. 파괴적 확인 버튼은 결과를 반복한다(§5)
|
||||
- [ ] 같은 행동이 버튼 → 확인 → 토스트까지 같은 이름을 쓴다(§5)
|
||||
- [ ] 오류 문구가 무엇이 잘못됐는지와 어떻게 고치는지를 함께 말한다. 비난·과한 사과·느낌표가 없다(§6)
|
||||
- [ ] 빈 상태가 정체성·이유·다음 행동을 말한다. 검색 빈 상태는 입력한 조건을 보여준다(§7)
|
||||
- [ ] 내부 구현 용어(webhook·config·payload·schema)가 사용자 라벨로 새지 않았다(§8)
|
||||
- [ ] 변수 주변에 문장 조각을 조립하지 않았다. 조사가 필요한 자리는 받침 판별 또는 문장 재구성으로 처리했다(§9)
|
||||
- [ ] 토글 라벨이 켜진 상태를 기준으로 쓰였다(§10-1)
|
||||
- [ ] 링크 텍스트가 맥락 없이도 목적지를 알려준다(§10-2)
|
||||
- [ ] placeholder가 라벨을 대신하지 않고 형식 예시로만 쓰였다(§10-3)
|
||||
- [ ] 아이콘 버튼의 `aria-label`이 화면에 보이는 문구를 포함한다(§10-4)
|
||||
- [ ] 영문 라벨이 있다면 요소 유형별로 대소문자 정책이 통일돼 있다(§12)
|
||||
- [ ] 잘림·줄바꿈에 영향받는 카피(버튼·제목·좁은 카드)는 실제 렌더로 확인했다(§14)
|
||||
|
|
@ -10,6 +10,7 @@
|
|||
| Playful | 탐색과 학습의 긴장을 낮춘다. 핵심 행동은 여전히 예측 가능해야 한다 | 밝은 색은 의미 역할을 분리하고, 일러스트/캐릭터는 과업 단서를 보강 | 작은 보상·피드백은 가능하나 중지 가능성과 오류 회복을 제공 | 교육·커뮤니티·가벼운 소비 경험에서 놀이가 목표를 돕는 경우 | 재미가 상태·가격·위험 신호를 가림 | 처음 방문자가 다음 행동·오류 복구를 설명할 수 있는지, reduced motion |
|
||||
| Immersive | 공간·시간·감각적 몰입을 우선하되 대체 경로를 제공한다. 하나의 장면에서 깊이를 만든다 | 큰 원본 미디어, 제한된 overlay text, 읽을 수 있는 대비와 대체 콘텐츠 | WebGL/스크롤 연출은 progressive enhancement; pause·fallback 필수 | 전시·게임·영화·브랜드 스토리처럼 경험 자체가 가치일 때 | 로딩·멀미·배터리 비용을 감추고, 마우스/고성능 기기만 지원 | 저사양/느린 망/키보드/reduced motion에서 동일 핵심 정보와 행동 가능 여부 |
|
||||
| Dense | 빈번한 비교·감시·편집을 빠르게 한다. 요약→필터→상세의 정보 계층을 명시한다 | 가독성 높은 본문·tabular 수치·기능색·표/차트의 명확한 라벨 | 값 변화는 위치 이동보다 상태 표식; 빠른 피드백 | 운영 도구·분석·거래처럼 반복 과업과 숙련 사용자가 있을 때 | 미니멀이라는 이유로 필터·단위·오류 맥락을 숨김; 색만으로 상태 전달 | 대표 과업 시간, 200% 확대, keyboard, 좁은 폭의 우선순위/대체 경로 |
|
||||
| Platform-native (Apple풍) | PWA·모바일 웹앱이 OS 네이티브 앱과 나란히 놓일 때 플랫폼 소속감을 만든다. 정보 구조는 위 계열 중 과업에 맞는 것을 그대로 쓰고, 이 행은 표면 마감만 더한다 | 유동 인터페이스 물리(인터럽트 가능한 스프링, 제스처 인식), 머티리얼 위계([svg-filters.md](svg-filters.md) 유리 절), 시스템 폰트의 옵티컬 사이징을 존중하는 타이포 | 스프링 기반 전환, 인터럽트 가능성, 제스처에 반응하는 촉감([interaction-feel.md](interaction-feel.md)) | 브리프가 "iOS 느낌"·"네이티브 앱 감각"을 명시했을 때만. 그 자체가 목표 시장이 아니면 열지 않는다 | 특정 미학을 기본값으로 삼아 모든 프로젝트에 유리·스프링을 얹음; 저사양·Firefox에서 무너짐 | 필터 off·CPU throttling 4×, Firefox·저사양 기기에서 폴백이 성립하는지, reduced motion·reduced transparency |
|
||||
|
||||
## 계열별 빠른 선택 예와 오용
|
||||
|
||||
|
|
@ -49,6 +50,15 @@
|
|||
- **밀도·타입**: tabular figure, 단위·기간 라벨, 기능색, 안정된 행 높이로 반복 과업의 속도를 돕는다.
|
||||
- **오용**: ‘깔끔함’을 이유로 필터·오류·데이터 출처·단위를 감추거나 색만으로 상태를 전달하지 않는다.
|
||||
|
||||
### Platform-native (Apple풍) — 조건부
|
||||
|
||||
언제: 브리프가 "iOS 느낌"·"네이티브 앱처럼"을 명시했거나 PWA가 OS 네이티브 앱 옆에 나란히 설치되는 과업일 때만 연다. 이 계열은 정보 구조를 새로 정하지 않는다 — 위 여섯 계열 중 과업에 맞는 것으로 뼈대를 고르고, 이 행은 그 위에 표면 마감만 얹는다.
|
||||
|
||||
- **구도**: 전환은 하드 컷이 아니라 스프링 기반으로 하고, 진행 중인 애니메이션을 중간에 잡아도(interrupt) 속도가 끊기지 않게 한다. 제스처(드래그·스와이프)에 실시간으로 반응하는 표면을 우선 검토한다. 상세 물리·수치는 [interaction-feel.md](interaction-feel.md)를 따른다.
|
||||
- **재질**: 유리·머티리얼 위계는 [svg-filters.md](svg-filters.md)의 "머티리얼 위계와 가독성" 절을 그대로 따른다 — `swiss-minimal`은 이 계열과 함께 쓰지 않는다.
|
||||
- **타입**: 커스텀 서체보다 플랫폼 시스템 폰트를 기본으로 검토한다. 이미 옵티컬 사이징과 트래킹 테이블이 튜닝돼 있다. 오버라이드는 근거가 있을 때만 한다[SKILL-APPLE-DESIGN].
|
||||
- **오용**: 이 표면 마감을 모든 프로젝트의 기본값처럼 반복하지 않는다. 유리·스프링·머티리얼이 저사양 기기와 Firefox에서 무너지지 않는지 §7(svg-filters.md)의 폴백 규칙으로 확인한다 — "예뻐서"가 채택 이유가 되지 않는다.
|
||||
|
||||
## 사례와 트렌드의 사용법
|
||||
|
||||
수상 사례는 완성된 화면을 베끼는 재료가 아니라 제약·구도·사용자 경로를 분해할 표본이다. Awwwards의 40/30/20/10은 그 심사 시스템의 배점일 뿐 효과 크기가 아니다 [AWWWARDS]. Webby와 CSSDA 사례도 심사/제작사 기록으로만 사용한다 [WEBBY, CSSDA-UNSEEN, AREA17, DROPBOX]. iF trend report는 가설 목록이며 웹 제품의 성과 증거가 아니다 [IFD-TREND].
|
||||
|
|
|
|||
|
|
@ -685,3 +685,40 @@ data URI 안에서 `#` 은 URI 를 끊으므로 색은 `rgb()` 로 적는다.
|
|||
|
||||
`backdrop-filter` 앞의 `blur()` 는 약하게 둬라. 세게 걸면 굴절이 뭉개져서
|
||||
지도를 고쳐도 차이가 안 보인다.
|
||||
|
||||
---
|
||||
|
||||
## 머티리얼 위계와 가독성 (vibrancy)
|
||||
|
||||
언제: 브리프나 골라 둔 프리셋이 유리·Apple풍 질감을 허용할 때만 연다. `swiss-minimal`은 유리 금지가 그대로 유지된다(§1). 아래 규칙은 "모든 nav·toolbar·sheet를 backdrop-filter로 만들어라"는 지시가 아니다 — 유리를 쓰기로 한 뒤의 마감 규칙이다[SKILL-APPLE-DESIGN].
|
||||
|
||||
**블러 위 텍스트 가독성(vibrancy)**: 반투명·블러 표면 위에 플랫 그레이 텍스트를 얹지 마라. 배경이 바뀌어도 읽히도록 대비를 올리고, 자간을 미세하게 늘리고, 굵기를 한 단계 올린다. 색은 솔리드 레이어에 두고 반투명 전경에 두지 않는다 — 반투명 위에서는 배경에 따라 채도가 흔들린다.
|
||||
|
||||
**반투명 표면끼리 겹치지 않기**: 밝은 반투명 표면을 다른 반투명 표면 위에 쌓지 마라. 겹칠수록 대비가 무너져 가독성이 붕괴한다. 겹쳐야 하면 아래쪽을 솔리드에 가깝게 불투명도를 올려라.
|
||||
|
||||
**무거운 재질은 구조, 가벼운 재질은 상호작용**: 더 어둡고 무거운 유리(강한 블러 + 깊은 그림자)는 사이드바 같은 구조적 영역을 분리하는 데 쓰고, 더 밝고 가벼운 유리는 버튼 같은 인터랙티브 요소로 주의를 끄는 데 쓴다. 큰 표면일수록 더 두껍게(강한 블러 + 깊은 그림자) 읽혀야 한다 — 작은 칩에 큰 표면급 블러를 걸지 않는다. 그림자 자체의 단계·값은 [elevation.md](elevation.md)를 따른다. 이 절은 그중 유리 재질에만 적용되는 위계 규칙이다.
|
||||
|
||||
**스티키 헤더는 그래디언트 마스크로**: 스티키 헤더 아래에 1px 하드 보더를 긋지 마라. 플로팅 크롬이 콘텐츠와 실제로 겹치는 부분에만 작은 블러 또는 그래디언트 마스크를 페이드시켜, 경계가 아니라 스크롤 엣지 효과로 읽히게 한다.
|
||||
|
||||
**materialize 전환**: 유리·블러 표면의 진입·퇴장은 `opacity` 하나만 바꾸지 마라. 블러 반경과 `scale`을 함께 애니메이션해 "머티리얼이 실제로 도착한다"는 인상을 만든다. 필터 애니메이션의 비용·감소 모션 경로는 §6·§7을 그대로 따른다 — 이 절이 그 규칙을 면제하지 않는다.
|
||||
|
||||
```css
|
||||
.sheet {
|
||||
opacity: 0; transform: scale(0.96); backdrop-filter: blur(0px);
|
||||
transition: opacity var(--dur-quick) var(--ease-out), transform var(--dur-quick) var(--ease-out),
|
||||
backdrop-filter var(--dur-quick) var(--ease-out); /* 새 duration 리터럴을 만들지 않는다(tokens.md §4) */
|
||||
}
|
||||
.sheet.is-open {
|
||||
opacity: 1; transform: scale(1); backdrop-filter: blur(18px) saturate(1.4);
|
||||
}
|
||||
```
|
||||
|
||||
**`prefers-reduced-transparency` 대체**: 이 미디어 쿼리는 Chromium 계열(Chrome·Edge)에만 있고 Firefox·Safari는 지원하지 않는다[WEB-BASELINE] — 지원 브라우저에서만 통하는 점진적 향상으로 다룬다. 감지되면 반투명 표면의 배경 불투명도를 올리고 블러를 낮춘다. 미지원 브라우저에서도 §7의 저사양 감지(`reduce-effects` 클래스)와 "필터를 전부 끈 상태에서 성립해야 한다"는 원칙이 안전망 역할을 한다.
|
||||
|
||||
```css
|
||||
@media (prefers-reduced-transparency: reduce) {
|
||||
.glass { backdrop-filter: blur(6px); background: rgba(255, 255, 255, 0.82); }
|
||||
}
|
||||
```
|
||||
|
||||
그라디언트를 재질의 일부로 쓸 때(예: 듀오톤·머티리얼 틴트)의 보간 공간 선택은 [color.md](color.md)를 참조한다.
|
||||
|
|
|
|||
|
|
@ -374,7 +374,7 @@ export function createStage(canvas, { dprMax = 1.5, alpha = false } = {}) {
|
|||
## 4. 코드 B — 메시 그라디언트 셰이더 플레인
|
||||
|
||||
**언제**: 랜딩 히어로 배경의 기본 무기. three를 쓰기로 했다면 90%는 이것으로 끝난다. 드로우콜 1개, 텍스처 0장.
|
||||
**색과 속도는 3단계 토큰에서 읽는다. 셰이더에 hex를 쓰지 마라** (하드 게이트 #1).
|
||||
**색과 속도는 3단계 토큰에서 읽는다. 셰이더에 hex를 쓰지 마라** (프로젝트 계약 — 토큰이 프로젝트 결정을 반영하는가).
|
||||
|
||||
```js
|
||||
// src/gl/scenes/hero.js
|
||||
|
|
@ -614,7 +614,7 @@ export default function mount(canvas, { selector = 'img[data-gl]' } = {}) {
|
|||
stage.onResize(stage.size.w, stage.size.h)
|
||||
|
||||
stage.add((t, dt) => {
|
||||
// 스크롤 값은 rAF 에서 직접 읽는다. `scroll` 리스너는 하드 게이트 #10 위반이고,
|
||||
// 스크롤 값은 rAF 에서 직접 읽는다. `scroll` 리스너는 '애니메이션·스크롤 로직이 입력을 방해하지 않는다' 하드 게이트 위반이고,
|
||||
// 어차피 매 프레임 필요한 값이다.
|
||||
const k = 1 - Math.pow(.001, dt), hk = 1 - Math.pow(.0005, dt), prev = scroll.cur
|
||||
scroll.cur += (scrollY - scroll.cur) * k
|
||||
|
|
|
|||
|
|
@ -52,6 +52,8 @@
|
|||
|
||||
행간과 measure 범위는 시작값이다. 한글 서체·문자·폭·실제 문단을 렌더하고 확대 상태까지 확인해 정한다. 폰트 수는 전송량·언어 범위·위계·라이선스를 함께 보고 예산으로 정한다. 두 텍스트 패밀리와 필요 시 모노는 흔한 시작점이지만, 세 번째 패밀리가 무조건 실패라는 뜻은 아니다.
|
||||
|
||||
**관찰 후보 — 라틴 측정폭 60자 하한은 고정선이 아니다.** 사이드바·카드처럼 컬럼 자체가 좁을 때는 45~59자 구간도 시작값으로 허용한다. 다른 스킬은 45~75자 범위를 제시하지만, 이 스킬의 60~75자·한글 25~40자 값을 대체하는 것이 아니라 좁은 컬럼에서만 여는 예외다 [SKILL-DESIGN-REVIEW].
|
||||
|
||||
> **어떤 폰트를 어떻게 고르고 싣는지는 `typography.md` 가 정본이다.** 여기서는 스케일 값만 정한다.
|
||||
> 로딩 전략, 폴백 메트릭 보정, OpenType 기능, 한글 서브셋, 라이선스 확인이 그 문서에 있다.
|
||||
|
||||
|
|
@ -75,6 +77,8 @@
|
|||
}
|
||||
```
|
||||
|
||||
> **색 체계의 램프 생성·OKLCH 파생·그라디언트 보간·다크모드 재조정·대비 수정 절차는 [color.md](color.md) 가 정본이다.** 여기서는 역할 토큰 7개만 정한다. 여기 없는 역할이 필요할 때(상태색을 더 세분하거나 채색 액션을 늘릴 때) 확장하는 방법은 color.md §8을 본다.
|
||||
|
||||
### 규칙
|
||||
|
||||
1. 강조 역할은 한 색에서 시작해 경쟁 여부를 검증한다. 여러 강조 역할이 필요할 수 있으며, 그때는 행동·상태·정보 목적을 역할 토큰으로 구분한다. 수만으로 위계 실패를 판정하지 않는다
|
||||
|
|
@ -229,7 +233,7 @@ h3 + *,
|
|||
|
||||
## 3-b. 형태 (radius·선)
|
||||
|
||||
하드 게이트 #3 이 검사하는 대상이다. **정의하지 않으면 검사할 수 없다.**
|
||||
'스타일 휴리스틱 — radius 어휘를 3종 미만으로 제한한다'가 검사하는 대상이다. **정의하지 않으면 검사할 수 없다.**
|
||||
|
||||
```css
|
||||
:root {
|
||||
|
|
@ -243,6 +247,24 @@ h3 + *,
|
|||
**서로 다른 radius 값은 3종 미만으로.** 어휘가 많을수록 우연히 결정된 것으로 보인다.
|
||||
1종만 쓰는 것도 정당한 선택이다 — `swiss-minimal`·`anti-grid` 에서는 오히려 그쪽이 맞다.
|
||||
|
||||
### 동심 radius — 중첩된 둥근 요소의 계산식
|
||||
|
||||
**관찰 후보 — 취향이 아니라 기하학이다.** 둥근 요소 안에 다른 둥근 요소를 넣을 때(배지를 담은 카드, 아이콘을 담은 버튼) 바깥 radius를 안쪽 radius와 무관하게 고르면 두 모서리의 곡률이 어긋나 인터페이스가 어색해 보인다. 바깥 radius는 **안쪽 radius + 안쪽 요소까지의 패딩**으로 계산한다.
|
||||
|
||||
```
|
||||
outer-radius = inner-radius + padding
|
||||
```
|
||||
|
||||
예: 안쪽 배지가 `--radius-sm`(2px)이고 카드 패딩이 `--space-3`(16px)이면 카드는 지역 토큰으로 이렇게 계산한다.
|
||||
|
||||
```css
|
||||
.card { --card-radius: calc(var(--radius-sm) + var(--space-3)); border-radius: var(--card-radius); }
|
||||
```
|
||||
|
||||
동심 계산으로 파생한 지역 값은 radius 어휘(3종 미만)에 새 종류로 세지 않는다 — 기준 radius에서 계산으로 나온 값이다. 패딩이 24px를 넘으면 이 관계가 시각적으로 느슨해지므로, 그 지점부터는 바깥 radius를 독립적으로 다시 고른다 [SKILL-BETTER-UI].
|
||||
|
||||
그림자·깊이 토큰과 radius를 함께 짝짓는 규칙은 [elevation.md](elevation.md) §3(radius·표면과 짝짓기)을 따른다.
|
||||
|
||||
---
|
||||
|
||||
## 4. 모션 토큰
|
||||
|
|
@ -262,6 +284,12 @@ h3 + *,
|
|||
|
||||
상세는 `references/motion.md`. 여기서는 **값을 고정**하는 것이 목적이다. 컴포넌트마다 다른 duration 을 쓰면 페이지가 불안해 보인다.
|
||||
|
||||
**프로젝트 계약 — 새 duration 리터럴을 만들지 않는다.** 외부 레퍼런스나 라이브러리 프리셋이 다른 수치를 제시해도(예: 프레스 피드백 `scale(0.96)`~`scale(0.98)`) 그 값을 위 4단계 duration 토큰 중 가장 가까운 것에 매핑해 쓴다 — 프레스처럼 즉시 반응해야 하는 상태 변화는 `--dur-instant`가 기본 후보다 [SKILL-APPLE-DESIGN][SKILL-BETTER-UI]. 값 자체(0.96 대 0.98)의 차이는 실질적이지 않으므로 번호를 맞추려 하지 마라.
|
||||
|
||||
**관찰 후보 — 드로어류에는 별도 이징 곡선을 조건부로 둘 수 있다.** iOS 스타일 드로어가 브리프의 요구일 때만 위 3단계 이징에 추가하지 않고 드로어 컴포넌트 스코프에 로컬로 `--ease-drawer`를 정의해 쓴다. 값과 적용 범위는 `references/motion.md`(§1 이징 각주)와 일관되게 유지한다 [SKILL-EMIL-DESIGN-ENG].
|
||||
|
||||
**프로젝트 계약 — `prefers-contrast`에도 반응한다.** WCAG 성공 기준은 아니지만, `prefers-reduced-motion`만 처리하고 대비를 더 원하는 사용자 설정을 무시하면 색 토큰이 그 사용자에게는 그대로 방치된다. `prefers-contrast`의 지원 상태와 판정 위계는 [accessibility.md](accessibility.md) §12가 정본이고, 램프를 실제로 재조정할 때 쓰는 색 쪽 기법(명도 우선 조정·재측정)은 [color.md](color.md) §6을 따른다 [SKILL-APPLE-DESIGN].
|
||||
|
||||
---
|
||||
|
||||
## 웨이트와 컨트롤 — 자주 빠지는 두 축
|
||||
|
|
@ -394,7 +422,7 @@ new PerformanceObserver((l) => {
|
|||
--surface, --surface-raised, --ink, --ink-muted, --line, --accent, --accent-ink
|
||||
/* 간격 */
|
||||
--space-1 ~ --space-8
|
||||
/* 형태 — 하드 게이트 #3 이 이걸 검사한다 */
|
||||
/* 형태 — '스타일 휴리스틱 — radius 어휘를 3종 미만으로 제한한다'가 이걸 검사한다 */
|
||||
--radius-*, --line-width
|
||||
/* 모션 */
|
||||
--dur-*, --ease-*
|
||||
|
|
|
|||
|
|
@ -15,6 +15,13 @@
|
|||
| 숫자가 흔들리거나 자간이 이상하다 | §5 OpenType |
|
||||
| 한글이 들어간다 | §6. 조판 규칙 자체는 `antipatterns.md` §8 |
|
||||
| 배포 전 | §7 라이선스, §8 체크리스트 |
|
||||
| 헤딩 크기·자간·행간을 어떻게 조정할지 원칙이 필요하다 | §9 |
|
||||
| 폰트 기능(`font-synthesis`, OpenType, 동적 숫자)을 더 쓰고 싶다 | §10 |
|
||||
| 줄바꿈·위도우·justify 문제가 있다 | §11 |
|
||||
| 밑줄이나 `text-box` 트림이 필요하다 | §12 |
|
||||
| 텍스트가 잘려서 전체 값을 볼 수 없다 | §13. **하드 게이트** |
|
||||
| 폰트 스무딩·선택 가능성·장식 텍스트 디테일이 필요하다 | §14 |
|
||||
| 배포 직전 타이포 전용 빠른 점검이 필요하다 | §15 |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -330,3 +337,288 @@ font-family: "Switzer", "Pretendard Variable", system-ui, sans-serif;
|
|||
- [ ] 폰트를 못 받은 상태로 페이지를 열어봤다. 그 상태로도 읽힌다
|
||||
|
||||
> 근거: research/references/03-trends-2026.md §7, 04-ai-slop-signatures.md §8 (조사일 2026-08-20)
|
||||
|
||||
---
|
||||
|
||||
## 9. 위계와 자간·행간 보강
|
||||
|
||||
### 9-1. 자간·행간은 크기의 함수다 — 통합 원칙
|
||||
|
||||
이 문서와 `tokens.md`에 이미 흩어져 있는 개별 규칙은 하나의 원리를 공유한다. **글자가 커질수록 자간은 좁아지고 행간은 타이트해지며, 글자가 작아질수록 자간은 넓어지고 행간은 느슨해진다.** 큰 디스플레이 텍스트에서 음수 자간을 후보로 두는 이유([tokens.md](tokens.md) §1 "함께 정해야 하는 것")도, 작은 대문자 라벨에 양의 자간을 주는 이유(§5의 `all-small-caps` 예시)도, 헤딩 행간을 좁히고(1.1~1.2대) 본문 행간을 넓히는(1.5~1.6, 한글 1.6~1.8, [tokens.md](tokens.md) §1) 이유도 같은 함수의 다른 지점일 뿐이다.
|
||||
|
||||
이 절은 **새 강제 수치를 만들지 않는다.** `tokens.md`·`antipatterns.md` §2가 이미 정한 값과 "보편 하한을 선언하지 않는다"는 방향은 그대로 유지한다. 여기서 더하는 것은 흩어진 개별 사례를 하나의 원리로 묶어 읽는 프레임뿐이다 [SKILL-BETTER-TYPE].
|
||||
|
||||
### 9-2. 타입을 능동적 디자인 요소로 쓰기 — 관찰 후보
|
||||
|
||||
**언제**: 헤드라인이나 숫자 자체가 화면의 시각적 주인공일 때(히어로 숫자, 잡지형 표지, 데이터 하나를 강조하는 카드).
|
||||
|
||||
이런 자리에서는 타입 처리 자체가 콘텐츠를 담기만 하는 중립적 그릇이 아니라 디자인의 능동적인 부분이 될 수 있다[SKILL-FRONTEND-DESIGN]. 예: 큰 사이즈, 의도적 절단(뷰포트 경계 밖으로 흘러나가는 글자), 겹침, 회전, 색 분리 같은 처리는 타이포그래피가 "읽히는" 것을 넘어 그 자체로 이미지가 되게 한다 — 이 나열 자체는 출처 없음(designpaca 확장)이다. **관찰 후보**다 — 본문·UI 라벨·폼처럼 기능이 우선인 텍스트에는 적용하지 않는다. 가독성이 과업에 실제로 필요한 자리(§13의 하드 게이트가 도는 자리)에서는 장식이 접근을 이기지 않는다.
|
||||
|
||||
### 9-3. Display와 Text는 다른 폰트다
|
||||
|
||||
"Display"라는 이름이 큰 사이즈용 최적화를 보장하지 않는다. 반대도 마찬가지다. SF Pro, Heldane 같은 일부 패밀리는 **큰 사이즈용 Display 변형과 작은 사이즈용 Text 변형을 별도 파일로 낸다** — Text 변형은 작은 크기에서도 읽히도록 획이 굵고 속공간이 넓게 그려지고, Display 변형은 큰 크기에서만 의도가 살도록 더 섬세하게 그려진다 [SKILL-BETTER-TYPE]. 가변 폰트라면 `font-optical-sizing: auto`(§2)가 이 전환을 자동으로 처리하지만, Display/Text가 별도 정적 파일로만 나오는 패밀리는 **설정하는 크기에 맞는 변형을 직접 골라야 한다.** 헤딩에 Display 파일을, 본문에 Text 파일을 매핑하지 않으면 한쪽에서 항상 맞지 않는 파일을 쓰게 된다.
|
||||
|
||||
### 9-4. 헤딩 위계 — 내림차순과, 본문보다 작아지지 않는 하한 (프로젝트 계약)
|
||||
|
||||
**프로젝트 계약**이다. 타입 스케일을 정했다면(`tokens.md` §1) 그 스케일에 헤딩 레벨을 매핑하는 기본 규칙이지, 위계가 "실제로 읽힌다"는 것을 증명하는 검사 자체는 아니다. 위계가 실제로 구별되는지는 `tokens.md` "제목 역할이 실제로 구분되는지 확인한다" 절의 실측 방법(여러 폭에서 계산된 크기를 재는 것)을 그대로 따른다 — 단일 비율 하나로 위계를 증명할 수 있다는 태도는 [design-foundations.md](design-foundations.md) §1이 이미 잘못된 일반화로 지목했다.
|
||||
|
||||
규칙은 두 가지다 [SKILL-BETTER-TYPE].
|
||||
|
||||
1. **헤딩 레벨은 타입 스케일의 내림차순 단계에 매핑한다.** h1이 h2보다, h2가 h3보다 시각적으로 작아서는 안 된다. 스케일 단계가 부족해 인접 레벨이 같은 크기를 공유해야 하면, 굵기나 자간으로 구별을 유지한다.
|
||||
2. **헤딩은 본문보다 작아지지 않는다.** 예외는 의도적으로 라벨형 오버라인(예: "CASE STUDY"처럼 본문 위에 놓이는 작은 대문자 태그)으로 쓸 때뿐이다. 어떤 시맨틱 요소를 쓸지는 [accessibility.md](accessibility.md) 소관이고, 이 규칙은 고른 요소의 시각적 크기만 다룬다 — `<h3>`를 골랐다고 브라우저 기본 크기를 그대로 두지 않는다.
|
||||
|
||||
---
|
||||
|
||||
## 10. 폰트 렌더링 제어 보강
|
||||
|
||||
### 10-1. font-synthesis — shorthand 대신 longhand
|
||||
|
||||
`font-synthesis: none`은 굵기·기울임·small-caps·상하첨자 **합성을 한 번에 다 끈다.** 폴백 스택 전체에서 필수 강조(굵게, 기울임)가 실제 글리프로 구별되는지 검증하기 전에 이 shorthand를 쓰면, 폴백으로 떨어졌을 때 강조가 조용히 사라진다 — 오류도 보고도 없이.
|
||||
|
||||
**한 모드만 끄고 싶으면 shorthand 대신 개별 속성을 쓴다** [SKILL-BETTER-TYPE].
|
||||
|
||||
```css
|
||||
/* 브랜드 워드마크처럼 합성된 굵기가 눈에 띄게 다를 때만, 검증 후 좁게 적용 */
|
||||
.brand-wordmark {
|
||||
font-synthesis-weight: none;
|
||||
}
|
||||
|
||||
/* shorthand는 폴백 전체의 강조 표현을 확인한 뒤에만 */
|
||||
.isolated-treatment {
|
||||
font-synthesis: none;
|
||||
}
|
||||
```
|
||||
|
||||
### 10-2. OpenType 확장 기능
|
||||
|
||||
§5가 이미 다루는 `tabular-nums`·`oldstyle-nums`·`diagonal-fractions`·`ordinal`·`all-small-caps`·`ss01` 외에, 자주 쓰는 나머지 기능이다 [SKILL-BETTER-TYPE].
|
||||
|
||||
```css
|
||||
/* 0과 O를 구별해야 하는 곳 — 일련번호, 인증코드, 주문번호 */
|
||||
.serial { font-variant-numeric: slashed-zero; }
|
||||
|
||||
/* 리가처(fi, fl 합자 등)를 명시적으로 켠다. 대부분 기본 켜짐이지만 폰트가 다르면 확인한다 */
|
||||
.prose { font-variant-ligatures: common-ligatures; }
|
||||
|
||||
/* 위·아래 첨자. 폰트가 실제 첨자 글리프를 담고 있어야 한다 — 없으면 인위적 축소로 대체된다 */
|
||||
.formula-sup { font-variant-position: super; } /* x² 의 2 */
|
||||
.formula-sub { font-variant-position: sub; } /* H₂O 의 2 */
|
||||
|
||||
/* 문자 변형(character variant). 슬롯 번호의 의미는 폰트마다 다르다 */
|
||||
.logo { font-feature-settings: "cv11" 1; }
|
||||
```
|
||||
|
||||
`ss01`~`ss20`(스타일 세트), `cv01`~`cv99`(문자 변형)는 번호가 폰트마다 다른 의미를 가지므로 쓰기 전에 그 폰트의 OpenType 기능 문서를 확인한다. 표준 속성(`font-variant-*`)이 있는 기능은 그것을 먼저 쓴다 — `font-feature-settings`는 표준 속성이 없는 기능에만 쓰는 마지막 수단이라는 §5의 원칙은 여기서도 그대로다.
|
||||
|
||||
### 10-3. 숫자가 흔들리면 안 되는 곳 — tabular-nums 확장
|
||||
|
||||
§5는 이미 "수치를 나열하는 곳"(표·대시보드·가격표)에 `tabular-nums`를 요구한다. 같은 이유가 **값이 실시간으로 바뀌는 자리**에도 그대로 적용된다 — 타이머, 카운트다운, 실시간 가격, 진행률 퍼센트처럼 숫자가 갱신될 때마다 폭이 바뀌면 주변 레이아웃이 갱신마다 흔들린다 [SKILL-BETTER-TYPE].
|
||||
|
||||
```css
|
||||
/* 정적인 표뿐 아니라, 매초 값이 바뀌는 타이머에도 같은 이유로 필요하다 */
|
||||
.countdown-digit { font-variant-numeric: tabular-nums; }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. 줄바꿈과 정렬
|
||||
|
||||
### 11-1. 자동 줄바꿈 선언 — text-wrap:balance / pretty
|
||||
|
||||
**지원 상태 (확인 2026-09-24)** [WEB-BASELINE].
|
||||
|
||||
| 선언 | 상태 | 지원 브라우저 |
|
||||
|---|---|---|
|
||||
| `text-wrap: balance` | Baseline **Newly available**(아직 Widely 아님) | Chrome/Chrome Android 114, Edge 114, Firefox/Firefox Android 121, Safari/Safari iOS 17.5 |
|
||||
| `text-wrap: pretty` | Baseline **Limited**(Baseline 아님) — **Firefox 미구현** | Chrome/Chrome Android 117, Edge 117, Safari/Safari iOS 26 |
|
||||
|
||||
두 값 모두 미지원 브라우저에서는 기본 줄바꿈으로 조용히 저하된다(레이아웃이 깨지지 않는다) — **점진적 향상으로 쓴다.** Firefox 비중이 큰 프로젝트라면 `pretty`가 주요 엔진 셋 중 Firefox만 미지원이라는 점을 감안한다.
|
||||
|
||||
```css
|
||||
/* 두 줄 헤딩이 한쪽으로 쏠리지 않게 폭을 맞춘다 */
|
||||
h1, h2, h3 { text-wrap: balance; }
|
||||
|
||||
/* 설명 문단의 위도우를 줄인다. 장문에는 쓰지 않는다(아래 §11-2) */
|
||||
.lead, .description { text-wrap: pretty; }
|
||||
```
|
||||
|
||||
`balance`는 브라우저가 몇 줄을 넘으면 스스로 적용을 포기한다 — 헤딩·소제목처럼 짧은 텍스트에 쓴다. 장문 본문에는 `balance`도 `pretty`도 쓰지 않는다. 문단 전체를 고르게 만들려는 시도는 공간을 낭비하고 가독성을 해칠 수 있다 [SKILL-BETTER-TYPE].
|
||||
|
||||
### 11-2. widow · orphan · river
|
||||
|
||||
- **widow(과부행)**: 문단이나 헤드라인의 마지막 줄에 단어 하나만 남는 것.
|
||||
- **orphan(고아행)**: 문단의 첫 줄이 혼자 떨어져 남는 것.
|
||||
- **river(리버)**: justify 정렬에서 여러 줄에 걸쳐 흰 공백이 세로로 이어져 보이는 것.
|
||||
|
||||
세 가지 모두 **관찰 후보**다. 헤드라인·리드 문단에서는 `text-wrap: balance`/`pretty`(§11-1)로 대부분 해소되고, 미지원 브라우저나 예외적인 문구 길이에서는 수동 개행(` `로 마지막 두 단어를 묶기)으로 보정한다. river는 justify 정렬(§11-4)에서만 나타나므로 인터페이스 본문에 justify를 쓰지 않으면 대부분 발생하지 않는다 [SKILL-DESIGN-REVIEW]. 실제 콘텐츠 길이와 뷰포트 폭으로 렌더해 확인한다 — 코드만 보고는 판정할 수 없다.
|
||||
|
||||
### 11-3. overflow-wrap과 white-space
|
||||
|
||||
- `overflow-wrap: anywhere` — 긴 단어·URL·ID가 컨테이너를 밀어내지 않게 강제로 끊는다. [preflight.md](preflight.md)의 배포 전 체크리스트가 이미 이 값을 요구한다. `break-word`보다 레이아웃 계산에서 더 안전한 값이다.
|
||||
- `white-space: nowrap` — 배지·라벨처럼 줄바꿈되면 의미가 깨지는 짧은 텍스트에 쓴다. 컨테이너보다 라벨이 길어질 가능성이 있으면 `text-overflow: ellipsis`(§13)를 함께 둔다.
|
||||
|
||||
한글 문서에서 `word-break`가 자소를 끊는 문제와 그 대응은 [antipatterns.md](antipatterns.md) §8이 정본이다 — 여기서는 라틴 문자열·URL·ID에 쓰는 `overflow-wrap`만 다룬다. 둘은 다른 문제를 푼다. `overflow-wrap`은 끊을 지점이 없는 긴 토큰의 예외 처리이고, 한글 `word-break` 규칙은 정상적인 어절 단위 줄바꿈 자체를 다룬다.
|
||||
|
||||
### 11-4. justify 사용 제한
|
||||
|
||||
**관찰 후보**다 — [antipatterns.md](antipatterns.md) §3의 판정과 같다. `text-align: start`를 인터페이스 기본으로 하고, `justify`는 자동 하이픈네이션이 걸린 좁은 에디토리얼 컬럼처럼 특정 레이아웃에서만 검토한다 [SKILL-BETTER-TYPE]. 인터페이스의 나머지 자리(카드 설명, 폼 도움말, 버튼 라벨)에 justify를 쓰면 단어 사이 공백이 불규칙해지고 §11-2의 river가 나타나기 쉽다. 한글은 어절 단위로 줄바꿈되므로 river가 나타나는 양상이 라틴과 다르다 — 실제 렌더로 확인한다.
|
||||
|
||||
### 11-5. 문장부호
|
||||
|
||||
한국어 문장부호(따옴표, 줄임표, 붙임표, 값과 단위 사이 공백 처리)의 관례는 [product-copy.md](product-copy.md) §11이 정본이다. 이 문서는 줄바꿈·렌더 규칙만 다루고 문장부호 자체는 다루지 않는다.
|
||||
|
||||
---
|
||||
|
||||
## 12. 밑줄과 트리밍
|
||||
|
||||
### 12-1. 밑줄 메트릭
|
||||
|
||||
폰트가 내장한 밑줄 위치·두께를 기본값으로 쓴다.
|
||||
|
||||
```css
|
||||
a {
|
||||
text-underline-position: from-font;
|
||||
text-decoration-thickness: from-font;
|
||||
text-decoration-skip-ink: auto; /* 하강부(g, y, p)를 밑줄이 가로지르지 않게 */
|
||||
}
|
||||
```
|
||||
|
||||
`from-font`가 원하는 두께를 안 주거나 폰트가 메트릭을 제공하지 않으면 수동으로 조정한다 [SKILL-BETTER-TYPE].
|
||||
|
||||
```css
|
||||
a {
|
||||
text-decoration-thickness: 1px; /* 실측 조정 — 시작값 */
|
||||
text-underline-offset: 3px; /* 실측 조정 — 시작값 */
|
||||
text-decoration-skip-ink: auto;
|
||||
}
|
||||
```
|
||||
|
||||
**관찰 후보 — 실측으로 확인한다.** 밑줄에서 애니메이션이 안정적으로 동작하는 속성은 `color`뿐이라는 관찰이 있다. `text-decoration-thickness`나 `text-underline-offset`을 트랜지션에 넣으면 브라우저마다 다르게(또는 전혀) 움직일 수 있다. 두께·오프셋이 변하는 밑줄 애니메이션(왼쪽에서 자라나는 효과 등)이 필요하면 `text-decoration`을 쓰지 말고 별도 요소(가상 요소 `::after`의 `transform: scaleX()`)로 구축하는 대안을 검토한다. 점선 밑줄(`text-decoration-style: dotted`)은 약어·정의어 같은 부가 정보 힌트의 관례로 쓴다.
|
||||
|
||||
### 12-2. text-box trim — 점진적 향상
|
||||
|
||||
**지원 상태 (확인 2026-09-24)** — 두 추적기가 서로 다르게 보고한다 [WEB-BASELINE]. MDN은 Baseline **Newly available**(2026년 8월부터)로 표시하지만, webstatus.dev는 여전히 **Limited**로 표시하며(Chrome/Chrome Android 133, Edge 133, Safari/Safari iOS 18.2, **Firefox 지원 정보 없음**) 두 출처가 어긋난다. Firefox의 실제 지원 시점은 이 조사 시점 기준 확인되지 않았다.
|
||||
|
||||
`text-box`는 글자 위·아래의 행간 여백(폰트가 내장한 ascender/descender 공간) 중 시각적으로 불필요한 부분을 잘라낸다 — 배지·필처럼 텍스트를 컨테이너에 광학적으로 정확히 맞춰야 하는 좁은 자리에 쓴다.
|
||||
|
||||
```css
|
||||
/* 위아래 모두 트림 */
|
||||
.badge { text-box: trim-both cap alphabetic; }
|
||||
/* 위쪽만 (큰 헤딩이 컨테이너 상단에 붙어야 할 때) */
|
||||
.heading { text-box: trim-start cap; }
|
||||
/* 아래쪽만 */
|
||||
.label { text-box: trim-end alphabetic; }
|
||||
```
|
||||
|
||||
미지원 브라우저에서는 이 선언이 무시되고 기존 여백이 그대로 남는다 — 레이아웃이 깨지지 않으므로 **점진적 향상으로 쓴다.** 광학 정렬이 하드 게이트인 자리는 없다. 지원 브라우저에서 더 정확해지는 정도로 다룬다.
|
||||
|
||||
---
|
||||
|
||||
## 13. 잘린 텍스트 — 전체 값에 도달하는 수단 (하드 게이트)
|
||||
|
||||
**하드 게이트**다. 정보 접근 자체가 걸린 문제이기 때문이다. 이 규칙은 [SKILL.md](../SKILL.md)의 규범·기능 하드 게이트, [accessibility.md](accessibility.md), [critique.md](critique.md)의 에스컬레이션 표와 같은 판정을 공유한다.
|
||||
|
||||
절단 자체는 금지가 아니다.
|
||||
|
||||
```css
|
||||
/* 한 줄 절단 */
|
||||
.truncate {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 여러 줄 절단 */
|
||||
.clamp {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
```
|
||||
|
||||
**금지되는 것은 잘린 정보가 어디서도 완전한 형태로 확인될 수 없는 상태다.** 잘려나간 값이 의미를 가지면(전체 파일명, 긴 이메일 제목, 잘린 가격 설명), 다음 중 하나로 전체 값에 도달할 수단을 남긴다.
|
||||
|
||||
- 네이티브 `title` 속성 또는 커스텀 툴팁으로 hover·focus 시 전체 텍스트 노출(키보드 포커스에서도 동작해야 한다 — [accessibility.md](accessibility.md)의 포커스 규칙을 따른다)
|
||||
- 클릭·탭으로 여는 확장 뷰(상세 페이지, 모달, 아코디언)
|
||||
- `aria-label`로 스크린리더에 전체 값을 노출(시각 텍스트와 스크린리더가 읽는 텍스트가 달라지는 것이므로 남용하지 않는다)
|
||||
|
||||
전체 값에 도달할 수단이 하나도 없는 절단은 통과시키지 않는다. 검증은 실제 콘텐츠 중 가장 긴 값으로 렌더한 뒤, 마우스 없이 키보드만으로 전체 값에 닿을 수 있는지 확인하는 것이다 [SKILL-BETTER-TYPE].
|
||||
|
||||
---
|
||||
|
||||
## 14. 렌더링과 선택 디테일
|
||||
|
||||
### 14-1. 폰트 스무딩은 루트에서 한 번
|
||||
|
||||
```css
|
||||
:root {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
```
|
||||
|
||||
**관찰 후보.** macOS에서 글자를 얇게 렌더하는 이 두 속성은 쓸지 말지부터 실제 렌더로 정한다(가는 서체·작은 크기에서는 획이 약해질 수 있다). 쓰기로 했다면 **루트에 한 번만** 선언한다. 컴포넌트마다 반복해서 적용하면 스무딩 방식이 요소 경계에서 갈라져 보이는 자리가 생긴다 [SKILL-BETTER-TYPE].
|
||||
|
||||
### 14-2. 시스템 폰트 폴백 스택 예시
|
||||
|
||||
브랜드 서체를 아직 못 구했거나 로딩 실패에 대비할 때, 또는 브리프가 "네이티브 느낌"을 요구할 때 쓸 수 있는 시작 스택이다 [SKILL-APPLE-DESIGN]. **기본값으로 강요하지 않는다** — 브랜드 서체가 정해졌으면 그 폴백 체인(§4)을 우선한다.
|
||||
|
||||
```css
|
||||
font-family:
|
||||
system-ui, -apple-system, "Segoe UI",
|
||||
Roboto, "Helvetica Neue", Arial,
|
||||
"Apple SD Gothic Neo", "Malgun Gothic",
|
||||
sans-serif;
|
||||
```
|
||||
|
||||
한글이 섞이는 프로젝트는 §6-3의 순서 규칙(라틴 폰트를 먼저, 한글 폰트를 뒤에)이 시스템 스택에도 그대로 적용된다.
|
||||
|
||||
### 14-3. 텍스트 선택 가능성 유지
|
||||
|
||||
텍스트는 기본적으로 선택 가능해야 한다. `::selection`으로 선택 영역의 배경·글자색을 브랜드에 맞게 바꾸는 것은 괜찮다 — 단, 그 조합으로도 선택된 글자가 읽혀야 한다(대비를 확인한다) [SKILL-BETTER-TYPE].
|
||||
|
||||
```css
|
||||
::selection {
|
||||
background: var(--accent);
|
||||
color: var(--accent-ink);
|
||||
}
|
||||
```
|
||||
|
||||
**`user-select: none`은 드래그·제스처 표면에서만 쓴다** — 정렬 핸들, 캔버스 위 라벨, 스와이프 카드처럼 선택이 제스처와 충돌하는 자리다. 인터페이스 전역에 걸거나, 버튼 라벨이 드래그 중 하이라이트될 가능성만으로 걸지 않는다. 본문·카드 설명·에러 메시지처럼 사용자가 복사하고 싶어할 수 있는 텍스트에 `user-select: none`이 걸려 있으면 그 자체가 완성도 미달 신호다.
|
||||
|
||||
### 14-4. 장식적 텍스트 기법 — 관찰 후보
|
||||
|
||||
**언제**: 에디토리얼 인트로, 인용구, 잡지형 섹션 오프닝처럼 타이포그래피가 §9-2의 능동적 요소로 쓰이는 자리.
|
||||
|
||||
| 기법 | 용도 |
|
||||
|---|---|
|
||||
| `::first-letter` | 드롭캡 |
|
||||
| `::first-line` | 첫 줄만 다른 스타일(작은 대문자 등) |
|
||||
| `-webkit-text-stroke` | 텍스트 외곽선. 가변 폰트에서는 겹치는 글자 모양이 병합되지 않은 채 보일 수 있다 — 정적 폰트로 확인한다 |
|
||||
| `text-shadow` | 텍스트 그림자. [elevation.md](elevation.md)의 그림자 원칙(광원 하나로 통일)과 같은 기준을 따른다 |
|
||||
|
||||
본문·UI 텍스트에는 쓰지 않는다. `background-clip: text`(그라디언트 텍스트)는 이미 `antipatterns.md`가 다룬다 [SKILL-BETTER-TYPE].
|
||||
|
||||
---
|
||||
|
||||
## 15. 타이포 마감 전 빠른 점검
|
||||
|
||||
§8의 배포 전 체크리스트가 로딩·라이선스를 다룬다면, 이 표는 **렌더된 실수**를 잡는다. 실제 콘텐츠로 채운 화면에서 확인한다.
|
||||
|
||||
| 발견 | 조치 |
|
||||
|---|---|
|
||||
| 합성된 굵기·기울임이 디자인과 다르게 보인다 | 실제 페이스를 로드한다. 검증된 모드만 `font-synthesis-*: none`(§10-1) |
|
||||
| Display 전용 파일을 작은 본문 크기에 그대로 쓴다 | 크기에 맞는 Text/Display 변형을 고른다(§9-3) |
|
||||
| 자식 헤딩이 부모보다 시각적으로 강하다 | 해당 섹션의 위계를 스케일 내림차순에 다시 매핑한다(§9-4) |
|
||||
| 헤딩 요소를 시각 크기만 보고 골랐다 | 시맨틱을 먼저 고르고([accessibility.md](accessibility.md)) 시각 크기는 CSS로 정한다(§9-4) |
|
||||
| 문단 마지막 줄에 단어 하나(위도우) | `text-wrap: pretty`(§11-1, 지원 상태 확인) 또는 수동 개행 |
|
||||
| 두 줄 헤딩이 한쪽으로 쏠린다 | `text-wrap: balance`(§11-1) |
|
||||
| 인터페이스에 justify 정렬이 쓰인다 | `text-align: start`로. justify는 에디토리얼 컬럼에만(§11-4) |
|
||||
| 밑줄이 하강부를 가로지른다 | `text-decoration-skip-ink: auto`, `from-font` 메트릭(§12-1) |
|
||||
| 밑줄 두께·오프셋을 트랜지션에 걸어 애니메이션이 브라우저마다 다르게 움직인다 | `color`만 트랜지션하거나 별도 요소로 구축한다(§12-1) |
|
||||
| 절단된 텍스트에 전체 값 도달 수단이 없다 | §13 — **하드 게이트**, 배포 차단 |
|
||||
| 인터페이스 전역에서 텍스트 선택이 막혀 있다 | 복원하고, 드래그·제스처와 충돌하는 자리에만 남긴다(§14-3) |
|
||||
| 갱신되는 숫자(타이머·카운터·실시간 가격)의 폭이 매번 바뀐다 | `tabular-nums`(§10-3) |
|
||||
|
||||
> 근거: [SKILL-BETTER-TYPE], [SKILL-FRONTEND-DESIGN], [SKILL-DESIGN-REVIEW], [SKILL-APPLE-DESIGN], [WEB-BASELINE](확인 2026-09-24)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue