전체 목록
CSSMedium#82

CSS 애니메이션과 JavaScript 애니메이션의 차이점과 각각의 적합한 사용 시나리오를 설명해주세요.

#CSS#애니메이션#성능#GPU
힌트

브라우저의 렌더링 파이프라인과 합성(Compositing) 레이어를 생각해보세요.

정답 및 해설

CSS 애니메이션과 JavaScript 애니메이션의 차이점과 각각의 적합한 사용 시나리오를 설명해주세요.

CSS 애니메이션과 JavaScript 애니메이션은 웹에서 동적인 시각 효과를 구현하는 두 가지 주요 방법입니다. 브라우저 렌더링 파이프라인의 어느 단계에서 처리되느냐에 따라 성능 특성이 크게 달라집니다. 올바른 방법을 선택하면 부드러운 60fps 애니메이션을 구현하면서 메인 스레드의 부하를 최소화할 수 있습니다.

브라우저 렌더링 파이프라인

애니메이션의 성능을 이해하려면 먼저 브라우저 렌더링 과정을 알아야 합니다.

JavaScript → Style → Layout → Paint → Composite
                                       (GPU 합성)

각 단계를 트리거하는 CSS 속성:
- Layout 트리거: width, height, margin, padding, top, left, font-size ...
  (가장 비싸다 → 리플로우/Reflow 발생)
- Paint 트리거: background, color, box-shadow, border-radius ...
  (중간 비용 → 리페인트/Repaint 발생)
- Composite만: transform, opacity
  (가장 저렴 → GPU 레이어에서 처리, 메인 스레드 비용 없음)

CSS 애니메이션

@keyframes와 animation 속성

/* @keyframes로 애니메이션 정의 */
@keyframes slideIn {
    from {
        transform: translateX(-100%);
        opacity: 0;
    }
    to {
        transform: translateX(0);
        opacity: 1;
    }
}

@keyframes pulse {
    0%   { transform: scale(1); }
    50%  { transform: scale(1.1); }
    100% { transform: scale(1); }
}

/* animation 속성으로 적용 */
.card {
    animation: slideIn 0.3s ease-out forwards;
}

.button:hover {
    animation: pulse 0.5s ease-in-out infinite;
}

animation 속성 상세

.animated-element {
    animation-name: slideIn;           /* 애니메이션 이름 */
    animation-duration: 0.5s;         /* 지속 시간 */
    animation-timing-function: ease;   /* 속도 곡선 */
    animation-delay: 0.2s;            /* 시작 지연 */
    animation-iteration-count: 1;     /* 반복 횟수 (infinite 가능) */
    animation-direction: normal;      /* 방향 (alternate, reverse) */
    animation-fill-mode: forwards;    /* 종료 후 상태 유지 */
    animation-play-state: running;    /* 재생/일시정지 */

    /* 단축 표기 */
    animation: slideIn 0.5s ease 0.2s 1 normal forwards;
}

CSS transition (단순 상태 전환)

.button {
    background-color: #007bff;
    transform: translateY(0);
    box-shadow: 0 2px 4px rgba(0,0,0,0.2);
    transition: background-color 0.2s ease,
                transform 0.15s ease,
                box-shadow 0.2s ease;
}

.button:hover {
    background-color: #0056b3;
    transform: translateY(-2px);
    box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}

.button:active {
    transform: translateY(0);
    box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}

GPU 가속 활용 (will-change)

/* GPU 레이어로 승격 예고 → 사전 최적화 */
.animated-card {
    will-change: transform, opacity;
}

/* 주의: 남용하면 메모리 사용량 증가 */
/* 애니메이션이 시작되기 직전에 추가하고, 종료 후 제거하는 것이 좋음 */

/* JavaScript로 동적 관리 */
element.addEventListener('mouseenter', () => {
    element.style.willChange = 'transform';
});
element.addEventListener('animationend', () => {
    element.style.willChange = 'auto';
});

JavaScript 애니메이션

