# 01. 모션 원칙 — 왜 이 움직임인가

> 이 문서의 목적: 에이전트가 "어떤 이징을, 몇 ms로, 어떤 순서로" 쓸지를 **취향이 아니라 규칙으로** 결정하게 만든다.
> 모션은 장식이 아니라 **인터페이스의 문법**이다. 문법에는 옳고 그름이 있다.

---

## 0. 대전제: 모션은 4가지 일만 한다

애니메이션을 넣기 전에 반드시 아래 4개 중 **어느 것에 해당하는지 한 문장으로** 답할 수 있어야 한다.
답할 수 없으면 그 애니메이션은 넣지 않는다.

| # | 역할 | 질문 | 예시 |
|---|---|---|---|
| 1 | **인과(causality)** | "이게 왜 여기 나타났나?"에 답하는가 | 버튼을 누른 지점에서 시트가 자라남 |
| 2 | **공간 연속성(continuity)** | "이 요소가 어디서 와서 어디로 갔나?"에 답하는가 | 썸네일 → 상세 이미지로 이어지는 shared element |
| 3 | **상태 변화 고지(feedback)** | "내 입력이 접수됐나?"에 답하는가 | 프레스 시 눌림, 토글 스위치, 검증 실패 표시 |
| 4 | **주의 유도(attention)** | "지금 어디를 봐야 하나?"에 답하는가 | 새 항목 하이라이트, 에러 필드로의 스크롤 |

**여기에 없는 다섯 번째, "멋있어서"는 정당한 이유가 아니다.** 단, 브랜드 표현이 명시적 요구사항인
히어로/랜딩/포트폴리오에서는 5번 **연출(expression)** 이 추가로 허용된다. 대신 연출 모션은
반드시 (a) 스크롤/호버로 사용자가 통제 가능하거나 (b) 1회성이며 (c) `prefers-reduced-motion`에서
완전히 제거 가능해야 한다.

---

## 1. 이징의 의미론 — 곡선은 "물리적 인상"이다

이징 곡선은 속도의 시간 변화율이다. 사람의 뇌는 이것을 **무게, 힘, 의도**로 읽는다.

### 1.1 기본 곡선 표

| 이징 | cubic-bezier | 속도 변화 | 심리적 인상 | 언제 쓰나 | 쓰면 안 되는 곳 |
|---|---|---|---|---|---|
| `linear` | `(0,0,1,1)` | 일정 | 기계적, 무생물 | 무한 회전(스피너), 마퀴, **스크롤 스크럽** | 위치 이동, 등장/퇴장 |
| `ease-out` | `(0,0,0.58,1)` | 빠르게 시작 → 감속 | "도착했다", 안정감 | **화면에 들어오는 모든 것**. 진입, 열림, 확장 | 화면 밖으로 나가는 것 |
| `ease-in` | `(0.42,0,1,1)` | 느리게 시작 → 가속 | "떠난다", 이탈 | **화면 밖으로 나가는 것**. 퇴장, 닫힘 | 화면에 남는 것 (끝이 뚝 끊겨 어색함) |
| `ease-in-out` | `(0.42,0,0.58,1)` | 감속-가속 대칭 | 신중함, 왕복 | 화면 안에서의 **이동**(A→B), 루프 애니메이션 | 짧은 마이크로 인터랙션(굼떠 보임) |
| `ease`(기본값) | `(0.25,0.1,0.25,1)` | 짧은 가속 → 긴 감속 | 무난함 | 딱히 고민할 여유가 없을 때의 기본값 | 프로덕션 최종 튜닝 |
| **spring** | (곡선 아님) | 물리 시뮬레이션 | 생동감, 실체감, 손맛 | 직접 조작(드래그·제스처), 토글, 레이아웃 변화 | 색/투명도 변화, 텍스트 |

> **핵심 규칙 (Josh Comeau, Material, Carbon이 모두 동의하는 지점)**
> **들어오는 것은 감속(ease-out), 나가는 것은 가속(ease-in), 안에서 움직이는 것은 감속-가속(ease-in-out).**
> 이유: 화면에 남을 요소가 급정거하면 충돌처럼 보이고, 화면을 떠날 요소가 느리게 사라지면
> "왜 아직 안 갔지"라는 대기 비용이 생긴다.

### 1.2 왜 `linear`는 거의 항상 틀린가

현실의 모든 물체는 질량이 있어 가속·감속한다. 등속 운동은 뇌가 "이건 물체가 아니라 그려진 것"으로
읽는다. 예외는 **애초에 물체가 아닌 것**뿐이다: 진행률 표시, 회전 로더, 무한 마퀴,
그리고 **스크롤에 직접 물린 애니메이션**(사용자의 스크롤 자체가 이미 이징이므로 곡선을 덧씌우면 이중 이징이 된다).

