# Motion 상태가 바뀌는 과정을 보여주는 지속시간과 가속 곡선입니다. > 원본: `03_ASSET_GUIDE/foundations/motion.js` · 값: `03_ASSET_GUIDE/styles/tokens.css` (`--- Motion ---` 절) > status `stable` · specSource `measured` | 단계 | 역할 | 예시 | |---|---|---| | **Primitive** | 지속시간 3단계 `--atom-duration-*` + 곡선 1개 `--atom-ease-out` | `--atom-duration-base` = `200ms` | | **Semantic** | 없음 — 무엇을 움직이는가에 따른 짝짓기(아래 2절)로 대신합니다 | `transform` → `base` + `--atom-ease-out` | > **원칙:** `transform` 과 `opacity` 만 애니메이션합니다. 모션은 관계를 설명할 때만 쓰고, 모션 없이도 동작해야 합니다. --- ## 1. Primitive tokens ### Duration 움직이는 거리가 멀고 요소가 클수록 오래 걸립니다. 라이트/다크에서 값이 같습니다. | Token | Value | 성격 | 대상 | 저장소 내 사용 횟수 | |---|---|---|---|---| | `--atom-duration-fast` | 120ms | 마이크로 | 색·배경·투명도·그림자. 제자리에서 바뀌는 것 | 28 | | `--atom-duration-base` | 200ms | 매크로 경계 | 위치가 바뀌는 것. 사이드바 슬라이드, 셰브런 회전 | 6 | | `--atom-duration-slow` | 600ms | 매크로 | 화면 폭만큼 움직이는 것 | 1 | **0.2초가 마이크로와 매크로의 경계**입니다. 제자리에서 색만 바뀌는 것은 그보다 짧게 두고, 위치가 이동하는 것은 이동 경로가 보이도록 그보다 길게 둡니다. `--atom-duration-slow`(600ms) 는 저장소에서 **한 곳**(`chrome.css:418` 의 검색 다이얼로그 패널 진입)에서만 쓰입니다. 화면 중앙에 큰 패널이 나타나는 동작이라 base 로는 너무 빠릅니다. ### Easing | Token | Value | 저장소 내 사용 횟수 | |---|---|---| | `--atom-ease-out` | `cubic-bezier(0.16, 1, 0.3, 1)` | 5 | 첫 제어점의 x 가 `0.16` 으로 0 에 가까워 **즉시 출발**하고, 두 번째 제어점의 y 가 `1` 이라 **끝에서 강하게 감속**합니다. 조작 직후 바로 움직이기 시작해 부드럽게 멈추므로 조작에 대한 응답에 씁니다. > [!WARNING] > **등장/퇴장 곡선이 나뉘어 있지 않습니다** > 나타나는 동작(ease-out)과 사라지는 동작(ease-in)은 반대 곡선을 써야 자연스럽습니다. > 사라질 때는 천천히 시작해 가속하며 화면을 벗어나는 편이 맞습니다. > `--atom-ease-in` 과 `--atom-ease-in-out` 은 아직 없고, 퇴장에도 `--atom-ease-out` 이나 CSS 기본 `ease` 를 쓰고 있습니다. > 사라지는 모션이 중요해지면 그때 추가합니다. `--atom-ease-in` · `--atom-ease-in-out` · `--atom-ease-linear` 는 **정의되어 있지 않습니다.** ### 반복 애니메이션 `@keyframes` 는 두 개뿐이고, 둘 다 무한 반복입니다. | 이름 | 위치 | 값 | 용도 | |---|---|---|---| | `atom-button-spin` | `elements/button.css:181` | `700ms linear infinite` | 버튼 로딩 스피너 | | `atom-text-input-blink` | `elements/text-input.css:100` | `1.1s steps(1) infinite` | 커서 깜빡임 | 둘 다 duration 이 토큰이 아니라 직접 쓴 값입니다. 반복 애니메이션의 주기는 전환 지속시간과 성격이 다르므로 (하나는 "얼마나 걸리는가", 하나는 "얼마나 자주 도는가") 같은 스케일에 넣지 않았습니다. 다만 스피너가 여러 곳에 생기면 `--atom-duration-spin` 같은 토큰이 필요해집니다. --- ## 2. Semantic tokens ATOM 은 모션에 역할 토큰을 두지 않습니다. 대신 **무엇을 움직이는가**로 지속시간과 곡선을 짝짓습니다. | 속성 | Duration | Easing | 예시 | |---|---|---|---| | `color` | `fast` | `ease` | 링크 hover | | `background-color` | `fast` | `ease` | 사이드바 항목 hover | | `opacity` | `fast` | `ease` | 카드 오버레이 | | `box-shadow` | `fast` | `ease` | 포커스 링 | | `transform` | `base` | `--atom-ease-out` | 셰브런 회전, 사이드바 슬라이드 | | `transform` | `slow` | `--atom-ease-out` | 검색 다이얼로그 진입 | > [!NOTE] > `fast` 를 쓰는 전환들은 easing 에 토큰이 아니라 CSS 기본 `ease` 를 씁니다. > 120ms 처럼 짧은 구간에서는 곡선 차이가 지각되지 않기 때문에 문제가 되지 않지만, > 일관성 관점에서는 `--atom-ease-out` 으로 통일하는 편이 낫습니다. ### 규칙 1. **`transform` 과 `opacity` 만** — 이 둘은 레이아웃을 다시 계산하지 않으므로 저사양 기기에서도 프레임이 떨어지지 않습니다. `width` · `height` · `top` · `margin` 은 애니메이션하지 않습니다. 2. **크기와 거리에 비례하게** — 제자리 변화는 `fast`, 이동은 `base`, 큰 패널은 `slow`. 3. **상태는 DOM 속성으로** — 열린 상태는 `[data-open="true"]` 로 표시하고 모션은 그 위에 얹습니다. --- ## 3. Design tokens (JSON) [W3C Design Tokens (DTCG)](https://tr.designtokens.org/format/) 형식입니다. 경로는 `--atom-duration-*` → `duration.*`, `--atom-ease-out` → `easing.out` 입니다. ```json { "duration": { "$type": "duration", "fast": { "$value": "120ms" }, "base": { "$value": "200ms" }, "slow": { "$value": "600ms" } }, "easing": { "$type": "cubicBezier", "out": { "$value": [0.16, 1, 0.3, 1] } } } ``` --- ## 4. CSS 적용 ```css /* 제자리 변화 — 짧게 */ .atom-nav-link { transition: color var(--atom-duration-fast) ease; } /* 이동 — transform 으로, ease-out 곡선 */ .atom-layout__sidebar { transform: translateX(-100%); transition: transform var(--atom-duration-base) var(--atom-ease-out); } .atom-layout__sidebar[data-open="true"] { transform: translateX(0); } ``` ### `prefers-reduced-motion` `styles/base.css` 최상단에서 **전역으로** 처리하므로, 새로 만든 컴포넌트를 포함해 모든 컴포넌트에 적용됩니다. ```css @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; } } ``` `0` 이 아니라 `0.01ms` 인 이유는 `transitionend` · `animationend` 이벤트가 계속 발생하도록 하기 위해서입니다. `0` 으로 두면 그 이벤트에 의존하는 스크립트가 멈춥니다. `animation-iteration-count: 1` 은 스피너 같은 무한 반복을 한 바퀴에서 끝냅니다. > [!NOTE] > 이 전역 처리는 참고한 네 개 디자인 시스템(seed · Spectrum · Ant · Codeit) 중 어느 쪽도 문서화하지 않은 항목입니다. > ATOM 은 코드로는 이미 대응하고 있으므로 이 문서가 그 규칙을 명문화합니다. --- ## 5. 사용 가이드 ### 모션은 관계를 설명할 때만 씁니다 모션은 요소가 어디서 나타나 어디로 사라지는지 알려줄 때 씁니다. 그런 설명이 필요 없으면 모션을 넣지 않습니다. - ✅ **Do** 시트가 아래에서 올라오게 해 "닫으면 아래로 간다" 를 알려줍니다. - ❌ **Don't** 페이지 로드마다 콘텐츠를 페이드인시키지 않습니다. ### 지속 시간은 짧게 둡니다 모션이 진행되는 동안 사용자는 기다려야 합니다. 자주 보는 화면에서는 긴 모션이 지연으로 느껴집니다. - ✅ **Do** 목적을 달성하는 가장 짧은 시간을 고릅니다. - ❌ **Don't** 마이크로 인터랙션에 300ms 이상을 쓰지 않습니다. ### 크기와 거리에 비례하게 같은 200ms 라도 8px 움직이는 셰브런은 순식간이고, 화면 폭만큼 움직이는 사이드바는 급합니다. 움직이는 거리가 멀수록 단계를 올립니다. - ✅ **Do** 제자리 변화는 `fast`, 이동은 `base`, 큰 패널은 `slow`. - ❌ **Don't** 모든 전환에 같은 duration 을 쓰지 않습니다. ### `transform` 과 `opacity` 만 애니메이션합니다 `width` 나 `top` 을 애니메이션하면 매 프레임 레이아웃이 다시 계산됩니다. 같은 결과를 `transform: translateX()` 나 `scale()` 로 만들 수 있습니다. - ✅ **Do** `transform: translateX(-100%)` 로 사이드바를 숨깁니다. - ❌ **Don't** `left: -240px` 를 애니메이션하지 않습니다. ### 모션 없이도 동작해야 합니다 모션은 보조 수단입니다. 애니메이션이 꺼져도 열림/닫힘이 구분되어야 합니다. - ✅ **Do** 열린 상태는 `[data-open="true"]` 로 표시하고 모션은 그 위에 얹습니다. - ❌ **Don't** 애니메이션 완료 콜백에서만 상태를 바꾸지 않습니다. ### 접근성 - **전정 장애(vestibular disorder)** — 큰 화면 이동이나 확대·축소는 어지럼증을 일으킬 수 있습니다. `prefers-reduced-motion` 전역 대응이 이 문제를 덮고 있지만, 자동 재생되는 캐러셀이나 패럴랙스처럼 사용자 조작 없이 움직이는 것은 애초에 만들지 않습니다. - **깜빡임** — 초당 3회를 넘는 깜빡임은 광과민성 발작을 유발할 수 있습니다. `atom-text-input-blink` 는 1.1초 주기(약 0.9Hz)로 이 기준 안에 있습니다. - **포커스 이동** — 모션이 끝나기 전에 포커스를 옮기면 스크린리더가 아직 화면 밖에 있는 요소를 읽습니다. 열림 애니메이션이 있는 다이얼로그는 애니메이션 완료 후 포커스를 옮깁니다. - **모션이 상태를 대신하지 않습니다** — `prefers-reduced-motion: reduce` 에서 전환이 사실상 즉시 끝나므로, 애니메이션 유무와 무관하게 상태가 DOM 속성으로 드러나야 합니다.