SSGOI LogoSSGOI

요소 애니메이션

개별 DOM 요소에 애니메이션 적용하기

SSGOI
from0
to1
spring

Transition 예제

다양한 transition 효과를 사용하는 방법입니다:

import { transition } from '@ssgoi/react';
import { fade, scale, blur, slide, fly, rotate, bounce } from '@ssgoi/react/transitions';

/**
 * [React] Auto Key Plugin
 *
 * 플러그인 사용 시 key를 생략할 수 있습니다.
 * 파일:라인:컬럼 기반으로 고유 key가 자동 생성됩니다.
 *
 * Next.js 설정:
 * ```ts
 * // next.config.ts
 * import SsgoiAutoKey from "@ssgoi/react/unplugin/webpack";
 *
 * const nextConfig = {
 *   webpack: (config) => {
 *     config.plugins.push(SsgoiAutoKey());
 *     return config;
 *   },
 * };
 * ```
 *
 * ⚠️ 주의: .map() 리스트에서는 JSX key만 있으면 됨 - 플러그인이 자동으로 처리
 *
 * 예시:
 * // .map() 리스트에서는 JSX key만 있으면 됨
 * // 플러그인이 자동으로 file:line:col:${jsxKey} 형태로 생성
 * {items.map((item) => (
 *   <div
 *     key={item.id}  // JSX key만 있으면 충분
 *     ref={transition(fade())}
 *   >
 *     {item.name}
 *   </div>
 * ))}
 *
 * 플러그인 없이 사용 시에는 key가 필수입니다.
 */

// Fade 트랜지션
<div ref={transition(fade({ physics: { spring: { stiffness: 300, damping: 30 } } }))}>
  Fade 효과
</div>

// Scale 트랜지션
<div ref={transition(scale({ physics: { spring: { stiffness: 300, damping: 30 } } }))}>
  Scale 효과
</div>

// Blur 트랜지션
<div ref={transition(blur({ amount: 10, physics: { spring: { stiffness: 300, damping: 30 } } }))}>
  Blur 효과
</div>

// Slide 트랜지션 (방향 지정)
<div ref={transition(slide({ direction: 'left', physics: { spring: { stiffness: 300, damping: 30 } } }))}>
  Slide 효과
</div>

// Fly 트랜지션 (커스텀 위치)
<div ref={transition(fly({ x: 200, y: -50, physics: { spring: { stiffness: 300, damping: 30 } } }))}>
  Fly 효과
</div>

// Rotate 트랜지션
<div ref={transition(rotate({ physics: { spring: { stiffness: 300, damping: 30 } } }))}>
  Rotate 효과
</div>

// Bounce 트랜지션
<div ref={transition(bounce({ physics: { spring: { stiffness: 300, damping: 30 } } }))}>
  Bounce 효과
</div>

// 명시적 key 사용 (플러그인 없이 사용 시 필수)
<div ref={transition({
  key: "scale-element",
  ...scale({ physics: { spring: { stiffness: 300, damping: 30 } } })
})}>
  Scale 효과
</div>

기본 구조

TransitionConfig 인터페이스

interface TransitionConfig {
  physics?: {
    // Spring: 스프링 물리 (ease-out 효과, 기본값)
    spring?: {
      stiffness: number;  // 스프링 강성 (기본: 300)
      damping: number;    // 감쇠 계수 (기본: 30)
      doubleSpring?: boolean | number; // 이중 스프링 효과 (기본: false)
    };
    // Inertia: 관성 물리 (ease-in 효과)
    inertia?: {
      acceleration: number; // 가속도 (기본: 500)
      resistance: number;   // 저항 계수 (기본: 10)
    };
    // Custom Integrator: 커스텀 물리 엔진
    integrator?: () => Integrator;
  };
  // 두 가지 애니메이션 모드 중 하나 선택 (둘 다 사용 불가)
  tick?: (progress: number) => void; // RAF 기반 애니메이션
  css?: (progress: number) => StyleObject; // Web Animation API 기반 (권장)
  prepare?: (element: HTMLElement) => void; // 애니메이션 시작 전 초기 설정
  onStart?: () => void;
  onEnd?: () => void;
}

물리 엔진 옵션