### 1.3 커스텀 cubic-bezier를 만들 때의 안전 규칙

- `x1`, `x2`는 반드시 `0~1` 범위. 벗어나면 무효.
- `y1`, `y2`는 `0~1`을 벗어나도 되며 이때 **오버슛/언더슛**이 생긴다 (예: `cubic-bezier(0.34, 1.56, 0.64, 1)` = back-out).
- 오버슛은 **작고 가벼운 요소**(칩, 아이콘, 토스트)에만. 큰 면적(모달, 시트)이 오버슛하면 멀미를 유발한다.
- 오버슛 폭은 이동 거리의 **5~10%**를 넘기지 않는다.

```css
/* 프로덕션에서 실제로 쓸 만한 커스텀 곡선 세트 */
--ease-out-quad:    cubic-bezier(0.25, 0.46, 0.45, 0.94);
--ease-out-cubic:   cubic-bezier(0.22, 0.61, 0.36, 1);
--ease-out-quart:   cubic-bezier(0.16, 1,    0.3,  1);   /* 가장 범용적인 "고급스러운" 진입 */
--ease-out-expo:    cubic-bezier(0.16, 1,    0.3,  1);
--ease-in-quart:    cubic-bezier(0.5,  0,    0.75, 0);   /* 퇴장 */
--ease-in-out-quart:cubic-bezier(0.76, 0,    0.24, 1);   /* 화면 내 이동 */
--ease-back-out:    cubic-bezier(0.34, 1.56, 0.64, 1);   /* 작은 요소 전용 오버슛 */
--ease-anticipate:  cubic-bezier(0.68, -0.6, 0.32, 1.6); /* 뒤로 당겼다 튀어나감 — 아주 아껴 쓸 것 */
```

---

## 2. 스프링 — 언제 곡선 대신 물리를 쓰나

### 2.1 스프링이 이기는 상황

스프링은 **목표 지점이 도중에 바뀔 수 있는 애니메이션**에서 압도적으로 유리하다.
duration 기반 이징은 목표가 바뀌면 처음부터 다시 시작하거나 속도가 뚝 끊기지만,
스프링은 **현재 속도(velocity)를 이어받아** 자연스럽게 궤도를 수정한다.

| 상황 | 선택 |
|---|---|
| 드래그를 놓았을 때의 관성/스냅 | **스프링** (필수) |
| 커서를 따라가는 요소 | **스프링** 또는 lerp |
| 토글 스위치, 체크박스 | **스프링** (손맛) |
| 레이아웃 변화(FLIP) | **스프링** |
| 모달 진입/퇴장 | 곡선 (예측 가능한 지속시간이 더 중요) |
| 색상·투명도 변화 | **곡선** (스프링의 바운스가 색에는 무의미하고 깜빡임처럼 보임) |
| 스크롤 스크럽 | **둘 다 아님** (linear) |

### 2.2 파라미터의 의미

| 파라미터 | 별칭 | 올리면 | 실무 기본값 |
|---|---|---|---|
| `stiffness` | tension | 더 빠르고 날카롭게 | 170~400 |
| `damping` | friction | 바운스가 줄고 차분해짐 | 20~40 |
| `mass` | — | 무겁고 굼뜨게, 관성 증가 | 1 (거의 안 건드림) |

**감쇠비(ζ) = damping / (2 × √(stiffness × mass))**

- ζ < 1 → 언더댐프(바운스 있음)
- ζ = 1 → 임계 감쇠(바운스 없이 최단 시간에 정착) ← **UI 기본값으로 가장 안전**
- ζ > 1 → 오버댐프(느리게 미끄러짐)

> Motion 라이브러리는 `stiffness/damping` 대신 **`bounce`(0~1) + `visualDuration`** 조합을 권장한다.
> 이게 디자이너 친화적이다: `{ type: "spring", visualDuration: 0.35, bounce: 0.2 }`
> `bounce: 0`이면 임계 감쇠와 동일, `bounce: 0.25`가 Motion 기본값.

### 2.3 CSS에서 스프링 흉내내기 — `linear()` 프리셋 (직접 계산한 값)

CSS에는 스프링이 없지만 `linear()`(Baseline Widely available, 2023-12)로 근사할 수 있다.
아래는 실제 스프링 미분방정식 `x(t) = 1 − e^(−ζω₀t)(cos(ω_d t) + (ζω₀/ω_d)sin(ω_d t))` 를
샘플링해 생성한 값이다. 주석의 `duration`을 **반드시 함께** 써야 의도한 물리가 나온다.