requestAnimationFrame 기본 사용

// requestAnimationFrame: 브라우저의 다음 리페인트 전에 콜백 호출
// 일반적으로 1초에 60번 호출 (60fps)

function animate(element, targetX, duration) {
    const startX = element.getBoundingClientRect().left;
    const distance = targetX - startX;
    let startTime = null;

    function step(timestamp) {
        if (!startTime) startTime = timestamp;

        const elapsed = timestamp - startTime;
        const progress = Math.min(elapsed / duration, 1);

        // 이징 함수 적용 (easeInOut)
        const eased = easeInOutCubic(progress);

        element.style.transform = `translateX(${distance * eased}px)`;

        if (progress < 1) {
            requestAnimationFrame(step); // 다음 프레임 예약
        }
    }

    requestAnimationFrame(step);
}

// 이징 함수
function easeInOutCubic(t) {
    return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
}

복잡한 인터랙티브 애니메이션

// 스크롤 기반 시차 효과 (Parallax)
class ParallaxEffect {
    constructor(elements) {
        this.elements = elements;
        this.ticking = false;
    }

    handleScroll() {
        const scrollY = window.scrollY;

        if (!this.ticking) {
            requestAnimationFrame(() => {
                this.updateParallax(scrollY);
                this.ticking = false;
            });
            this.ticking = true; // 중복 RAF 방지
        }
    }

    updateParallax(scrollY) {
        this.elements.forEach(({ element, speed }) => {
            const offset = scrollY * speed;
            element.style.transform = `translateY(${offset}px)`;
        });
    }

    init() {
        window.addEventListener('scroll', this.handleScroll.bind(this), { passive: true });
    }
}

// 사용
const parallax = new ParallaxEffect([
    { element: document.querySelector('.hero-bg'), speed: 0.3 },
    { element: document.querySelector('.hero-text'), speed: 0.5 }
]);
parallax.init();

물리 기반 애니메이션 (스프링)

// 스프링 물리 시뮬레이션
class SpringAnimation {
    constructor(config = {}) {
        this.stiffness = config.stiffness || 100; // 탄성 계수
        this.damping = config.damping || 10;       // 감쇠 계수
        this.mass = config.mass || 1;              // 질량
    }

    animate(element, targetValue, onUpdate, onComplete) {
        let currentValue = parseFloat(element.dataset.value || 0);
        let velocity = 0;
        let rafId;

        const step = (timestamp) => {
            const force = -this.stiffness * (currentValue - targetValue);
            const dampingForce = -this.damping * velocity;
            const acceleration = (force + dampingForce) / this.mass;

            velocity += acceleration * 0.016; // ~60fps delta time
            currentValue += velocity * 0.016;

            onUpdate(currentValue);

            const isSettled = Math.abs(targetValue - currentValue) < 0.01
                            && Math.abs(velocity) < 0.01;

            if (!isSettled) {
                rafId = requestAnimationFrame(step);
            } else {
                onUpdate(targetValue);
                onComplete && onComplete();
            }
        };

        rafId = requestAnimationFrame(step);
        return () => cancelAnimationFrame(rafId); // 취소 함수 반환
    }
}

Web Animations API (WAAPI)

JavaScript에서도 GPU 가속을 활용할 수 있는 현대적인 API입니다.

// Web Animations API: JS에서 CSS 애니메이션처럼 GPU 가속 활용
const element = document.querySelector('.card');

// animate() 메서드
const animation = element.animate(
    [
        { transform: 'translateX(-100%)', opacity: 0 }, // keyframe 0%
        { transform: 'translateX(0)',     opacity: 1 }  // keyframe 100%
    ],
    {
        duration: 500,
        easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
        fill: 'forwards',
        delay: 200
    }
);

// 애니메이션 제어
animation.pause();
animation.play();
animation.reverse();
animation.cancel();

// 완료 감지
animation.finished.then(() => {
    console.log('애니메이션 완료');
});

