aster®

UI 애니메이션의 시간과 움직임을 토큰으로 통일하기

마무리 · 2026-09

같은 모양의 화면 셋 아래에 길이가 제각각인 막대가 하나씩 놓여 있고, 그 아래 작은 카드 하나에 길이가 똑같은 짧은 선 세 줄만 들어 있다.

애니메이션 좀 자연스럽게 넣어달라고 했다. 받아보니 모달은 400ms, 드롭다운은 300ms 였다. 토스트는 아예 @keyframes 로 되어 있었다. 하나씩 놓고 보면 다 그럴듯했다. 그런데 앱 안에서 화면을 옮겨 다녀 보니 속도가 제각각이었다.

자연스럽게라는 요청만으로는 속도를 맞추기 어려웠다. 이징과 지속 시간을 누가 어떤 기준으로 정할지부터 합의해야 했다.

curvez 의 motion-standards 스킬은 그 숫자를 어디서 정할지 다룬다.

1. 직접 입력한 이징 값의 불일치

CSS 에서 속도 변화는 cubic-bezier(0.4, 0, 0.2, 1) 처럼 숫자 넷으로 적는다. 이 숫자를 계산해서 쓰는 사람은 거의 없고 어디서 본 것 같으니까 손으로 옮겨 적는다. 다음 화면에는 그것도 어디서 본 것 같다는 이유로 cubic-bezier(0.4, 0, 0.6, 1) 이 들어간다.

SKILL.md 는 이 상황을 한 줄로 적어 뒀다.

값을 지어내지 마라. 커브는 사본의 표나 사본이 지목한 출처에서만 가져온다.

왜인지도 바로 붙여 놨다. 익숙해 보이는 cubic-bezier 를 손으로 쓰면 프로젝트마다 미묘하게 다른 곡선이 쌓인다. 그러면 어느 것이 의도된 값인지 판정할 근거가 사라진다.

이게 앞 편과 이어지는 지점이다. 에이전트에게 UI 구현을 맡기기 전 디자인 값 정하기 에서 색·간격·타이포를 값으로 확정했다. 그런데 그 값이 움직일 때 무엇이 맞는지는 비어 있었다. wireframe-spec 은 정지 화면까지만 정한다. 그 뒤는 구현 에이전트가 각자 추측해 왔다.

2. 애니메이션이 필요한 동작 판단하기

1단계에서는 커브를 고르기 전에 애니메이션이 필요한지 판단한다.

animate/SKILL.md 의 빈도 표로 판정한다. 하루 100회 이상 보는 것과 키보드로 시작되는 동작은 애니메이션 없이 끝낸다.

판정 결과는 빈도 등급 하나와 목적 한 단어를 한 줄로 남긴다. 목적을 한 단어로 못 대면 만들지 않는다.

그리고 1단계에는 이런 문장이 붙어 있다.

완료 판정: 만들지 않기로 했으면 여기서 끝이다. 코드 0줄이 정상 결과다.

에이전트에게 "코드 0줄이 정상"이라고 적어 두는 스킬은 흔치 않다. 안 적어 두면 에이전트는 뭐라도 내놓으려고 한다. "애니메이션 넣어줘" 라는 지시를 받았으니 안 넣는 결론을 스스로 내기 어렵다.

규칙 절에 순서를 못 박은 이유도 여기 있다.

빈도 판정 없이 커브부터 고르지 마라. 1단계가 2단계를 막는 관문이다. 이유: 값을 먼저 고르면 그 값을 쓰기 위해 애니메이션을 정당화하게 된다.

3. 이징과 지속 시간의 출처

SKILL.md 본문에는 밀리초가 하나도 없다. 이징 이름도 없다. 대놓고 이렇게 적혀 있다.

이 문서는 값을 갖지 않는다. 값은 벤더 사본에 있고, 상황마다 열 파일이 다르다.

값이 있는 곳은 plugins/curvez/vendor/emilkowalski-skills/ 아래다. Emil Kowalski 가 공개한 skills 저장소를 복사해 우리 저장소에 넣어 둔 것이고 vendor/ 라는 폴더 이름이 그 표시라 벤더 사본이라고 부른다. 복사해 온 시점이 커밋 해시 d23d7f88a2e21c9e4b1418c7abe420f5c1052ba7 로 고정돼 있어서 원본이 그 뒤에 어떻게 바뀌었든 우리가 보는 건 그 시점의 파일이다. 출처와 복사 시점은 vendor/emilkowalski-skills/VENDOR.md 에 적혀 있다.