```css
:root {
  /* soft — m=1 k=170 d=26, ζ=0.997, 바운스 없음 / duration: 700ms */
  --spring-soft: linear(
    0, 0.0039 1%, 0.0148 2%, 0.0314 3%, 0.0526 4%, 0.1053 6%, 0.4302 16%,
    0.5189 19%, 0.598 22%, 0.6669 25%, 0.7259 28%, 0.7907 32%, 0.8415 36%,
    0.8971 42%, 0.9474 51%, 1
  );

  /* snappy — m=1 k=300 d=30, ζ=0.866, 거의 바운스 없음 / duration: 505ms */
  --spring-snappy: linear(
    0, 0.0036 1%, 0.0138 2%, 0.0296 3%, 0.05 4%, 0.0743 5%, 0.1317 7%,
    0.4735 17%, 0.5669 20%, 0.65 23%, 0.7221 26%, 0.7831 29%, 0.8337 32%,
    0.8869 36%, 0.9342 41%, 0.9739 48%, 1
  );

  /* bouncy — m=1 k=400 d=20, ζ=0.5, 최대 16% 오버슛 / duration: 635ms */
  --spring-bouncy: linear(
    0, 0.0077 1%, 0.0295 2%, 0.0634 3%, 0.1074 4%, 0.1597 5%, 0.2185 6%,
    0.282 7%, 0.6231 12%, 0.6881 13%, 0.75 14%, 0.8084 15%, 0.8627 16%,
    0.9126 17%, 0.9579 18%, 0.9984 19%, 1.0341 20%, 1.065 21%, 1.0913 22%,
    1.113 23%, 1.1305 24%, 1.1438 25%, 1.1535 26%, 1.1596 27%, 1.1628 29%,
    1.156 31%, 1.1329 34%, 1.0273 44%, 0.9976 48%, 0.9777 53%, 1
  );

  /* gentle — m=1 k=120 d=14, ζ=0.639, 7% 오버슛 / duration: 940ms */
  --spring-gentle: linear(
    0, 0.0051 1%, 0.0195 2%, 0.0419 3%, 0.071 4%, 0.1058 5%, 0.1453 6%,
    0.2342 8%, 0.528 14%, 0.6206 16%, 0.7058 18%, 0.7823 20%, 0.8492 22%,
    0.9062 24%, 0.9534 26%, 0.9915 28%, 1.021 30%, 1.0429 32%, 1.0636 35%,
    1.0725 38%, 1.0673 44%, 1.0014 67%, 1
  );

  /* nobounce — m=1 k=200 d=28.284, ζ=1.0 (임계 감쇠) / duration: 650ms */
  --spring-nobounce: linear(
    0, 0.004 1%, 0.0151 2%, 0.0319 3%, 0.0534 4%, 0.1068 6%, 0.4339 16%,
    0.5227 19%, 0.6017 22%, 0.6703 25%, 0.7291 28%, 0.7934 32%, 0.8438 36%,
    0.8986 42%, 0.9483 51%, 1
  );
}

/* 사용 예 */
.sheet {
  transition: translate 700ms var(--spring-soft);
}
.chip {
  transition: scale 635ms var(--spring-bouncy);
}
```

> **주의**: `linear()`로 오버슛(값 > 1)을 만들려면 애니메이션되는 값 자체가 오버슛을 허용해야 한다.
> `opacity`에 `--spring-bouncy`를 쓰면 1을 넘는 구간이 클램프되어 정체 구간이 생긴다. **transform 계열에만.**

---

## 3. 지속시간 — 숫자로 못 박기

### 3.1 인지 기준선

| 임계값 | 의미 | 근거 |
|---|---|---|
| **~100ms** | 사용자가 "즉각적"이라고 느끼는 한계. 직접 조작의 착각이 유지됨 | NN/g |
| **~1s** | 사고 흐름이 끊기지 않는 한계 | NN/g |
| **>400ms** | UI 애니메이션은 여기서부터 "느리다"고 인식되기 시작 | NN/g |
| **>500ms** | "짜증난다(a real drag)" 구간 진입 | NN/g |
| **200ms** | INP(Interaction to Next Paint) "good" 임계값 | Core Web Vitals |

> NN/g 요약: **대부분의 UI 애니메이션은 100~500ms.** 애니메이션은 "너무 짧아서" 문제가 되는 경우보다
> **"너무 길어서" 문제가 되는 경우가 압도적으로 많다.**

### 3.2 designpaca 표준 지속시간 표 ← **이 표를 기본값으로 쓴다**

