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같은 함수면 더하기, 다르면 addrotate(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) animationtransition이 같은 속성을 두고 싸움

.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 — 가장 안전한 보간 대상인 좌표 변환.