SKILL.md 에는 어느 파일을 열지 정해 둔 표가 있다. 웹 애니메이션이면 animate/SKILL.md, 리뷰면 review-animations/SKILL.md, 코드베이스 전체 감사면 improve-animations/SKILL.md 가 입구다. 옆 파일에는 조건이 붙는다. 만들려는 컴포넌트가 사본에 있을 때만 animate/RECIPES.md 를 같이 열고 리뷰에는 STANDARDS.md 가, 감사에는 AUDIT.md 와 PLAN-TEMPLATE.md 가 따라붙는다.

값을 이 문서로 옮겨 적는 것도 금지다.

모션 규칙을 이 문서에 옮겨 적지 마라. 표가 필요하면 사본을 열어 읽는다. 이유: 사본이 개정되면 옮겨 적은 값만 낡는다.

사본 자체도 고치지 못한다. 한 줄이라도 고치면 원본과 대조할 수 없고 그때부터는 우리 식으로 따로 관리해야 한다. 원본이 개정돼도 다시 가져올 수 없다. 완료 기준의 마지막 항목이 그걸 못 박는다. git status 로 사본 폴더에 바뀐 파일이 없는지 본다.

프로젝트마다 다른 값을 쓰고 싶을 때는 2단계 규칙을 따른다.

.curvez/design/tokens.md 에 이징·지속 시간 토큰이 이미 있으면 그것이 이긴다. 사본의 값과 달라도 프로젝트 토큰을 쓴다.

앞 편에서 색과 간격을 토큰으로 정했듯이 이징과 지속 시간도 같은 파일에 이름을 갖는다. 없으면 사본의 표에서 가져와 .curvez/design/tokens.md 에 먼저 추가하고 코드에서는 숫자 대신 그 토큰 이름으로 참조한다. 토큰 이름 규칙은 wireframe-spec 이 정본이다. 2단계의 완료 판정도 여기서 나온다. 쓰려는 커브와 지속 시간이 전부 .curvez/design/tokens.md 에 이름을 갖는 것.

사본에 실제로 들어 있는 값은 이런 것들이다. --ease-out: cubic-bezier(0.23, 1, 0.32, 1), --ease-in-out: cubic-bezier(0.77, 0, 0.175, 1), --ease-drawer: cubic-bezier(0.32, 0.72, 0, 1). 이름이 곧 쓰임이라 --ease-out 은 화면에 무언가 나타날 때 쓰고 지속 시간은 요소별로 범위가 따로 정해져 있다.

무엇범위
버튼 누름100–160ms
툴팁125–200ms
드롭다운150–250ms
모달·드로어200–500ms

표 1. 사본이 정해 둔 요소별 지속 시간 범위

UI 애니메이션은 300ms 아래로 둔다.

4. 애니메이션 중단 가능성 확인하기

스킬 설명문은 "이징·지속 시간·중단 가능성을 값으로 정해 구현하고 리뷰한다" 로 시작한다. 셋 중 앞의 둘은 위에서 봤다.

중단 가능성(interruptible)은 이런 상황에서 필요하다. 토스트가 400ms 동안 밀려 들어오는 중에 두 번째 토스트가 뜬다. 사용자가 토글을 켜자마자 다시 끈다. 애니메이션이 아직 돌고 있는데 상태가 또 바뀐 것이다. 돌고 있던 것을 어떻게 처리하느냐가 여기서 달라진다.

transition 은 "이 값이 바뀌면 그만큼 부드럽게 따라가라" 고 적어 두는 것이다. @keyframes 는 "0% 일 때 이 모양, 100% 일 때 이 모양" 이라고 대본을 써 둔다. 그리고 처음부터 재생한다.