| 카테고리 | 지속시간 | 이징 | 예시 |
|---|---|---|---|
| **즉각 피드백** | 60~100ms | `ease-out` | 버튼 프레스, 체크박스, 리플 시작, 아이콘 상태 |
| **호버/포커스** | 120~180ms | `ease-out` (진입) / 240ms `ease` (이탈) | 링크 밑줄, 카드 리프트, 컬러 변화 |
| **작은 컴포넌트 진입** | 180~250ms | `ease-out-quart` | 툴팁, 드롭다운, 토스트, 칩 |
| **작은 컴포넌트 퇴장** | 120~180ms | `ease-in` | 위와 동일 (진입의 **0.6~0.75배**) |
| **중간 표면 진입** | 250~350ms | `ease-out-quart` 또는 spring(nobounce) | 모달, 사이드 시트, 아코디언 |
| **중간 표면 퇴장** | 180~250ms | `ease-in-quart` | 위와 동일 |
| **전체 화면 전환** | 300~450ms | `ease-in-out-quart` | 페이지 전환, 풀스크린 확장 |
| **shared element 전환** | 350~500ms | `ease-in-out-quart` | 썸네일 → 상세 |
| **스크롤 리빌** | 400~700ms | `ease-out-quart` | 섹션 등장 (1회성) |
| **앰비언트 / 루프** | 3~20s | `ease-in-out` 또는 `linear` | 배경 그라디언트 드리프트, 마퀴 |
| **스크롤 스크럽** | (해당 없음) | `linear` / `none` | 스크롤 진행률에 직접 물린 것 |

### 3.3 지속시간 조정 규칙 (표에서 벗어날 때)

1. **거리 비례**: 이동 거리가 2배면 지속시간은 **√2배** 정도만 늘린다 (선형 비례 아님).
   IBM Carbon이 명시: "요소의 이동 거리·크기가 클수록 애니메이션은 길어져야 한다."
   실무 근사: `duration = base × (distance / baseDistance)^0.5`, 상한은 항상 위 표의 최대값.
2. **진입 > 퇴장**: 퇴장은 진입의 **0.6~0.75배**. 나가는 것은 볼 이유가 없다.
3. **면적 비례**: 화면 면적을 많이 차지할수록 길게. 단 500ms 상한.
4. **반복 빈도 반비례**: 사용자가 하루에 100번 보는 애니메이션은 **가장 짧게**. 1번 보는 것은 길어도 된다.
   → 검색 결과 필터링(빈번) 150ms vs 온보딩 일러스트(1회) 800ms.
5. **모바일은 짧게**: 터치는 마우스보다 직접적이라 지연에 더 민감하다. 데스크톱 값의 0.8배.

### 3.4 정립된 시스템의 실제 토큰값 (1차 출처 확인)

#### Material Design 3 (`material-web` 리포지토리 `_md-sys-motion.scss`, v0.192)

```css
/* Easing */
--md-sys-motion-easing-linear:                cubic-bezier(0, 0, 1, 1);
--md-sys-motion-easing-standard:              cubic-bezier(0.2, 0, 0, 1);
--md-sys-motion-easing-standard-accelerate:   cubic-bezier(0.3, 0, 1, 1);
--md-sys-motion-easing-standard-decelerate:   cubic-bezier(0, 0, 0, 1);
--md-sys-motion-easing-emphasized:            cubic-bezier(0.2, 0, 0, 1);
--md-sys-motion-easing-emphasized-accelerate: cubic-bezier(0.3, 0, 0.8, 0.15);
--md-sys-motion-easing-emphasized-decelerate: cubic-bezier(0.05, 0.7, 0.1, 1);
--md-sys-motion-easing-legacy:                cubic-bezier(0.4, 0, 0.2, 1);
--md-sys-motion-easing-legacy-accelerate:     cubic-bezier(0.4, 0, 1, 1);
--md-sys-motion-easing-legacy-decelerate:     cubic-bezier(0, 0, 0.2, 1);

/* Duration */
--md-sys-motion-duration-short1:      50ms;
--md-sys-motion-duration-short2:     100ms;
--md-sys-motion-duration-short3:     150ms;
--md-sys-motion-duration-short4:     200ms;
--md-sys-motion-duration-medium1:    250ms;
--md-sys-motion-duration-medium2:    300ms;
--md-sys-motion-duration-medium3:    350ms;
--md-sys-motion-duration-medium4:    400ms;
--md-sys-motion-duration-long1:      450ms;
--md-sys-motion-duration-long2:      500ms;
--md-sys-motion-duration-long3:      550ms;
--md-sys-motion-duration-long4:      600ms;
--md-sys-motion-duration-extra-long1: 700ms;
--md-sys-motion-duration-extra-long2: 800ms;
--md-sys-motion-duration-extra-long3: 900ms;
--md-sys-motion-duration-extra-long4:1000ms;
```

