내장 트랜지션으로 부족할 때, 직접 만들면 됩니다 — Svelte의 트랜지션 함수 인터페이스는 놀라울 만큼 단순합니다.

개념 정의

커스텀 Transition 은 Svelte의 트랜지션 인터페이스를 구현한 함수입니다. (node, params) => TransitionConfig 형태의 함수를 작성하면 transition:, in:, out: 디렉티브에서 사용할 수 있습니다.

TransitionConfig 인터페이스

TYPESCRIPT
interface TransitionConfig {
  delay?: number;       // 시작 지연 (ms)
  duration?: number;    // 지속 시간 (ms)
  easing?: (t: number) => number;  // 이징 함수
  css?: (t: number, u: number) => string;  // CSS 문자열 반환
  tick?: (t: number, u: number) => void;   // 프레임마다 실행
}
// t: 0(시작) → 1(끝), u: 1-t

CSS 기반 커스텀 트랜지션

CSS 기반은 메인 스레드를 차단하지 않아 성능이 좋습니다.

SVELTE
<script>
  let visible = $state(true);

  // 타이프라이터 효과
  function typewriter(node, { speed = 50 }) {
    const text = node.textContent;
    const duration = text.length * speed;

    return {
      duration,
      css: (t) => {
        const i = Math.floor(text.length * t);
        return `
          clip-path: inset(0 ${100 - (i / text.length) * 100}% 0 0);
          white-space: nowrap;
        `;
      }
    };
  }

  // 회전하며 나타나기
  function spin(node, { duration = 500, degrees = 360 }) {
    return {
      duration,
      css: (t) => {
        const eased = t;
        return `
          transform: rotate(${eased * degrees}deg) scale(${eased});
          opacity: ${eased};
        `;
      }
    };
  }

  // 흔들림 효과
  function shake(node, { duration = 400, intensity = 10 }) {
    return {
      duration,
      css: (t) => {
        const offset = Math.sin(t * Math.PI * 6) * intensity * (1 - t);
        return `transform: translateX(${offset}px);`;
      }
    };
  }
</script>

<button onclick={() => visible = !visible}>토글</button>

{#if visible}
  <p transition:typewriter={{ speed: 30 }}>
    이 텍스트가 한 글자씩 나타납니다
  </p>

  <div transition:spin={{ duration: 800 }}>
    회전하며 등장!
  </div>
{/if}

tick 기반 커스텀 트랜지션

CSS로 표현할 수 없는 애니메이션은 tick을 사용합니다.

SVELTE
<script>
  let visible = $state(true);

  // 숫자 카운트업 효과
  function countUp(node, { target = 100, duration = 1000 }) {
    return {
      duration,
      tick: (t) => {
        node.textContent = Math.floor(t * target).toLocaleString();
      }
    };
  }

  // 랜덤 문자 효과 (매트릭스 스타일)
  function scramble(node, { duration = 800 }) {
    const text = node.textContent;
    const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';

    return {
      duration,
      tick: (t) => {
        let result = '';
        for (let i = 0; i < text.length; i++) {
          if (i < text.length * t) {
            result += text[i];
          } else {
            result += chars[Math.floor(Math.random() * chars.length)];
          }
        }
        node.textContent = result;
      }
    };
  }
</script>

{#if visible}
  <span in:countUp={{ target: 1000, duration: 1500 }}>0</span>
  <p in:scramble={{ duration: 1000 }}>안녕하세요 스벨트!</p>
{/if}

실전 예시 — 카드 플립 트랜지션

SVELTE
<script>
  let visible = $state(true);

  function cardFlip(node, { duration = 600, direction = 'horizontal' }) {
    const axis = direction === 'horizontal' ? 'Y' : 'X';
    return {
      duration,
      css: (t) => {
        const degrees = (1 - t) * 180;
        return `
          transform: perspective(600px) rotate${axis}(${degrees}deg);
          opacity: ${t};
          backface-visibility: hidden;
        `;
      }
    };
  }

  function ripple(node, { duration = 600 }) {
    const style = getComputedStyle(node);
    const width = parseFloat(style.width);
    const height = parseFloat(style.height);
    const maxDim = Math.max(width, height);

    return {
      duration,
      css: (t) => `
        clip-path: circle(${t * maxDim * 1.5}px at center);
        opacity: ${Math.min(t * 2, 1)};
      `
    };
  }
</script>

<button onclick={() => visible = !visible}>토글</button>

{#if visible}
  <div class="card" transition:cardFlip={{ duration: 800 }}>
    <h3>카드 플립</h3>
    <p>3D 회전 트랜지션</p>
  </div>

  <div class="card" transition:ripple>
    <h3>리플 효과</h3>
    <p>원형으로 확장</p>
  </div>
{/if}

<style>
  .card { padding: 1.5rem; margin: 1rem 0; background: #f5f5f5; border-radius: 8px; }
</style>

트랜지션 함수 조합

SVELTE
<script>
  import { cubicOut } from 'svelte/easing';

  // 여러 효과를 조합하는 팩토리 함수
  function combined(node, {
    duration = 500,
    y = 20,
    scale = 0.95,
    blur = 5,
    easing = cubicOut
  }) {
    return {
      duration,
      easing,
      css: (t) => `
        transform: translateY(${(1 - t) * y}px) scale(${scale + (1 - scale) * t});
        opacity: ${t};
        filter: blur(${(1 - t) * blur}px);
      `
    };
  }
</script>

{#if visible}
  <div transition:combined={{ y: 30, blur: 8, duration: 400 }}>
    복합 트랜지션 효과
  </div>
{/if}

면접 포인트

  • "CSS 기반과 tick 기반의 차이는?": CSS 기반 트랜지션은 CSS 애니메이션으로 변환되어 GPU에서 실행되므로 성능이 좋습니다. tick 기반은 매 프레임 JavaScript를 실행하므로 메인 스레드를 차지하지만, DOM 내용 변경 같은 CSS로 불가능한 효과를 구현할 수 있습니다.
  • "t와 u 매개변수는 무엇인가요?": t는 0에서 1로 진행하는 트랜지션 진행률이고, u는 1-t입니다. in 트랜지션에서 t=0이 시작, t=1이 끝이며, out 트랜지션에서는 반대입니다.

정리

커스텀 트랜지션은 결국 "t 값(0→1)에 따라 CSS나 DOM을 어떻게 변화시킬지" 정의하는 함수입니다. 인터페이스가 단순하니 상상력만 있으면 어떤 애니메이션이든 만들 수 있습니다.

댓글 로딩 중...