한 번만 실행되는 동안에는 둘 다 똑같아 보인다. 중간에 상태가 바뀌면 그때 달라진다. transition 은 지금 화면에 있는 그 자리에서 새 목표값 쪽으로 방향만 튼다. @keyframes 는 대본을 처음부터 다시 튼다. 요소가 시작 위치로 순간 되돌아갔다가 거기서 다시 움직인다. 토글을 빠르게 두 번 누르면 그 되돌아가는 게 그대로 보인다.

그런데 SKILL.md 본문을 다 읽어도 이 이야기가 안 나온다. 중단 이라는 낱말은 프론트매터의 설명문에 딱 한 번 나오고 끝이다.

중단 가능성 설명은 벤더 사본에서 찾았다. 값을 갖지 않기로 한 SKILL.md 의 규칙을 여기서도 지킨 셈이다.

transition 과 @keyframes 는 같은 것을 적는 두 가지 문법으로 읽히기 쉽다. 토스트를 연달아 두 번 띄우면 결과가 달라진다.

다만 이 대목은 motion-standards/SKILL.md 본문에는 없다. 설명문이 약속한 셋 중 하나가 본문의 절차·규칙·완료 기준 어디에도 없다. 사본까지 따라가면 다 있다. 하지만 SKILL.md 만 읽고 작업하는 에이전트는 이걸 놓칠 수 있다.

5. 동작 줄이기와 hover 조건 적용

prefers-reduced-motion 은 브라우저가 CSS 에 알려 주는 사용자 설정이다. 운영체제 접근성 설정의 "동작 줄이기" 를 켜 두면 참이 된다. 전정기관에 문제가 있으면 큰 움직임 하나에 멀미가 오기 때문에, 화면이 미끄러지고 튀면 어지러운 사람이 이 설정을 켠다. @media (prefers-reduced-motion: reduce) 로 물어서 다르게 그릴 수 있다.

SKILL.md 에는 두 번 나온다. 두 번 다 "나중에 하지 마라" 는 말이다.

3단계 구현 절에 이렇게 적혀 있다.

레시피가 있으면 빈 파일이 아니라 레시피에서 시작한다. prefers-reduced-motion 과 hover 게이팅은 나중이 아니라 이때 함께 넣는다.

완료 기준의 네 번째 항목이 다시 확인한다. prefers-reduced-motion 과 hover 게이팅이 같은 변경에 들어갔다.

같은 변경에 넣으라는 조건이 붙은 건 따로 떼어 놓으면 그 PR 이 오지 않기 때문이다. 애니메이션이 이미 동작하니 다음 순번으로 밀린 채 남는다.

사본 쪽 기준은 한 가지를 더 정해 뒀다. reduced motion 은 애니메이션을 없애는 게 아니라 줄이고 부드럽게 하는 것이다. 이해를 돕는 전환은 남기고 움직임과 위치 변화만 뺀다. improve-animations 는 "모든 피드백을 없애 버린 reduced-motion 구현" 을 결함으로 잡는다.

hover 게이팅이 같이 묶인 이유는 따로 있다. 터치 기기에는 커서가 없는데도 탭 한 번에 hover 가 발동한다. 그래서 @media (hover: hover) and (pointer: fine) 로 감싸 진짜 커서가 있는 기기에서만 켠다. 이렇게 조건을 걸어 두는 것이 게이팅이다.

6. 스킬의 실행·검증 절차

트리거는 "애니메이션 넣어줘", "모션 추가해줘", "움직이게 해줘", "전환 효과", "애니메이션 리뷰해줘" 다. 영어로는 animate this, add motion, transition.

절차는 다섯 단계고 3단계 구현의 완료 판정이 명확하다.

완료 판정: 애니메이션되는 속성이 transform 과 opacity 뿐이다. 예외(clip-path, 아코디언의 height)를 썼으면 왜 그것이어야 했는지 한 줄로 남긴다.

4단계는 자기가 바꾼 파일에만 정규식을 돌리는 자체 검사다. 아래 한 줄이 자주 나오는 잘못 네 가지를 한꺼번에 찾는다.

rg -n "transition:\s*all|scale\(0\)|ease-in[^-]|transform-origin:\s*center" <바꾼 경로>