M3의 의미론:
- **decelerate** = 화면 밖에서 들어와 화면에 멈추는 것(**enter**)
- **accelerate** = 화면에 있던 것이 멈추지 않고 화면 밖으로 나가는 것(**exit**)
- **emphasized** = 사용자의 주의를 끌어야 하는 큰 전환. `standard`보다 초반이 더 느리고 후반이 더 빠른 비대칭 곡선
- **standard** = 화면 안에서 시작하고 끝나는 대부분의 전환
- **legacy** = M2 시절 곡선. 신규 프로젝트에서는 쓰지 않는다

> **주의**: M3 사양의 "emphasized"는 원래 **2개 곡선을 이어붙인 3점 곡선**이라 단일 cubic-bezier로
> 정확히 표현되지 않는다. 위 토큰값은 웹용 근사치다. 정확히 재현하려면 `linear()`를 쓰거나
> JS 애니메이션에서 키프레임 2개로 나눈다.

#### IBM Carbon (`@carbon/motion` v11.50.0 패키지 소스에서 직접 확인)

```css
/* productive — 실무 작업용. 빠르고 방해되지 않음 */
--carbon-ease-standard-productive:  cubic-bezier(0.2, 0, 0.38, 0.9);
--carbon-ease-entrance-productive:  cubic-bezier(0, 0, 0.38, 0.9);
--carbon-ease-exit-productive:      cubic-bezier(0.2, 0, 1, 0.9);

/* expressive — 브랜드/감성 표현용. 크고 눈에 띄는 전환 */
--carbon-ease-standard-expressive:  cubic-bezier(0.4, 0.14, 0.3, 1);
--carbon-ease-entrance-expressive:  cubic-bezier(0, 0, 0.3, 1);
--carbon-ease-exit-expressive:      cubic-bezier(0.4, 0.14, 1, 1);

/* duration — 용도가 토큰 이름에 직접 문서화되어 있다 */
--carbon-duration-fast-01:      70ms;  /* 버튼·토글 등 마이크로 인터랙션. 즉각 반응 */
--carbon-duration-fast-02:     110ms;  /* 작은 UI 요소의 미묘한 진입/퇴장(페이드) */
--carbon-duration-moderate-01: 150ms;  /* 작은 확장, 짧은 거리 이동. 기본 전환 속도 */
--carbon-duration-moderate-02: 240ms;  /* 확장, 시스템 알림, 토스트 */
--carbon-duration-slow-01:     400ms;  /* 큰 확장, 중요 시스템 알림 */
--carbon-duration-slow-02:     700ms;  /* 배경 디밍, 대형 히어로 전환 */
```

Carbon의 핵심 통찰: **하나의 제품 안에 두 개의 모션 인격이 공존한다.**
- 데이터 테이블, 폼, 툴바 = **productive** (짧고 절제됨, 70~240ms)
- 온보딩, 마케팅 섹션, 빈 상태 = **expressive** (400~700ms)
같은 제품에서 이 둘을 섞지 말고, **컴포넌트 성격에 따라 결정**한다.

#### Apple HIG

Apple은 수치 토큰을 공개하지 않고 원칙만 제시한다. 요약:
- 모션은 **사용자의 작업을 돕는 목적**일 때만 쓴다
- **일관성**: 같은 종류의 전환은 항상 같은 모션
- **Reduce Motion** 설정을 반드시 존중한다. 시스템 제공 애니메이션은 자동 대체되지만 커스텀은 직접 처리
- iOS/macOS의 기본 감성은 **스프링**이다 (SwiftUI의 기본 애니메이션이 스프링)
- 화면 전체가 회전·확대·시차 이동하는 효과는 멀미 유발원 1순위

---

## 4. Stagger와 오케스트레이션

### 4.1 stagger 규칙

| 항목 수 | 항목당 지연 | 총 지속시간 상한 |
|---|---|---|
| 3~5개 (네비게이션) | 40~60ms | 300ms |
| 6~12개 (카드 그리드) | 30~50ms | 500ms |
| 12~30개 (리스트) | 15~25ms | 600ms |
| 30개 초과 | **stagger 쓰지 않음** — 첫 화면 항목만 stagger, 나머지는 동시 | — |

**절대 규칙**
1. `총 지속시간 = (항목수 − 1) × stagger + 개별 duration` 이 **800ms를 넘으면 안 된다.**
   100개 리스트에 20ms stagger를 걸면 2초가 걸린다. 그건 기능이 아니라 버그다.