// 진행 상태 확인
console.log(animation.currentTime);     // ms 단위
console.log(animation.playbackRate);    // 재생 속도 (1 = 보통)

GSAP (GreenSock Animation Platform)

복잡한 JS 애니메이션에 가장 많이 사용되는 라이브러리입니다.

// GSAP으로 타임라인 기반 시퀀스 애니메이션
import gsap from 'gsap';

// 기본 트윈
gsap.to('.card', {
    x: 200,
    opacity: 0.5,
    duration: 1,
    ease: 'power2.out'
});

// 타임라인으로 순차 애니메이션
const tl = gsap.timeline({ repeat: -1, yoyo: true });

tl.from('.title', { y: -50, opacity: 0, duration: 0.5 })
  .from('.subtitle', { y: 30, opacity: 0, duration: 0.4 }, '-=0.2') // 0.2초 겹침
  .from('.button', { scale: 0, duration: 0.3, ease: 'back.out(1.7)' });

// 스크롤 트리거 (ScrollTrigger 플러그인)
gsap.registerPlugin(ScrollTrigger);

gsap.from('.feature-card', {
    scrollTrigger: {
        trigger: '.features-section',
        start: 'top 80%',
        end: 'bottom 20%',
        scrub: true
    },
    x: -100,
    opacity: 0,
    stagger: 0.2 // 각 요소 0.2초 간격으로 순차 적용
});

CSS vs JavaScript 애니메이션 비교

성능 관점

CSS transform/opacity 애니메이션:
메인 스레드  ──────────────────────────────────────>
                (JS, Style, Layout, Paint)
GPU 스레드   ─── [Composite: transform/opacity 처리] ──>
                 ↑ 메인 스레드 블로킹에 영향 없음

JS layout 변경 애니메이션 (left, width 등):
메인 스레드  ─── [JS] ─ [Style] ─ [Layout] ─ [Paint] ─ [Composite] ──>
                 ↑ 모든 단계 거침, Jank 발생 가능

언제 무엇을 사용할까?

사용 사례별 선택 가이드:

단순 호버/전환 효과  → CSS transition
────────────────────────────────────
버튼 호버, 색상 변경, 요소 등장/퇴장

반복되는 UI 애니메이션  → CSS @keyframes
────────────────────────────────────────
로딩 스피너, 깜빡임 효과, 무한 루프

스크롤 기반 애니메이션  → JavaScript (GSAP ScrollTrigger)
──────────────────────────────────────────────────────
패럴랙스, 요소 등장 제어, 진행 표시

물리/수학 기반 애니메이션  → JavaScript
──────────────────────────────────────
스프링 효과, 입자 시스템, 게임 효과

복잡한 시퀀스 애니메이션  → JavaScript (GSAP)
─────────────────────────────────────────────
단계별 소개 화면, 스토리텔링

사용자 인터랙션 기반  → JavaScript
───────────────────────────────
드래그 앤 드롭, 커서 추적, 터치 제스처

요약 표

구분CSS 애니메이션JavaScript 애니메이션
실행 스레드GPU (transform/opacity)기본 메인 스레드, WAAPI는 GPU 가능
성능transform/opacity는 Jank 없음rAF + transform 사용 시 우수
동적 제어제한적 (클래스 추가/제거)완전한 제어 가능
복잡한 로직어려움용이
물리 시뮬레이션불가능가능
중간 취소/역방향제한적자유로움
코드 복잡도낮음높음 (라이브러리로 보완)
적합한 사례UI 전환, 간단한 반복 효과인터랙티브, 시퀀스, 물리 기반
최적화 방법설명적용 대상
transform/opacity 사용Composite 단계만 트리거CSS/JS 모두
will-changeGPU 레이어 사전 승격미리 알 수 있는 애니메이션
requestAnimationFrame브라우저 최적 시점에 실행JS 애니메이션
passive 이벤트 리스너스크롤 이벤트 최적화스크롤 기반
WAAPIJS에서도 GPU 가속복잡한 JS 애니메이션