SSGOI는 다양한 물리 엔진을 지원하여 자연스러운 애니메이션을 구현합니다:

Spring (스프링 물리)

ease-out 효과를 제공하는 기본 물리 엔진입니다. 애니메이션이 빠르게 시작하여 목표 지점에 도달하면서 자연스럽게 감속합니다.

{
  in: (element) => ({
    physics: {
      spring: {
        stiffness: 300,  // 스프링 강성 (높을수록 빠름)
        damping: 30,     // 감쇠 계수 (높을수록 탄성 감소)
        doubleSpring: false  // 이중 스프링 효과
      }
    },
    css: (progress) => ({
      opacity: progress,
      transform: `translateY(${(1 - progress) * 20}px)`,
    }),
  })
}

Inertia (관성 물리)

ease-in 효과를 제공하는 물리 엔진입니다. 애니메이션이 천천히 시작하여 점점 가속됩니다.

{
  in: (element) => ({
    physics: {
      inertia: {
        acceleration: 500,  // 가속도 (높을수록 빠르게 가속)
        resistance: 10      // 저항 계수 (높을수록 감속)
      }
    },
    css: (progress) => ({
      opacity: progress,
      transform: `scale(${0.8 + progress * 0.2})`,
    }),
  })
}

Custom Integrator (커스텀 물리 엔진)

직접 물리 엔진을 구현하여 사용할 수 있습니다.

{
  in: (element) => ({
    physics: {
      integrator: () => ({
        next: (dt, state) => {
          // 커스텀 물리 계산 로직
          return newState;
        }
      })
    },
    css: (progress) => ({
      opacity: progress,
    }),
  })
}

애니메이션 모드: css vs tick

SSGOI는 두 가지 애니메이션 모드를 지원합니다:

css 모드 (권장)

{
  in: (element) => ({
    physics: { spring: { stiffness: 300, damping: 30 } },
    css: (progress) => ({
      opacity: progress,
      transform: `translateY(${(1 - progress) * 20}px)`,
    }),
  })
}
  • Web Animation API 사용으로 메인 스레드 부하 최소화
  • 스프링 물리 연산을 미리 계산하여 키프레임으로 변환
  • GPU 가속을 활용한 부드러운 애니메이션
  • 성능이 더 좋아 대부분의 경우 권장

tick 모드

{
  in: (element) => ({
    physics: { spring: { stiffness: 300, damping: 30 } },
    tick: (progress) => {
      element.style.opacity = String(progress);
      element.style.transform = `translateY(${(1 - progress) * 20}px)`;
    },
  })
}
  • requestAnimationFrame 기반으로 매 프레임마다 콜백 실행
  • DOM을 직접 조작해야 하는 복잡한 애니메이션에 적합
  • 애니메이션 중간에 동적으로 값을 변경해야 할 때 유용

csstick은 동시에 사용할 수 없습니다. 하나만 선택하세요.

트랜지션 정의

interface Transition {
  in?: (element: HTMLElement) => TransitionConfig;
  out?: (element: HTMLElement) => TransitionConfig;
}

동작 원리

  1. 마운트 시: 요소가 DOM에 추가될 때 in 함수 실행
  2. 언마운트 시: 요소가 제거되기 전 out 함수 실행
  3. 애니메이션: 스프링 물리 엔진이 progress 생성
    • in: 0 → 1
    • out: 1 → 0
  4. tick 콜백: 매 프레임마다 호출되어 스타일 업데이트

트랜지션 프리셋

import { fade, scale /** 등등 */ } from "@ssgoi/react/transitions";

프레임워크별 사용법

import { transition } from "@ssgoi/react";

<div
  ref={transition({
    key: "unique-key",
    in: (element) => ({
      tick: (progress) => {
        element.style.opacity = progress;
        element.style.transform = `translateY(${20 * (1 - progress)}px)`;
      },
    }),
    out: (element) => ({
      tick: (progress) => {
        element.style.opacity = 1 - progress;
      },
    }),
  })}
>
  Content
</div>

Progress 동작 방식

key