2. stagger는 **가상 스크롤/무한 스크롤에서는 첫 페이지에만** 적용한다. 스크롤할 때마다 stagger가
   재생되면 읽기를 방해한다.
3. stagger 방향은 **읽기 방향**과 일치시킨다 (좌→우, 상→하). 중앙에서 퍼지는 stagger는
   그리드/방사형 레이아웃에만.

### 4.2 오케스트레이션 — 순서의 문법

Disney 12원칙 중 UI에 실제로 유효한 4가지:

| 원칙 | UI 적용 |
|---|---|
| **Slow in / Slow out** (이징) | 위 1장 전체 |
| **Timing** | 위 3장 전체 |
| **Follow through / Overlapping action** | 연관 요소들이 **동시에 끝나지 않게** 한다. 카드가 먼저, 그 안의 텍스트가 60ms 뒤에. 이게 "고급스러움"의 정체다 |
| **Anticipation** | 큰 동작 전 반대 방향으로 살짝 당긴다. **아껴 쓸 것** — 지연으로 느껴지면 실패 |

나머지 8개(squash&stretch, straight ahead, arcs, secondary action, exaggeration, solid drawing,
appeal, staging)는 캐릭터 애니메이션용이며 UI에 억지로 적용하면 유치해진다.
**squash & stretch**만 버튼 프레스에 한해 미세하게(scale 0.96~0.98) 유효하다.

### 4.3 진입 순서 규칙

```
컨테이너 → 구조(헤더/구분선) → 콘텐츠(텍스트/이미지) → 인터랙션 요소(버튼)
```

각 단계 사이 40~80ms 오버랩. **동시에 시작해서 동시에 끝나면 평면적으로 보인다.**

퇴장은 **정확히 역순**이고 더 빨라야 한다. 실무에서는 퇴장 시 stagger를 없애고 한꺼번에
페이드아웃하는 것이 더 깔끔한 경우가 많다.

---

## 5. 공간 연속성 — "어디서 와서 어디로 가는가"

### 5.1 원칙

사용자의 머릿속에는 **인터페이스의 공간 모델**이 있다. 모션은 그 모델을 유지시키거나 파괴한다.

- 동일한 객체는 **같은 정체성을 유지**한 채 이동해야 한다 (shared element transition).
- 계층 구조에서 **아래로 들어가면** 확대·전진, **위로 나오면** 축소·후퇴.
- 형제 관계 이동은 **수평 슬라이드** (탭, 캐러셀, 스텝).
- 모달/시트는 **z축 위**에서 온다 (아래에서 올라오거나, 트리거 지점에서 자라남).

### 5.2 트리거 지점 원칙 (인과의 시각화)

**요소는 사용자가 클릭한 곳에서 나와야 한다.** 화면 정중앙에서 페이드인하는 팝오버는
"어디서 왔는지" 정보를 버리는 것이다.

```css
/* 클릭 지점을 transform-origin으로 삼는 최소 구현 */
.popover {
  transform-origin: var(--origin-x, 50%) var(--origin-y, 0%);
  scale: 0.95;
  opacity: 0;
  transition: scale 180ms var(--ease-out-quart), opacity 180ms linear;
}
.popover[data-open] {
  scale: 1;
  opacity: 1;
}
```

```js
// 트리거 버튼의 위치를 팝오버의 origin으로 전달
function openPopover(trigger, popover) {
  const t = trigger.getBoundingClientRect();
  const p = popover.getBoundingClientRect();
  popover.style.setProperty('--origin-x', `${t.left + t.width / 2 - p.left}px`);
  popover.style.setProperty('--origin-y', `${t.top + t.height / 2 - p.top}px`);
  popover.dataset.open = '';
}
```

### 5.3 시선 유도

- 사람의 시선은 **움직이는 것**을 먼저 본다. 따라서 **한 시점에 움직이는 것은 하나**여야 한다.
- 동시에 여러 곳이 움직이면 시선이 분산되고 아무것도 강조되지 않는다.
- 순차 진입(stagger)은 시선을 **경로를 따라 끌고 간다**. 그 경로가 정보 우선순위와 일치해야 한다.
- 에러/알림은 **스크롤 이동 + 짧은 강조**로 유도한다. 화면 밖에 있는 것을 흔들어봐야 아무도 못 본다.

---

## 6. 나쁜 모션의 징후 — 실패 진단 체크리스트

에이전트는 자기가 만든 모션에 대해 아래를 **전부** 확인해야 한다. 하나라도 걸리면 고친다.

