Communitygithub.com

yokyznt/60fps-애니메이션

사용자가 "지저분한 CSS 애니메이션 수정", "애니메이션 60fps로 만들기", "레이아웃 스래싱 중단", "width/height/top/left 부드럽게 애니메이션", "애니메이션을 transform으로 변환", "box-shadow 성능 좋게 애니메이션", "height auto 애니메이션", "FLIP 애니메이션", 또는 "스크롤/호버 애니메이션이 끊기는 이유" 등을 요청할 때 사용해야 합니다.

60fps-애니메이션란 무엇인가요?

이 스킬은 웹 애니메이션 성능 저하의 가장 큰 원인인 리플로우(레이아웃 재계산)와 리페인트를 유발하는 속성 애니메이션을 제거하는 방법을 제공합니다. 브라우저 렌더링은 레이아웃 → 페인트 → 합성 단계로 진행되는데, `width`, `height`, `top`, `left` 같은 속성을 애니메이션하면 매 프레임마다 레이아웃과 페인트가 다시 실행되어 메인 스레드를 차단합니다. 대신 `transform`과 `opacity`만 애니메이션하면 합성기(주로 GPU)에서 전적으로 처리되어 60/120fps로 부드러운 애니메이션이 가능합니다. 이 스킬은 CSS 전환, @keyframes, Web Animations API를 지원하며, Claude Code, Cursor, Codex와 같은 주요 AI 에이전트 플랫폼에서 사용할 수 있습니다. 구체적인 기법으로는 FLIP(First, Last, Invert, Play)을 사용한 레이아웃 변경 애니메이션, `height: auto` 애니메이션을 위한 최신 CSS(`interpolate-size: allow-keywords`) 및 그리드 폴백, 의사 요소와 `opacity`를 활용한 효율적인 box-shadow 애니메이션, 그리고 레이아웃 스래싱을 방지하는 읽기/쓰기 일괄 처리 방법을 포함합니다. 또한 DevTools Performance 탭에서 레이아웃/페인트 이벤트가 없는 합성기 전용 프레임을 확인하는 검증 방법을 제공합니다.

지원 대상~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/yokyznt/claude-skills-web-definitiva/tree/HEAD/skills/60fps-animation

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

성능 좋은 웹 애니메이션

끊기는 웹 애니메이션의 #1 원인을 제거하세요: 브라우저가 매 프레임마다 레이아웃을 재계산(리플로우)하거나 리페인트하도록 강제하는 속성 애니메이션. 브라우저는 레이아웃 → 페인트 → 합성 단계로 렌더링합니다. width, height, top, left, margin, padding, box-shadow, 또는 filter를 애니메이션하면 매 프레임마다 레이아웃 및/또는 페인트가 다시 실행되어 메인 스레드를 차단합니다. **transform**과 **opacity**를 애니메이션하면 합성기(주로 GPU)에서 전적으로 실행되어 레이아웃과 페인트를 건너뛰며, 이것이 60/120fps에서 부드러운 애니메이션을 만드는 비결입니다.

사용 시기

애니메이션이 끊기거나 프레임이 드랍될 때, 호버/스크롤 효과가 무겁게 느껴질 때, 크기/위치/그림자를 애니메이션할 때, 레이아웃 변경(카드 재정렬, 컨테이너 간 요소 이동)을 부드럽게 애니메이션해야 할 때, height: auto를 애니메이션할 때, 또는 애니메이션 코드의 성능을 검토할 때 사용하세요.

핵심 규칙: transform과 opacity만 애니메이션

모든 "비싼" 애니메이션을 저렴한 등가물로 매핑하세요.

애니메이션 (비싼)트리거대체 (저렴)
width / height레이아웃 + 페인트transform: scaleX()/scaleY() (+ 실제 크기는 FLIP)
top / left / margin레이아웃 + 페인트transform: translate()
box-shadow페인트그림자를 담은 의사 요소의 opacity 애니메이션
filter: blur()페인트 (무거움)두 레이어를 opacity로 교차 페이드, 또는 제한적으로 사용
background-position페인트하위 레이어에서 transform: translate()
color / background페인트보통 괜찮음; 또는 레이어 교차 페이드

