내가 Excalidraw용 'Magic Move' 애니메이션 엔진을 처음부터 만든 방법 (출판)

발행: (2026년 1월 12일 오후 10:11 GMT+9)
5 분 소요
원문: Dev.to

Source: Dev.to

목표

저는 시스템 아키텍처를 스케치할 때 Excalidraw를 사랑합니다. 하지만 스케치는 정적입니다. 로드 밸런서를 통과하는 패킷이나 데이터베이스 샤드가 분할되는 과정을 보여주고 싶을 때마다 손을 허둥대며 설명하거나 10개의 서로 다른 슬라이드를 만들어야 했습니다.

저는 “스케치 로직, 모션 내보내기” 를 할 수 있기를 원했습니다.

After Effects 같은 타임라인 편집기는 원하지 않았습니다. 단순한 다이어그램에 비해 너무 과한 작업이니까요.

저는 “키 없는 애니메이션” 을 원했습니다:

  • 프레임 1 (시작 상태)을 그린다.
  • 이를 프레임 2 로 복제한다.
  • 요소들을 새로운 위치로 이동한다.
  • 엔진이 자동으로 전환을 계산한다.

저는 이 엔진을 Next.js, Excalidraw, 그리고 Framer Motion 으로 만들었습니다. 아래는 구현 로직에 대한 기술적인 깊이 있는 탐구입니다.

1. 핵심 로직: 상태 차이점 찾기 (Diffing)

가장 어려운 부분은 애니메이션 루프가 아니라 차이점 찾기 입니다. 프레임 A 에서 프레임 B 로 이동할 때, 우리는 요소들을 안정적인 ID 로 식별하고 세 가지 버킷 중 하나에 분류합니다:

  • Stable: 두 프레임 모두에 존재하는 요소 (형태 변환/이동 필요).
  • Entering: B에는 존재하지만 A에는 없는 요소 (페이드 인 필요).
  • Exiting: A에는 존재하지만 B에는 없는 요소 (페이드 아웃 필요).

아래는 요소들을 효율적으로 매핑하는 categorizeTransition 유틸리티입니다:

// Simplified logic from src/utils/editor/transition-logic.ts
export function categorizeTransition(prevElements, currElements) {
    const stable = [];
    const morphed = [];
    const entering = [];
    const exiting = [];

    const prevMap = new Map(prevElements.map(e => [e.id, e]));
    const currMap = new Map(currElements.map(e => [e.id, e]));

    // 1. Find Morphs (Stable) & Entering
    currElements.forEach(curr => {
        if (prevMap.has(curr.id)) {
            const prev = prevMap.get(curr.id);
            // Separate "Stable" (identical) from "Morphed" (changed)
            // to optimize the render loop
            if (areVisuallyIdentical(prev, curr)) {
                stable.push({ key: curr.id, element: curr });
            } else {
                morphed.push({ key: curr.id, start: prev, end: curr });
            }
        } else {
            entering.push({ key: curr.id, end: curr });
        }
    });

    // 2. Find Exiting
    prevElements.forEach(prev => {
        if (!currMap.has(prev.id)) {
            exiting.push({ key: prev.id, start: prev });
        }
    });

    return { stable, morphed, entering, exiting };
}

2. 속성 보간 (Interpolating Properties)

Morphed 요소에 대해서는 주어진 progress (0.0 → 1.0) 에서 중간 상태를 계산해야 합니다.

  • 숫자 (x, y, width): 선형 보간이 충분합니다.
  • 색상 (strokeColor): Hex 를 RGBA 로 변환하고 각 채널을 보간한 뒤 다시 변환합니다.
  • 각도: “최단 경로” 보간을 사용합니다.

예를 들어 객체가 10° 에서 350° 로 회전한다면, 선형 보간은 긴 경로를 따라가게 됩니다. 아래 헬퍼는 최단 방향을 찾아줍니다:

// src/utils/smart-animation.ts
const angleProgress = (oldAngle, newAngle, progress) => {
    let diff = newAngle - oldAngle;

    // Normalize to -π to +π to find shortest direction
    while (diff > Math.PI) diff -= 2 * Math.PI;
    while (diff  {
    return 1 - Math.pow(1 - t, 4);
};

이렇게 하면 큰 아키텍처 블록이 유령처럼 미끄러지는 대신 무게감 있게 “쿵” 하고 제자리에 자리 잡게 됩니다.

다음 단계는?

현재 진행 중인 작업은 다음과 같습니다:

  • 하위 단계 애니메이션: 하나의 프레임 안에서 불릿 포인트를 클릭해 순차적으로 표시할 수 있게 하기.
  • MP4 로 내보내기: 캔버스 스트림을 직접 비디오 파일로 기록하기.

프로젝트는 라이브이며, 개발자들이 더 잘 소통하도록 돕기 위해 만들었습니다.

여기서 체험해 보세요:
Free Stripe Promotion Code: postara

접근 방식에 대한 의견을 알려 주세요!

Back to Blog

관련 글

더 보기 »

안녕, 뉴비 여기요.

안녕! 나는 다시 S.T.E.M. 분야로 돌아가고 있어. 에너지 시스템, 과학, 기술, 공학, 그리고 수학을 배우는 것을 즐겨. 내가 진행하고 있는 프로젝트 중 하나는...