### 6.1 즉시 실격 (반드시 수정)

| 징후 | 왜 나쁜가 | 처방 |
|---|---|---|
| 페이지 로드 시 콘텐츠가 애니메이션 때문에 **읽히기까지 지연** | 콘텐츠는 즉시 읽혀야 한다. 애니메이션이 게이트가 되면 안 된다 | 첫 화면(above the fold)은 애니메이션 없이 즉시 표시. 리빌은 스크롤 이후에만 |
| 스크롤 리빌이 **매번 재생**됨 | 위아래로 스크롤할 때마다 깜빡여 읽기를 방해 | `once: true` / `animation-fill-mode: forwards` / IntersectionObserver 후 unobserve |
| 애니메이션 중 **레이아웃 시프트**(CLS) 발생 | 클릭 대상이 움직여 오조작 유발 | transform/opacity만 애니메이션. 공간은 미리 확보 |
| **동시에 3개 이상**의 독립적 애니메이션 | 시선 분산, 인지 부하 | 하나로 통합하거나 순차화 |
| 500ms 초과 **차단성** 애니메이션 | 사용자가 기다림 | 400ms 이하로 줄이거나 인터럽트 가능하게 |
| **인터럽트 불가** — 애니메이션 중 클릭 무시 | 통제권 상실 | 애니메이션 중에도 입력 수용, 새 목표로 재타겟 |
| `prefers-reduced-motion` 미대응 | 접근성 위반, 실제 신체 반응 유발 | 05번 문서 참조 |
| 무한 반복되는 **큰 면적** 움직임 | 전정기관 자극 + 배터리 소모 | 정지 가능하게 하거나 제거 |
| 스크롤 재킹으로 **네이티브 스크롤 파괴** | 키보드/스크린리더/스크롤바 붕괴 | 03번 문서 Lenis 섹션 참조 |

### 6.2 냄새 (재검토 필요)

| 징후 | 진단 질문 |
|---|---|
| 이징이 전부 `ease` 또는 `linear` | 진입/퇴장/이동을 구분했는가? |
| 모든 애니메이션이 같은 duration | 크기·거리·빈도가 다른데 왜 같은가? |
| 진입과 퇴장의 duration이 같음 | 퇴장을 0.6~0.75배로 줄였는가? |
| `will-change`가 10개 이상 요소에 상시 적용 | GPU 메모리 낭비. 필요할 때만 켜고 끄는가? |
| 로딩 스켈레톤이 2초 이상 유지 | 스켈레톤은 지연을 가리는 반창고. 실제 지연을 줄였는가? |
| 애니메이션 후에도 포커스가 원래 자리 | 포커스가 시각적 흐름을 따라가는가? |
| 텍스트가 글자 단위로 쪼개짐 | 스크린리더가 "ㅇ-ㅏ-ㄴ-ㄴ-ㅕ-ㅇ"으로 읽는다. aria 처리했는가? |
| hover에만 존재하는 정보 | 터치 기기와 키보드에서는? |

### 6.3 "애니메이션을 넣지 말아야 할" 상황

- **고빈도 반복 작업 흐름**: 데이터 입력, 표 편집, 코드 에디터. 여기서 애니메이션은 순수 비용이다.
- **에러 복구 경로**: 사용자가 이미 짜증난 상태. 지연을 추가하지 않는다.
- **결과가 이미 예측되는 전환**: 탭 전환처럼 "당연히 이렇게 될 것"인 곳에 화려한 모션은 낭비.
- **오프라인/저사양 환경**: 프레임 드랍된 애니메이션은 없느니만 못하다.
- **정보 밀도가 높은 대시보드**: 숫자가 계속 바뀌는 곳에서 카운트업 애니메이션은 읽기를 방해한다.

---

## 7. 결정 트리 — 에이전트용 실행 규칙