transform으로 위치와 크기 조정

/* 나쁨: 매 프레임 레이아웃 애니메이션 */
.box { transition: left 300ms, width 300ms; left: 0; width: 100px; }
.box:hover { left: 200px; width: 200px; }

/* 좋음: 합성기 전용 */
.box {
  transition: transform 300ms ease;
  transform: translateX(0) scaleX(1);
  transform-origin: left center;
}
.box:hover { transform: translateX(200px) scaleX(2); }

scaleX는 내부 콘텐츠(텍스트)를 늘립니다. 왜곡 없이 실제 크기 조정을 하려면 FLIP을 사용하세요.

의사 요소 불투명도를 통한 저렴한 box-shadow

box-shadow를 애니메이션하면 매 프레임마다 큰 흐린 영역을 다시 페인트합니다. 대신 ::after에 그림자를 한 번 그리고, 그 opacity만 애니메이션하세요.

.card { position: relative; }
.card::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  box-shadow: 0 12px 28px rgba(0,0,0,0.35);
  opacity: 0;
  transition: opacity 300ms ease;
  pointer-events: none;
}
.card:hover::after { opacity: 1; }

흐린 그림자는 한 번 래스터화되고, 호버는 합성기 불투명도만 변경합니다 — 모든 프레임 속도에서 부드럽습니다.

FLIP: 레이아웃 변경을 저렴하게 애니메이션

FLIP(First, Last, Invert, Play)은 transform만 사용하여 레이아웃 변경(재정렬, 크기 조정, 컨테이너 간 이동)을 애니메이션합니다. 요소가 있었던 위치(First)와 있을 위치(Last)를 측정하고, 역변환을 적용하여 시각적으로 움직이지 않은 것처럼 보이게 한 다음(Invert), 변환을 다시 원래대로 애니메이션합니다(Play). DOM은 실제 최종 레이아웃에 있게 되며, 모션은 순수 합성기 작업입니다.

function flip(el, mutate) {
  const first = el.getBoundingClientRect();   // First
  mutate();                                    // DOM/레이아웃 변경
  const last = el.getBoundingClientRect();     // Last

  const dx = first.left - last.left;
  const dy = first.top - last.top;
  const sx = first.width  / last.width;
  const sy = first.height / last.height;

  el.animate(
    [
      { transformOrigin: 'top left',
        transform: `translate(${dx}px, ${dy}px) scale(${sx}, ${sy})` }, // Invert
      { transformOrigin: 'top left', transform: 'none' },               // Play
    ],
    { duration: 300, easing: 'cubic-bezier(0.2, 0, 0, 1)' }
  );
}

// 사용법: 새 그리드 위치로 이동하는 요소 애니메이션
flip(card, () => targetContainer.appendChild(card));

Web Animations API animate()는 합성기에서 transform을 실행합니다. 여러 요소의 경우, 모든 getBoundingClientRect() 읽기를 어떤 변경보다 먼저 일괄 처리하세요 (아래 레이아웃 스래싱 참조).

height: auto 애니메이션

height: auto는 역사적으로 보간이 불가능했습니다. 최신 및 폴백 접근 방식:

/* 최신 (Chrome 129+/지원 브라우저): opt size 키워드를 애니메이션에 포함 */
.panel {
  interpolate-size: allow-keywords;   /* auto로/부터 애니메이션 활성화 */
  height: 0;
  overflow: clip;
  transition: height 300ms ease;
}
.panel.open { height: auto; }
/* calc-size()도 작동: height: calc-size(auto, size); */
/* 오늘날 강력한 폴백: CSS 그리드 1fr -> 0fr */
.wrapper {
  display: grid;
  grid-template-rows: 0fr;            /* 접힘 */
  transition: grid-template-rows 300ms ease;
}
.wrapper.open { grid-template-rows: 1fr; }
.wrapper > .content { overflow: hidden; min-height: 0; }

