“애니메이션을 자연스럽게 넣어 달라”고 요청했다. 결과물에서 모달은 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의 규칙을 여기서도 지킨 셈이다.
review-animations/STANDARDS.md에## Interruptibility절이 따로 있다. 빠르게 연달아 발생하는 것에는transition을 쓰라고 적혀 있다. 토스트가 쌓일 때와 토글이 그렇다.animate/SKILL.md의 6단계가Interruption and exit이다. 손가락으로 끌다 놓는 제스처에는 스프링을 쓴다. 스프링은 중단될 때 그때까지의 속도를 이어받는다.improve-animations/AUDIT.md의 4번 항목이Interruptibility다. 심각도 표에서non-interruptible dynamic UI는 MEDIUM이다. 상태가 자주 바뀌는 화면인데 중간에 끊을 수 없게 만든 것을 말한다.
transition과 @keyframes는 같은 것을 적는 두 가지 문법으로 읽히기 쉽다. 토스트를 연달아 두 번 띄우면 결과가 달라진다.
다만 이 대목은 motion-standards/SKILL.md 본문에는 없다. 설명문이 약속한 셋 중 하나가 본문의 절차·규칙·완료 기준 어디에도 없다. 사본까지 따라가면 다 있다. 하지만 SKILL.md만 읽고 작업하는 에이전트는 이걸 놓칠 수 있다.
5. 접근성은 마지막에 붙이는 게 아니었다
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에 이름을 가진 토큰으로 적어 둬야 한다. 그 자리에 없는 숫자는 코드에 들어가지 못한다.
이제는 모션도 기준을 두고 리뷰할 수 있다. 전에는 “이거 좀 굼뜬데” 라는 지적에 맞댈 근거가 없었다. 취향 대 취향이었다. 이제는 물을 수 있다. 빈도 등급이 뭐였는지, 목적 한 단어가 뭐였는지, 그 커브가 토큰에 이름이 있는지. 답이 안 나오면 그건 결함이다.

답글 남기기