```
[1] 이 모션의 역할이 1~4(인과/연속성/피드백/주의) 중 하나인가?
    NO → 만들지 않는다. (단 브랜드 표현이 명시 요구사항이면 5번으로)
    YES ↓

[2] 사용자의 직접 조작(드래그/제스처)에 물려 있거나, 목표가 도중에 바뀔 수 있는가?
    YES → spring. bounce는 0~0.25. visualDuration은 아래 [4]의 값.
    NO  ↓

[3] 스크롤 진행률에 직접 물려 있는가?
    YES → linear / ease "none". duration 개념 없음. CSS scroll-driven 우선(02번), 불가하면 ScrollTrigger scrub(03번).
    NO  ↓

[4] 방향은?
    화면 안으로 들어옴  → ease-out 계열 (--ease-out-quart)
    화면 밖으로 나감    → ease-in 계열  (--ease-in-quart), duration은 진입의 0.65배
    화면 안에서 이동    → ease-in-out 계열 (--ease-in-out-quart)

[5] 지속시간은 3.2 표에서 카테고리를 골라 가져온다.
    거리가 표준의 2배 이상이면 ×1.4, 표의 상한을 넘지 않는다.

[6] 여러 요소인가?
    YES → 4.1 stagger 표. 총 800ms 상한 검증.
    NO  ↓

[7] 성능 검증: transform/opacity/filter/clip-path 만 애니메이션하는가?
    NO → 05번 문서의 대체 기법 적용.

[8] prefers-reduced-motion 대응이 있는가?
    NO → 05번 문서 4장의 3단계 대응 적용. (전부 끄기는 오답)

[9] 구현 스택 선택: CSS로 되나?
    YES → 02번 문서. (기본값. 라이브러리는 필요할 때만)
    NO  → 03번 문서의 선택 기준표.
```

---

## 8. 복사해서 바로 쓰는 모션 토큰 세트

```css
:root {
  /* ── Duration ─────────────────────────────────────────── */
  --dur-instant:  80ms;   /* 프레스, 체크, 리플 시작 */
  --dur-fast:    150ms;   /* 호버, 컬러, 아이콘 상태 */
  --dur-quick:   220ms;   /* 툴팁, 드롭다운, 토스트 진입 */
  --dur-base:    300ms;   /* 모달, 시트, 아코디언 진입 */
  --dur-slow:    420ms;   /* 페이지 전환, shared element */
  --dur-reveal:  600ms;   /* 스크롤 리빌 (1회성) */
  --dur-ambient:  12s;    /* 배경 루프 */

  /* 퇴장은 진입의 0.65배 */
  --dur-quick-out: 145ms;
  --dur-base-out:  195ms;
  --dur-slow-out:  275ms;

  /* ── Easing ───────────────────────────────────────────── */
  --ease-in:        cubic-bezier(0.5,  0,    0.75, 0);
  --ease-out:       cubic-bezier(0.16, 1,    0.3,  1);
  --ease-in-out:    cubic-bezier(0.76, 0,    0.24, 1);
  --ease-back-out:  cubic-bezier(0.34, 1.56, 0.64, 1);
  --ease-linear:    linear;

  /* Material 3 호환 별칭 */
  --ease-emphasized:            cubic-bezier(0.2,  0,   0,    1);
  --ease-emphasized-decelerate: cubic-bezier(0.05, 0.7, 0.1,  1);
  --ease-emphasized-accelerate: cubic-bezier(0.3,  0,   0.8,  0.15);

  /* ── Stagger ──────────────────────────────────────────── */
  --stagger-tight:  25ms;
  --stagger-base:   40ms;
  --stagger-loose:  60ms;

  /* ── Distance ─────────────────────────────────────────── */
  --shift-xs:  4px;   /* 미세 피드백 */
  --shift-sm:  8px;   /* 툴팁, 드롭다운 */
  --shift-md: 16px;   /* 카드 리빌 */
  --shift-lg: 32px;   /* 섹션 리빌 */
}

/* reduced-motion에서의 재정의 — "끄기"가 아니라 "줄이기" */
@media (prefers-reduced-motion: reduce) {
  :root {
    --dur-instant: 1ms;
    --dur-fast:    1ms;
    --dur-quick:  100ms;   /* 페이드는 남긴다 */
    --dur-base:   120ms;
    --dur-slow:   120ms;
    --dur-reveal: 120ms;
    --dur-ambient: 0s;

    --dur-quick-out: 80ms;
    --dur-base-out:  80ms;
    --dur-slow-out:  80ms;

    --stagger-tight: 0ms;
    --stagger-base:  0ms;
    --stagger-loose: 0ms;

    --shift-xs: 0px;
    --shift-sm: 0px;
    --shift-md: 0px;
    --shift-lg: 0px;   /* 이동 거리를 0으로 → 페이드만 남음 */

    --ease-out:    ease-out;
    --ease-in:     ease-out;
    --ease-in-out: ease-out;
    --ease-back-out: ease-out;  /* 오버슛 제거 */
  }
}
```

> 이 토큰 세트의 핵심 설계: **`prefers-reduced-motion`에서 거리 토큰을 0으로 만들면
> 모든 슬라이드 애니메이션이 자동으로 크로스페이드가 된다.** 컴포넌트 코드를 한 줄도
> 고치지 않고 전역에서 모션 강도를 낮출 수 있고, `animationend` 이벤트 의존 코드도 깨지지 않는다.