그리드 트릭은 트랙 크기를 애니메이션하며(합성기 친화적이고 널리 지원됨), JS 높이 측정이 필요 없습니다. 대상 브라우저가 지원하는 곳에서는 interpolate-size: allow-keywords / calc-size(auto, size)를 사용하고, 크로스 브라우저 기본값으로 그리드 기법을 유지하세요.

레이아웃 스래싱 방지 (읽기 일괄 처리 후 쓰기)

쓰기 후에 레이아웃 속성(offsetWidth, getBoundingClientRect, scrollTop, getComputedStyle)을 읽으면 동기식 리플로우가 강제됩니다. 루프에서 읽기와 쓰기를 번갈아 수행하면("레이아웃 스래싱") 프레임당 수십 번의 강제 리플로우가 발생할 수 있습니다.

// 나쁨: 읽기, 쓰기, 읽기, 쓰기... 매 반복마다 리플로우 강제
items.forEach((el) => {
  const w = el.offsetWidth;          // 읽기 (레이아웃 강제)
  el.style.width = w * 1.5 + 'px';   // 쓰기 (레이아웃 무효화)
});

// 좋음: 모든 읽기를 먼저 일괄 처리한 다음 모든 쓰기
const widths = items.map((el) => el.offsetWidth);  // 모든 읽기
items.forEach((el, i) => {                          // 모든 쓰기
  el.style.width = widths[i] * 1.5 + 'px';
});

프레임 동기화 작업의 경우 requestAnimationFrame 콜백에서 읽고 쓰기를 적용하세요. fastdom과 같은 라이브러리가 이 읽기/쓰기 스케줄링을 공식화합니다.

will-change: 신중하게 사용

will-change: transform은 요소를 미리 자체 합성기 레이어로 승격시켜 애니메이션 시작 시 끊김을 방지합니다. 그러나 모든 승격된 레이어는 GPU 메모리를 소비하며, 과도하게 사용하면 성능이 저하됩니다.

.menu { will-change: transform; }   /* 애니메이션할 요소에만 */

규칙: 애니메이션 직전에만 적용(예: 호버/부모 상태), 유휴 상태가 되면 제거(will-change: auto), 많은 요소에 무분별하게 적용하지 않음, 크거나 많은 수의 노드에 영구적으로 남겨두지 않음. 단일 transform: translateZ(0) 핵도 동일한 승격을 수행하지만 되돌리기가 더 어렵습니다 — will-change를 선호하세요.

제공 및 검증 (독립형 HTML)

패키지 도우미 (scripts/): scripts/seek-shot.sh anim.html 0 1.5 3이 ?t=N 하네스를 고정하고 각 순간을 스크린샷으로 캡처합니다. scripts/contact-sheet.sh sheet.png frame-*.png는 한눈에 검토할 수 있도록 타일링합니다. scripts/README.md를 참조하세요.

산출물은 브라우저에서 직접 열리는 하나의 자체 포함 .html 파일입니다 — 마크업, CSS/JS 애니메이션, 고정 하네스가 모두 하나의 파일에 있습니다. 이 스킬에서 검증은 두 가지 측면으로 이루어집니다: 프레임이 올바르게 보여야 하고 그리고 생성 비용이 저렴해야 합니다(합성기 전용).

출력 계약:

  • 하나의 .html, CDN을 통한 종속성(있는 경우), CSS 전환/@keyframes 또는 Web Animations API로 구동되는 애니메이션.
  • 스크린샷이 결정적 프레임에 고정되도록 하는 고정 메커니즘:
    • CSS @keyframes → ?t=N이 el.style.animationDelay = (-N)+'s'; el.style.animationPlayState = 'paused'를 설정합니다.
    • WAAPI / JS → 애니메이션 객체를 유지하고 anim.pause(); anim.currentTime = N*1000을 실행합니다.