key는 SSGOI가 각 요소의 애니메이션 상태를 추적하는 데 사용하는 고유 식별자입니다:

  • 고유성: 페이지 내에서 각 transition() 호출마다 고유한 key가 필요합니다
  • 상태 추적: DOM 요소가 생성되거나 삭제될 때도 애니메이션 상태를 유지할 수 있도록 합니다
  • React 자동 생성: React에서는 Auto Key Plugin을 사용하면 key를 수동으로 지정하지 않아도 됩니다 (설정 방법은 위 React 탭의 주석 참조)
  • 수동 지정: 다른 프레임워크에서는 명시적으로 key를 지정해야 합니다

in 애니메이션

  • progress: 0 → 1
  • 요소가 나타날 때 실행
  • 투명도 0에서 1로, 작은 크기에서 원래 크기로

out 애니메이션

  • progress: 1 → 0
  • 요소가 사라질 때 실행
  • 투명도 1에서 0으로, 원래 크기에서 작은 크기로
// 예시: in과 out의 차이
{
  in: (element) => ({
    tick: (progress) => {
      // progress: 0 → 1
      element.style.opacity = progress;  // 0 → 1
    }
  }),
  out: (element) => ({
    tick: (progress) => {
      // progress: 1 → 0
      element.style.opacity = progress;  // 1 → 0
    }
  })
}

prepare 콜백

애니메이션이 시작되기 전에 DOM 요소를 준비하는 단계입니다:

{
  in: {
    prepare: (element) => {
      // tick이 실행되기 전에 초기 상태 설정
      element.style.willChange = 'opacity, transform';
    },
    tick: (progress) => ({
      opacity: progress,
      transform: `translateY(${20 * (1 - progress)}px)`
    })
  }
}

TransitionScope

TransitionScope는 자식 요소들의 애니메이션 동작을 제어하는 컨테이너입니다. 부모와 자식이 동시에 마운트/언마운트될 때 불필요한 애니메이션을 건너뛸 수 있습니다.

사용 시나리오

  • 페이지 전환: 페이지가 전환될 때 개별 요소들이 각각 애니메이션하지 않도록
  • 모달/다이얼로그: 컨테이너와 함께 나타나는 콘텐츠의 중복 애니메이션 방지
  • 리스트: 전체 리스트가 마운트/언마운트될 때 개별 아이템 애니메이션 건너뛰기

scope 옵션

interface TransitionOptions {
  // ...
  scope?: 'global' | 'local';
}
  • global (기본값): 항상 애니메이션 실행
  • local: TransitionScope와 동시에 마운트/언마운트될 때 애니메이션 건너뜀

사용 예시

import { TransitionScope, transition } from '@ssgoi/react';

function MyComponent() {
  const [show, setShow] = useState(true);

  return (
    <>
      <button onClick={() => setShow(!show)}>Toggle</button>

      {show && (
        <TransitionScope>
          <div style={{ padding: '2rem', border: '1px dashed #ccc' }}>
            {/* local: Scope와 함께 마운트/언마운트 시 애니메이션 건너뜀 */}
            <div
              ref={transition({
                key: 'local-child',
                scope: 'local',
                in: () => ({
                  css: (p) => ({ opacity: p, transform: `scale(${0.5 + p * 0.5})` }),
                }),
                out: () => ({
                  css: (p) => ({ opacity: p, transform: `scale(${0.5 + p * 0.5})` }),
                }),
              })}
            >
              Local Scope (Scope와 함께 나타남/사라짐)
            </div>

            {/* global: 항상 애니메이션 실행 */}
            <div
              ref={transition({
                key: 'global-child',
                // scope: 'global'  기본값
                in: () => ({
                  css: (p) => ({ opacity: p, transform: `scale(${0.5 + p * 0.5})` }),
                }),
                out: () => ({
                  css: (p) => ({ opacity: p, transform: `scale(${0.5 + p * 0.5})` }),
                }),
              })}
            >
              Global Scope (항상 애니메이션)
            </div>
          </div>
        </TransitionScope>
      )}
    </>
  );
}

동작 방식

상황scope: 'local'scope: 'global'
Scope와 동시에 마운트애니메이션 건너뜀애니메이션 실행
Scope와 동시에 언마운트애니메이션 건너뜀애니메이션 실행
Scope 내에서 개별 토글애니메이션 실행애니메이션 실행