검출을 곧바로 결함으로 치지 않고 걸린 줄마다 따로 판정한다. 모달에 붙은 transform-origin: center 는 정상이고 트리거에 붙은 팝오버에 같은 값이 있으면 결함이다. 모달은 화면 가운데 뜨니까 가운데서 커지는 게 맞고 팝오버는 자기를 띄운 버튼 쪽에서 자라야 맞다.

5단계에서는 판정할 수 없는 것을 밝히라고 한다.

코드만 보고 정할 수 없는 것이 있다 — 크로스페이드의 자연스러움, 스프링의 튐 정도, 목록이 들어올 때 투명도와 높이의 균형. 이런 것은 추측해서 값을 박지 말고 무엇을 어떻게 확인해야 하는지를 남긴다. 느린 재생(2~5배), 프레임 단위 확인, 다음 날 다시 보기.

그리고 이건 blocked_on 이 아니다. blocked_on 은 답을 받기 전에는 다음으로 넘어갈 수 없는 것을 적는 칸이다. 여기는 작업을 멈춰 세우지 않고 사람이 볼 목록으로 남긴다. 에이전트가 판단하지 못한 사항을 별도로 기록하고 사람이 확인할 수 있도록 했다.

산출물은 세 군데에 남는다. 토큰은 .curvez/design/tokens.md, 코드는 프로젝트 소스, 돌린 검사와 결과는 핸드오프의 verification 칸. 핸드오프는 일을 끝낸 에이전트가 다음 에이전트에게 넘기는 인수인계서다.

모션 규칙을 관리하며 생긴 제약

매번 사본을 열어야 한다. 값을 SKILL.md 에 옮겨 적지 못하게 했다. 그래서 드롭다운 하나 만들 때도 animate/SKILL.md 를 읽고 필요하면 RECIPES.md 까지 읽는다. 파일 하나면 끝날 조회가 둘·셋으로 늘어난다. 사본의 값을 옮겨 적어 낡게 만드는 일을 막으려면 매번 파일을 더 읽어야 한다.

사본을 우리 취향대로 못 고친다. 값이 마음에 안 들면 .curvez/design/tokens.md 에 원하는 값을 정해 우선 적용할 수는 있다. 다만 프로젝트 토큰을 쓰기로 하면 사본의 표는 참고 자료로만 남는다. 두 곳을 다 봐야 하는 상태가 된다.

스킬 경계를 좁게 그어야 했다. SKILL.md 가 이유를 직접 밝힌다. 모션은 구현·디자인·리뷰 세 단계에 걸쳐 나타난다. 그래서 세 스킬 모두와 겹친다. 경계를 긋지 않으면 "버튼 만들어줘" 에 이 스킬까지 함께 뜬다. 구현 절차 위에 자기 절차를 덮어쓴다. 그래서 "언제 쓰지 않는가" 목록이 다섯 줄이나 붙었다. 화면 구조·색·간격은 wireframe-spec, 컴포넌트 첫 구현은 nextjs-implementation, 레이어 경계는 structure-audit, typecheck·lint·test 실행은 quality-gate, 핸드오프 JSON 형식은 agent-contract.

1단계 관문이 요청을 거절한다. "여기 애니메이션 좀" 이라고 했는데 코드 0줄이 돌아올 수 있다. 그게 정상 결과라고 문서에 적혀 있어도, 요청한 사람 입장에서는 거절이다. 빈도 등급과 목적 한 단어라는 근거가 같이 오긴 한다. 그래도 원했던 답은 아니다.

중단 가능성이 SKILL.md 에 없다. 위에서 적은 그대로고 사본까지 따라가야 나온다.

7. 토큰과 사용 목적을 기준으로 모션 리뷰하기

모든 애니메이션에 같은 시간을 적용하지는 않는다. 선택한 값은 .curvez/design/tokens.md에 토큰으로 기록해야 한다. 그 자리에 없는 숫자는 코드에 들어가지 못한다.

이제는 모션도 기준을 두고 리뷰할 수 있다. 전에는 "이거 좀 굼뜬데" 라는 지적에 맞댈 근거가 없었다. 기준이 없어 의견을 비교하기 어려웠다. 이제는 다음 항목을 확인한다. 빈도 등급이 뭐였는지, 목적 한 단어가 뭐였는지, 그 커브가 토큰에 이름이 있는지. 답이 안 나오면 그건 결함이다.