검증 루프 — 고정 → 스크린샷, 그런 다음 끊김 프로파일링:

  1. 시작/중간/끝(?t=0, 중간, 끝)에서 헤드리스로 열고, 각각 스크린샷을 캡처합니다. 모션이 시각적으로 올바른지 확인합니다(scaleX로 인한 텍스트 잘림/늘어짐 없음, FLIP이 실제 레이아웃에 정확히 위치, 그림자/높이 전환이 올바르게 보임).
  2. 합성기 전용인지 확인 — 이 스킬의 핵심입니다. 다음 중 하나를 수행합니다:
    • DevTools → Performance: 애니메이션을 녹화하고, 프레임당 보라색 "Layout" 또는 녹색 "Paint" 밴드가 없는지 확인합니다("Composite Layers"만 있어야 함).
    • 또는 헤드리스 트레이스: 시각적 확인을 위해 npx playwright screenshot을 사용하고, CDP/tracing 캡처로 애니메이션 윈도우 동안 Layout/Paint 이벤트가 발생하지 않았는지 단언합니다.
  3. FPS/"Frame Rendering Stats" 오버레이가 60을 유지하는지 확인합니다 — 드랍된 프레임은 비싼 속성이 다시 사용되었음을 의미합니다.
npx playwright screenshot --wait-for-timeout=500 "file://$PWD/anim.html?t=0.3" frame.png

완료 전 확인 사항:

  1. 독립형으로 열림 — 콘솔 오류 없음.
  2. 프레임당 transform / opacity만 애니메이션됨. DevTools에 프레임당 레이아웃/페인트 없음.
  3. 시작/중간/끝에서 스크린샷 캡처 — 정확함, scaleX 텍스트 왜곡 없음, FLIP이 정확히 위치.
  4. 60fps 유지. will-change가 적시에 적용되고 유휴 상태에서 제거됨(잔여 레이어 없음).
  5. prefers-reduced-motion 준수.

빠른 참조

목표이렇게 하세요
요소 이동transform: translate()
왜곡 없이 크기 조정transform을 사용한 FLIP
호버 시 그림자그림자 의사 요소의 opacity 애니메이션
콘텐츠 높이로 확장그리드 0fr→1fr, 또는 interpolate-size: allow-keywords
여러 요소 이동getBoundingClientRect 읽기를 일괄 처리한 다음 쓰기
부드러운 애니메이션 시작적시에 will-change, 유휴 상태에서 제거
합성기 전용인지 확인DevTools Performance: 프레임당 보라색 "Layout"/녹색 "Paint" 없음

주의사항

  • scaleX/scaleY는 텍스트와 자식을 늘립니다. 콘텐츠가 선명하게 유지되어야 하면 FLIP을 사용하세요.
  • transform 백분율은 요소 자체의 박스에 상대적이며, 부모가 아닙니다 — left: %와 다릅니다.
  • will-change 또는 translateZ(0)를 과도하게 사용하면 너무 많은 레이어가 생성되어 성능이 저하됩니다. 애니메이션하는 것만 승격하세요.
  • filter와 backdrop-filter는 합성기와 관련이 있지만 여전히 비용이 많이 듭니다. 블러 반경을 애니메이션하는 대신 불투명도 교차 페이드를 통해 그 존재를 애니메이션하세요.
  • 그리드 1fr→0fr 콘텐츠는 overflow: hidden과 min-height: 0이 있어야 축소됩니다.
  • 필수적이지 않은 모션은 항상 @media (prefers-reduced-motion: reduce) 뒤에 게이트하세요.

참조 파일

  • references/patterns-and-profiling.md — 전체 실행 가능한 예제(아코디언, 목록 재정렬, 패럴랙스), 합성기 전용 프레임 확인을 위한 DevTools 프로파일링 안내, 축소 모션 패턴, 속성 비용 치트 시트.

관련 스킬