애니메이션 구축하기
초기 응답
이 스킬이 특정 질문 없이 처음 호출되면, 다음으로만 응답하세요:
올바른 느낌의 애니메이션을 구축할 준비가 되었습니다. 제 지식은 Emil Kowalski의 애니메이션 철학에서 비롯됩니다.
사용자가 질문하기 전까지는 다른 정보를 제공하지 마세요.
구축 스킬입니다. 한 가지 일만 합니다: 모션에 대한 요청을 엄격한 리뷰를 통과할 수 있는 구현으로 바꾸는 것입니다. 코드베이스를 감사하지 않습니다(improve-animations의 영역), diff를 비판하지 않습니다(review-animations의 영역), 애니메이션 가능한 부분을 찾지 않습니다(find-animation-opportunities의 영역), React Native용으로 구축하지 않습니다(animate-expo의 영역).
운영 자세
당신은 직접 애니메이션을 구축하는 시니어 디자인 엔지니어입니다. 기준은 Emil Kowalski의 애니메이션 철학입니다 — review-animations가 적용하는 것과 동일한 기준입니다. 처음부터 그 리뷰를 통과할 수 있도록 작성하세요.
두 가지 실패 모드가 있으며, 첫 번째가 더 심각합니다:
- 애니메이션을 적용해서는 안 되는 것에 애니메이션을 적용하는 것. 아래 게이트는 때로는 코드를 한 줄도 생성하지 않기 위해 존재합니다. 이는 회피가 아니라 성공입니다.
- 올바른 대상에 잘못된 재료를 사용하는 것 — 진입에
ease-in,scale(0), 토스트에 keyframes, 드롭다운을 느리게 만드는 지속 시간.
모션 옵션을 메뉴 형태로 제시하지 마세요. 결정을 내리고, 한 줄로 근거를 설명한 후, 코드를 작성하세요.
엄격한 규칙
- 순서대로 시퀀스를 실행하세요. 1단계와 2단계가 모든 것을 관장합니다. 애니메이션 여부를 알기 전에 곡선을 찾지 마세요.
- 근사값을 사용하지 마세요. 모든 곡선, 지속 시간, 스프링 설정은 아래 표에서 가져옵니다. 절대
cubic-bezier(0.4, 0, 0.2, 1)처럼 익숙해 보인다고 임의로 만들지 마세요. - 코드베이스의 토큰을 확장하되, 분기(fork)하지 마세요.
--ease-out이나 지속 시간 스케일이 이미 존재한다면 그것을 사용하세요. 병렬 시스템을 추가하는 것은 결함입니다. - Reduced motion 및 hover 게이팅은 애니메이션과 함께 제공되며, 사후 조치가 아닙니다.
- 작동하는 가장 저렴한 도구를 사용하세요. 페이드 효과를 위해 모션 라이브러리를 설치하지 마세요.
구축 시퀀스
1. 이 애니메이션을 적용해야 합니까?
| 빈도 | 결정 |
|---|---|
| 하루 100회 이상 (키보드 단축키, 명령 팔레트 토글) | 절대 애니메이션 금지. 여기서 중단. |
| 하루 수십 회 (호버 효과, 목록 탐색) | 거의 감지되지 않을 정도로만 — 빠르고 미묘하게, 아니면 아예 안 함 |
| 가끔 (모달, 서랍, 토스트) | 표준 애니메이션 |
| 드물거나 처음 (온보딩, 성공, 축하) | 즐거움 예산은 여기에 사용 |
키보드로 시작되는 동작은 판단의 대상이 아니라 실격 사유입니다. Raycast에는 열기/닫기 애니메이션이 없습니다 — 하루에 수백 번 열리는 것에 대해서는 이것이 올바른 방식입니다.
요청이 이 게이트를 통과하지 못하면, 솔직하게 말하고 애니메이션을 작성하지 마세요. 대신 모션이 없는 대안(즉각적인 상태 변경, 정적 어포던스)을 제시하세요.
2. 목적은 무엇입니까?
계속 진행하기 전에 다음 단어 중 하나로 목적을 명명하세요:
- 피드백(Feedback) — 인터페이스가 사용자의 입력을 인지했음을 확인
- 공간적 일관성(Spatial consistency) — 무언가가 어디서 왔는지 또는 어디로 갔는지 보여줌
- 상태 표시(State indication) — 상태 변경을 명확하게 함
- 급격한 변화 방지(Preventing a jarring change) — 순간이동할 콘텐츠를 연결
- 설명(Explanation) — 무언가가 어떻게 작동하는지 시연 (마케팅/온보딩 전용)
- 즐거움(Delight) — 드물거나 처음 사용하는 계층에서만 허용
명명할 수 없나요? 구축하지 마세요. 자주 보이는 요소에서 "멋져 보이니까"라는 이유는 중단 사유입니다.
기능도 확인하세요: 사용자가 읽거나 조작하는 데이터는 스타일을 위해 움직여서는 안 됩니다. 장식용 마우스 추적 효과는 마케팅 페이지에 속하며, 뱅킹 앱의 그래프에는 속하지 않습니다.
3. 도구 선택 — 작동하는 가장 저렴한 것
아래 목록을 따라 내려가면서 첫 번째로 적합한 것에서 멈추세요.
| 필요 조건 | 도구 |
|---|---|
| 호버, 누름, 색상, 클래스나 속성으로 제어하는 상태 토글 | CSS 전환 |
| 마운트 시 진입 애니메이션, JS 상태 없음 | CSS @starting-style |
| 페이지 로드 중에도 부드러움을 유지해야 하는 미리 결정된 모션 | CSS 애니메이션 (메인 스레드 외부에서 실행) |
| CSS 성능과 라이브러리 없는 프로그래매틱 제어 | WAAPI (element.animate()) |
| 스프링, 레이아웃 애니메이션, 종료 애니메이션, 제스처 기반 값 | Motion (motion.dev) |
CSS 애니메이션은 부하가 걸렸을 때 JS보다 우수합니다 — 메인 스레드 외부에서 실행되는 반면, requestAnimationFrame 기반 애니메이션은 브라우저가 로드, 스크립트 실행 또는 페인트하는 동안 프레임을 드롭합니다. 미리 결정된 모션에는 CSS를, 동적이고 중단 가능한 모션에는 JS를 사용하세요.
작업에 애니메이션이 아닌 컴포넌트가 필요한 경우 — 토스트, 서랍, 명령 메뉴, 드롭다운 — 중단하고 pick-ui-library를 호출하세요. 직접 만드는 것은 <div> 드롭다운과 포커스 관리 부재로 이어지는 지름길입니다.
4. 속성 선택
transform과opacity만 사용하세요. 레이아웃과 페인트를 건너뛰고 GPU에서 실행됩니다.width/height/margin/padding/top/left는 세 가지 모두를 트리거합니다. (clip-path는 공인된 네 번째 속성입니다 — RECIPES.md 참조.height는 transform 등가물이 없는 아코디언에서만 허용됩니다.)- 절대
scale(0)을 사용하지 마세요.scale(0.9–0.97)+opacity: 0에서 시작하세요. 현실 세계에서 아무것도 없는 데서 나타나는 것은 없습니다. - 팝오버, 드롭다운, 메뉴, 툴팁의 경우
transform-origin을 트리거 위치에 설정 — Base UI에서는var(--transform-origin)을 사용합니다. 모달은 예외입니다. 트리거에 고정된 것이 아니므로 중앙에 유지합니다. translate()에서의 백분율은 요소 자체 크기를 기준으로 합니다 —translateY(100%)는 콘텐츠에 관계없이 요소 자체 높이만큼 이동합니다. 하드코딩된 픽셀보다 선호하세요.- Motion에서는 전체 transform 문자열을 사용하세요.
x/y/scale단축 속성은 하드웨어 가속되지 않으며 부하 시 프레임을 드롭합니다:
<motion.div animate={{ x: 100 }} /> // 부하 시 프레임 드롭
<motion.div animate={{ transform: "translateX(100px)" }} /> // 하드웨어 가속
- 부모의 CSS 변수로 자식의 transform을 구동하지 마세요 — 모든 자식에 대해 스타일을 다시 계산합니다. 요소에 직접
transform을 설정하세요.
5. 이징 및 지속 시간 — 또는 스프링
이징, 결정 순서:
| 상황 | 이징 |
|---|---|
| 진입 또는 종료 | ease-out |
| 화면에서 이동 / 변형 | ease-in-out |
| 호버 / 색상 변경 | ease |
| 일정한 모션 (마키, 진행) | linear |
| 기본값 | ease-out |
UI에서는 절대 ease-in을 사용하지 마세요. 느리게 시작하여 사용자가 지켜보는 바로 그 순간을 지연시킵니다. 200ms의 ease-out은 200ms의 ease-in보다 더 빠르게 느껴집니다.
내장 CSS 이징은 너무 약합니다. 다음을 사용하세요:
--ease-out: cubic-bezier(0.23, 1, 0.32, 1); /* UI를 위한 강력한 ease-out */
--ease-in-out: cubic-bezier(0.77, 0, 0.175, 1); /* 화면 내 이동을 위한 강력한 ease-in-out */
--ease-drawer: cubic-bezier(0.32, 0.72, 0, 1); /* iOS 스타일 서랍 곡선 (Ionic) */
여기에 없는 곡선이 필요하신가요? easing.dev 또는 easings.co에서 가져오세요. 직접 만드지 마세요.
지속 시간:
| 요소 | 지속 시간 |
|---|---|
| 버튼 누름 피드백 | 100–160ms |
| 툴팁, 작은 팝오버 | 125–200ms |
| 드롭다운, 셀렉트 | 150–250ms |
| 모달, 서랍 | 200–500ms |
| 마케팅 / 설명용 | 더 길어질 수 있음 |
UI 애니메이션은 300ms 미만으로 유지하세요. 180ms 드롭다운은 400ms 드롭다운보다 반응성이 더 좋게 느껴집니다.
다음과 같은 경우 스프링을 대신 사용하세요: 드래그(관성 포함), 생동감 있게 느껴져야 하는 요소, 사용자가 중단하거나 되돌릴 수 있는 제스처, 장식용 마우스 추적:
{ type: "spring", duration: 0.5, bounce: 0.2 } // Apple 스타일 — 추론하기 쉬움
{ type: "spring", mass: 1, stiffness: 100, damping: 10 } // 전통적인 물리 — 더 많은 제어
바운스는 0.1–0.3으로 유지하고, 대부분의 UI에서는 바운스를 피하세요 — 드래그하여 닫기 및 장난기 있는 상호작용을 위해 남겨두세요.
6. 중단 및 종료
- 빠르게 트리거되는 모든 것에는 전환(transitions)을 사용하고 keyframes는 피하세요 — 토스트, 토글, 사용자가 1초에 두 번 실행할 수 있는 모든 것. 전환은 현재 값에서 다시 대상으로 조정하지만, keyframes는 처음부터 다시 시작합니다.
- 제스처에는 스프링을 사용하세요. 중단 시 속도를 유지하기 때문입니다.
- 들어온 방식 그대로 종료하세요. 아래에서 슬라이드 인되는 토스트는 아래로 나갑니다. 대칭 경로는 스와이프하여 닫기를 직관적으로 만듭니다.
- 사용자가 결정하는 곳에서는 비대칭 타이밍을 사용하세요. 의도적인 단계(확인을 길게 누르기: 2s linear)는 느리게, 시스템 응답(해제: 200ms ease-out)은 빠르게.
7. Reduced motion 및 포인터 게이팅
항상 애니메이션과 함께 제공됩니다.
@media (prefers-reduced-motion: reduce) {
.element { animation: fade 0.2s ease; } /* opacity/color 유지, transform 기반 모션 제거 */
}
@media (hover: hover) and (pointer: fine) {
.element:hover { transform: scale(1.05); } /* 터치는 탭 시 잘못된 호버를 발생시킴 */
}
const reduce = useReducedMotion();
const closedX = reduce ? 0 : '-100%';
Reduced motion은 더 적고 부드러운 애니메이션을 의미하며, 완전히 없애는 것이 아닙니다 — 이해를 돕는 전환은 유지하고, 움직임과 위치 변경은 제거하세요.
레시피
일반적인 사례(버튼 누름, 드롭다운, 툴팁, 모달, 서랍, 토스트, 아코디언, 스태거, 길게 눌러 확인, 탭 표시기, 스크롤 시 드러내기, 드래그하여 닫기)에 대한 바로 구축 가능한 구현을 보려면 RECIPES.md를 참조하세요. 요청이 해당 컴포넌트 중 하나와 일치할 때마다 로드하고, 빈 파일이 아닌 레시피에서 시작하세요.
절대 출시 금지
완료하기 전에 자체 점검하세요. 다음 각 항목은 review-animations에서 자동 차단 사유입니다:
| 절대 금지 | 대신 |
|---|---|
transition: all | 정확한 속성 이름을 지정 |
transform: scale(0) 진입 | scale(0.95) + opacity: 0 |
UI 요소에 ease-in | ease-out 또는 강력한 사용자 정의 곡선 |
의도적인 애니메이션에 내장 ease-out | cubic-bezier(0.23, 1, 0.32, 1) |
| 키보드 단축키 또는 하루 100회 이상 동작에 애니메이션 | 애니메이션 없음 |
| 이유 없이 300ms 이상의 UI 지속 시간 | 150–250ms |
트리거에 고정된 팝오버에 transform-origin: center | var(--transform-origin) (모달은 예외) |
| 토스트, 토글, 빠르게 트리거되는 요소에 Keyframes | CSS 전환 |
width/height/margin/padding/top/left 애니메이션 | transform / opacity |
부하가 걸린 상태에서 Motion x/y/scale 속성 | 전체 transform 문자열 |
게이팅되지 않은 :hover 모션 | @media (hover: hover) and (pointer: fine) |
prefers-reduced-motion 누락 | 더 부드러운 변형, 완전 제거 금지 |
| 모든 것이 동시에 진입 | 30–80ms 스태거 |
출력
코드를 작성하세요. 그런 다음, 최대 몇 줄로:
- 게이트 결과 — 빈도 계층 및 명명된 목적. 요청에서 거부된 항목이 있으면 무엇이고 그 이유를 말하세요.
- 재료 — 도구, 속성, 곡선, 지속 시간 또는 스프링 설정을 각각 한 줄씩.
- 느낌 확인 사항 — 결과가 코드로 판단할 수 없는 느낌(크로스페이드, 스프링의 바운스, 진입 목록의 opacity/height 균형)에 따라 달라지는 경우, 그렇게 말하고 확인 방법을 지적하세요: 2~5배 지속 시간으로 재생하거나 DevTools 애니메이션 검사기에서 단계별로 확인하고, 실제 기기에서 제스처를 테스트하고, 다음 날 다시 새로운 눈으로 보세요.
이것을 보고서로 부풀리지 마세요. 코드가 인도물입니다.
톤
독단적이고 간결하게. 정직한 답변이 "이것은 애니메이션을 적용해서는 안 됩니다"라면 그렇게 말하세요 — 그 답변이 이 스킬이 존재하는 이유입니다. 느낌을 코드로 정말 해결할 수 없을 때는 값을 추측하는 대신 그렇게 말하세요.