02 — Animation & Keyframes (자율적 시퀀스)
한 줄 답:
animation은 시간 자체가 트리거인 **선언적 시간선(timeline)**이다.@keyframes로 어떤 비율에서 어떤 값이어야 하는지를 선언하면, 브라우저가 그 사이를 보간해 자동 재생한다. 2023년animation-composition이 도입되면서 변환을 덮어쓰지 않고 덧붙이는 새 합성 모델이 가능해졌다.
Why — transition이 있는데 왜 또?
transition은 *상태 머신의 변(edge)*에서만 동작한다. 그러나 현실에는 상태와 무관한 시간 기반 변화가 많다.
| 사례 | transition으로 가능? | animation 필요한가? |
|---|---|---|
| 호버 시 색 변경 | ✅ | ❌ |
| 로딩 스피너 (계속 회전) | ❌ (상태 변화 없음) | ✅ |
| 토스트가 나타났다가 3초 뒤 사라짐 | △ (delay로 가능하지만 복잡) | ✅ |
| 시퀀스: A→B→C 단계적 변화 | ❌ (transition은 A→B 1단계) | ✅ |
| 무한 반복 펄스 | ❌ | ✅ |
| 페이지 진입 시 자동 등장 | △ | ✅ (가장 자연스러움) |
즉 반복·시퀀스·상태 무관 자동 재생이 필요할 때 animation이 필요하다.
How — 어떻게 동작하는가
1) @keyframes — 시간 비율 → 속성 값 매핑
@keyframes pulse {
0% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.05); opacity: 0.7; }
100% { transform: scale(1); opacity: 1; }
}- 키프레임 선택자: 백분율 또는
from(=0%) /to(=100%). - 각 키프레임은 그 시점의 스냅샷. 브라우저는 그 사이를 보간한다.
- 보간 알고리즘은 transition과 동일 (interpolable property 정의에 따름).
2) animation 8 longhand × shorthand
.spinner {
animation-name: spin;
animation-duration: 1s;
animation-timing-function: linear;
animation-delay: 0s;
animation-iteration-count: infinite;
animation-direction: normal;
animation-fill-mode: none;
animation-play-state: running;
/* 동등한 shorthand */
animation: spin 1s linear 0s infinite normal none running;
}
@keyframes spin {
to { transform: rotate(360deg); }
}3) timing-function의 키프레임별 적용
⚠️ 자주 헷갈리는 지점: animation에 적용한 timing-function은 전체 곡선이 아니라 각 키프레임 사이의 곡선에 적용된다.
@keyframes step {
0% { transform: translateX(0); animation-timing-function: ease-out; }
50% { transform: translateX(100px); animation-timing-function: ease-in; }
100% { transform: translateX(200px); }
}각 키프레임에서 다음 키프레임으로 가는 곡선을 따로 지정할 수 있다.
4) animation-fill-mode — 시작 전·종료 후 상태
가장 헷갈리는 longhand. 4가지 값이 4가지 기본 위치를 결정한다.
BP: enter 애니메이션은 forwards(끝난 후 마지막 값 유지), exit은 forwards. 그렇지 않으면 애니메이션 끝나는 순간 원래 위치로 튀어 돌아간다.
5) animation-composition (2023+) — 합성 모드
기존: animation이 기본 스타일을 덮어쓴다 (replace).
.item {
transform: translateX(50px); /* base */
animation: shake 0.3s;
}
@keyframes shake {
50% { transform: translateX(-10px); } /* base가 무시됨 → -10px */
}새 모델 — animation-composition:
| 값 | 의미 | 예 |
|---|---|---|
replace (기본) | 기본 스타일 무시 | translateX(-10px) |
add | 변환을 누적 | translateX(50px) translateX(-10px) = translateX(40px) |
accumulate | 같은 함수면 더하기, 다르면 add | rotate(45deg) + rotate(90deg) = rotate(135deg) |
.item {
transform: translateX(50px);
animation: shake 0.3s;
animation-composition: add; /* base 위에 덧붙임 */
}이로써 여러 애니메이션이 같은 transform을 두고 싸우지 않는다 — JavaScript 모션 라이브러리(Framer Motion, GSAP)가 CSS만으로 표현 가능해진 결정적 변화.
Baseline: Chrome 112+ / Safari 16+ / Firefox 115+.
What — 구체 스펙
무한 회전 스피너 (가장 단순)
.spinner {
width: 24px; height: 24px;
border: 3px solid #ddd;
border-top-color: #333;
border-radius: 50%;
animation: spin 800ms linear infinite;
}
@keyframes spin {
to { transform: rotate(1turn); }
}왜 1turn? — 360deg와 같지만, 각도 단위는 의미를 드러낸다. 1turn은 한 바퀴임이 명백. 0.5turn = 180deg.
페이지 진입 페이드인
.hero {
animation: fade-up 600ms cubic-bezier(.2,.8,.2,1) forwards;
}
@keyframes fade-up {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}핵심: forwards. 없으면 애니메이션이 끝나는 순간 다시 opacity:0 + translateY(20px)로 튀어 돌아간다.
시퀀스 — staggered list
.item {
opacity: 0;
animation: fade-in 300ms forwards;
}
.item:nth-child(1) { animation-delay: 0ms; }
.item:nth-child(2) { animation-delay: 100ms; }
.item:nth-child(3) { animation-delay: 200ms; }
/* ... 또는 CSS 변수로 */
.item {
animation-delay: calc(var(--i) * 100ms);
}animation 여러 개 동시
.combo {
animation:
fade-in 300ms forwards,
slide-up 400ms 100ms forwards,
pulse 1s 700ms infinite;
}쉼표로 구분된 여러 시간선이 동시에 진행된다. animation-composition: add와 함께 쓰면 강력하다.
Web Animations API와의 관계
CSS animation은 모두 **WAAPI(Web Animations API)**로 표현 가능. JS에서:
el.animate(
[
{ opacity: 0, transform: 'translateY(20px)' },
{ opacity: 1, transform: 'translateY(0)' }
],
{ duration: 600, easing: 'cubic-bezier(.2,.8,.2,1)', fill: 'forwards' }
);CSS animation은 선언적이고 캐시 가능. WAAPI는 동적·인터랙티브 (현재 시간 조회, 일시정지, reverse). 디자인 시스템 토큰에 등록된 애니메이션은 CSS, 사용자 입력에 즉시 반응하는 건 WAAPI.
What-if — 잘못 쓰면
1) animation-fill-mode: none인 채로 enter 애니메이션
.modal { animation: fade-in 300ms; } /* ❌ fill-mode 없음 */
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}to에서 opacity:1이었지만, 애니메이션 끝나면 *원래 스타일(opacity 미지정 → initial value 1)*로 복귀. 우연히 작동하지만, 이번엔 from에서 transform: scale(.9)였다면 끝난 순간 scale이 원래로 튀어 돌아온다. 항상 forwards 명시.
2) infinite + Layout-trigger 속성
@keyframes wobble {
50% { width: 110%; } /* ❌ */
}
.thing { animation: wobble 2s infinite; }매 프레임 layout 재계산 → 모바일에서 전체 페이지가 느려진다. 대체: transform: scaleX(1.1).
3) animation-delay로 시작 전 상태가 깜빡임
.hero {
animation: fade-in 600ms 300ms forwards;
}
@keyframes fade-in { from { opacity: 0 } to { opacity: 1 } }처음 300ms 동안 .hero는 *원래 상태(opacity:1)*이므로 보였다가 사라졌다 다시 나타남. 해결: animation-fill-mode: both.
4) :hover로 키프레임 애니메이션 토글 시 안 돌아옴
.card { animation: none; }
.card:hover { animation: pulse 300ms; } /* ❌ */호버 해제 시 animation이 갑자기 사라지면서 그 순간 키프레임 값이 즉시 원래로 점프 → 깜빡임. 이 경우 transition이 더 적합. animation은 해제 가능한 토글에는 맞지 않다 (animation에 역방향 트리거 개념이 없기 때문).
5) animation과 transition이 같은 속성을 두고 싸움
.thing {
transition: transform 200ms;
animation: shake 500ms infinite;
}명세상 animation이 transition을 이긴다 (cascade origin animation > transition). 그러나 animation-composition: add로 동시에 살릴 수 있다.
Insight — 한 단락 이야기
“keyframes는 Disney Animation의 1933년 12 Principles에서 차용된 이름이다.”
디즈니의 애니메이터 Ub Iwerks는 1933년 “키 포즈를 그리는 사람 + 사이를 채우는 사람” 분업을 정립했다 (Animator + Inbetweener). Key pose가 줄어 keyframe이 됐다. CSS
@keyframes는 같은 발상이다 — 결정적 순간만 적고 사이는 브라우저가 채운다. 그래서 CSS animation은 전체 그림이 아니라 어디서 무엇이 일어나야 하는가만 선언한다. 2023년animation-composition은 더 나아가 여러 애니메이터가 동시에 그릴 수 있게 했다 — Disney 분업 모델의 다음 진화다. 이 추상이 정확히 디자인 도구(Figma의 Smart Animate, Lottie)와 일치하기에, 디자이너가 그리는 모션 = CSS가 표현하는 모션의 등가성이 성립한다.
요약 + Mermaid
@keyframes로 비율→값 맵을 정의,animation으로 시간선을 호출.- 8 longhand:
name·duration·timing-function·delay·iteration-count·direction·fill-mode·play-state. fill-mode: forwards는 enter 애니메이션의 기본 BP.- 2023년
animation-composition: add로 여러 애니메이션이 같은 transform을 누적. JS 모션 라이브러리 영역의 큰 부분이 CSS로. - transition vs animation: 상태 트리거 vs 시간 트리거, 멘탈 모델이 다르다.
- 무한 반복·시퀀스·자동 재생 → animation. 호버·포커스·토글 → transition.
다음: 03-transform — 가장 안전한 보간 대상인 좌표 변환.