Figma 애니메이션을 Remotion으로 옮기기: JSON 파싱부터 GSAP까지

– ArtXTech

Figma 애니메이션을 코드로 옮기는 삽질기

Figma나 Jitter에서 만든 예쁜 애니메이션을 보고 “이거 코드로 똑같이 만들어보자!“라고 생각했던 적 있나?

시작은 단순했다

목표: Figma/Jitter → Remotion + GSAP 자동 변환
현실: 온갖 예상치 못한 버그와 삽질의 연속

애초 계획은 간단했다. 애니메이션 JSON을 파싱해서 GSAP 타임라인으로 변환하고, Remotion으로 비디오 렌더링까지 자동화하는 거였다. After Effects나 Lottie 없이도 웹 기술 스택만으로 고품질 애니메이션을 만들어보자는 야심.

그런데 막상 시작하니까… JSON 구조부터가 생각보다 복잡했다.

실제로 만든 것들

1. Countdown 애니메이션 컴포넌트

첫 번째로 만든 건 카운트다운 애니메이션이었다. 5개 레이어가 순차적으로 사라지면서 최종적으로 하나만 남는 구조였는데, 생각보다 까다로웠다.

// 각 레이어별 스케일과 타이밍
const layers = [
  { scale: '140% → 25%', hideTime: 0.7 },
  { scale: '45%', hideTime: 0.8 },
  { scale: '65%', hideTime: 0.9 },
  { scale: '85%', hideTime: 1.0 },
  { scale: '105%', hideTime: 1.12 }
];

// 그룹 레이어는 110% → 90%로 전체적으로 축소

문제는 레이어 스택킹이었다. 상위 레이어가 하위 레이어를 완전히 가려야 하는데, 텍스트 형태라서 투명한 부분이 있어서 뒤가 비쳐 보이는 거였다.

해결책: 배경색을 완전 불투명하게 설정하고 z-index를 명시적으로 지정했다.

.countdownLayer {
  background-color: #FFDE02; // 완전 불투명 배경
  z-index: var(--layer-index); // 레이어별 명시적 순서
}

2. Figma-Jitter JSON 파싱

두 번째는 본격적인 JSON 파싱이었다. “DON’T WORRY, BE HAPPY” 텍스트가 회전하고 스케일 변하고 마스크 효과까지 있는 복잡한 애니메이션이었다.

JSON 구조: 컴포지션  레이어  트랜스폼  키프레임
현실: 중첩된 객체 구조에서 필요한 데이터 찾기가 보물찾기 수준

가장 골치 아팠던 건 키프레임 데이터를 GSAP 타임라인으로 변환하는 부분이었다. Lottie/AE JSON 구조는 정말 복잡하게 되어 있더라.

// JSON에서 키프레임 추출
const keyframes = layer.transform.scale.keyframes;

// GSAP 프로퍼티로 매핑
tl.fromTo(element, 
  { scale: keyframes[0].value },
  { scale: keyframes[1].value, duration: timing }
);

삽질 과정에서 만든 아키텍처

이런 저런 시행착오를 겪으면서 나름의 패턴이 생겼다.

공통 훅 시스템

// useSketchBase: 캔버스 설정과 스케일링 로직 추상화
const useSketchBase = (width, height) => {
  return {
    canvasStyle: { width, height },
    containerStyle: { transform: `scale(${scaleFactor})` }
  };
};

// useGsapTimeline: GSAP 타임라인 생성과 관리
const useGsapTimeline = () => {
  const tl = useRef();
  // 타임라인 설정 로직
  return { timeline: tl.current, addAnimation };
};

프로젝트 구조

VideoEditPlayground/
├── src/remotion/
   ├── Components/
      └── useSketchBase.ts          # 공통 캔버스 훅
   ├── Sketches1920p/
      ├── day40/
         ├── day40Sketch.tsx       # 텍스트 스크램블
         └── sketchDay40.module.scss
      └── countdown/
          ├── CountdownSketch.tsx   # 레이어 카운트다운
          └── countdownSketch.module.scss
   └── 100DaysOfMotion1/
       └── day36/
           ├── Index.tsx             # JSON 변환 애니메이션
           └── day36.module.scss

컴포넌트 관계 (Mermaid)

graph TD
    A[Animation Component] --> B[useSketchBase Hook]
    A --> C[useGsapTimeline Hook]
    B --> D[Canvas Dimensions]
    B --> E[Scaling Logic]
    B --> F[Container Styles]
    C --> G[GSAP Timeline]
    C --> H[Animation Refs]
    
    I[JSON Input] --> J[Parser]
    J --> K[Transform Data]
    K --> C
    
    L[Designer Requirements] --> M[Component Props]
    M --> A

만난 문제들과 해결법

레이어 가시성 문제

문제: 상위 레이어가 하위 레이어를 완전히 가리지 못함
원인: 텍스트 형태로 인한 투명 영역, z-index 미설정
해결: 배경색 불투명화 + 명시적 z-index 설정

타이밍 제어 이슈

문제: 점진적 fade가 아닌 즉시 hide가 필요했음
원인: tl.to() 대신 tl.set() 사용해야 함
해결: 정확한 시점에 즉시 숨김 처리

// 잘못된 방법
tl.to(layerRef.current, { opacity: 0, duration: 0.1 }, exactTime);

// 올바른 방법
tl.set(layerRef.current, { visibility: 'hidden' }, exactTime);

GSAP Timeline 패턴:

    const animationBoxRef = useGsapTimeline<HTMLDivElement>(() => {
      const tl = gsap.timeline();
      // 애니메이션 로직
      return tl;
    });

JSON 구조 복잡성

문제: Lottie/AE JSON의 중첩 구조 해석이 어려움
해결: 체계적인 파싱 로직 구축

// 컴포지션 → 레이어 → 프로퍼티 순서로 파싱
const parseAnimation = (json) => {
  const compositions = json.compositions;
  return compositions.map(comp => ({
    layers: comp.layers.map(parseLayer),
    duration: comp.duration
  }));
};

데이터 흐름도

[Figma/Jitter JSON]
      (parse)
[Animation Config]
      (props)
[Animation Component]
     (useSketchBase)
[Canvas Setup] + [Scaling Logic]
     (useGsapTimeline)
[GSAP Timeline]
      (execute)
[DOM Animation]
      (render)
[Remotion Video Output]

SOLVED ISSUES

  • 레이어 가시성: 배경색 + z-index 조합
  • 타이밍 제어: set() vs to() 구분 사용
  • JSON 파싱: Lottie 구조 → GSAP 프로퍼티 매핑

기술 스택 조합의 묘미

Remotion + GSAP 조합이 생각보다 괜찮았다. Remotion은 비디오 렌더링에 최적화되어 있고, GSAP은 애니메이션 성능이 뛰어나니까 서로의 장점을 살릴 수 있었다.

// Remotion 컴포넌트 내에서 GSAP 사용
export const AnimationComponent = () => {
  const { timeline } = useGsapTimeline();
  
  useEffect(() => {
    timeline
      .fromTo('.element1', { scale: 0 }, { scale: 1, duration: 1 })
      .to('.element2', { rotation: 360, duration: 2 }, '-=0.5');
  }, []);
  
  return <div className="animation-container">...</div>;
};

여기에 TypeScript까지 더해서 타입 안전성도 챙겼다. JSON 파싱할 때 타입 체크가 있으니까 런타임 에러 잡기가 훨씬 수월했다.

최종 목표와 현재 상황

애초 목표는 이거였다:

  • Figma/Jitter 디자이너의 반복 작업 최소화
  • After Effects/Lottie 대체 솔루션 구축
  • 코드 기반 애니메이션 자동 생성
  • 웹 기술 스택으로 고품질 비디오 제작

지금까지 기본적인 레이어 애니메이션과 간단한 JSON 파싱은 구현했다. 하지만 아직 갈 길이 멀다. 복잡한 패스 애니메이션이나 고급 이징 함수, 3D 트랜스폼 같은 건 아직 미지의 영역이다.

배운 점들

  1. JSON 구조 이해의 중요성: Lottie/AE JSON 스펙을 제대로 이해하지 않으면 파싱이 불가능.
  2. 레이어 관리의 복잡성: 단순해 보이는 레이어 스택킹도 실제로는 z-index, 투명도, 마스킹 등 고려할 요소가 많다.
  3. 타이밍의 정확성: 애니메이션에서 0.1초 차이도 전체 느낌을 바꿀 수 있다.
  4. 추상화의 필요성: 공통 로직을 훅으로 분리하니까 코드 재사용성이 크게 향상.

앞으로 해야 할 것들

  • 더 복잡한 애니메이션 패턴 지원
  • 자동 파싱 정확도 향상
  • 성능 최적화 (특히 복잡한 애니메이션에서)
  • 디자이너 친화적인 설정 인터페이스 구축

다른 리모션 포스팅을 보고 싶다면

2025-01-16-implementing-canvas-animation-to-remotion-kr
2025-03-18-making-my-own-video-editor-in-progress
KR-no-more-adobe-starting-remotion-kr
KR-remotion-gsap
KR-making-chart-data-journalism-storytelling-motion-setup-with-remotion
KR-converting-bodymovin-animation-to-remotion-template
KR-how-i-make-a-youtube-series-with-remotion-dynamic-srt-script-setup-exp-likejennie
KR-how-i-made-podcast-interview-with-ai-virtual-idol
KR-remotion-and-shader
KR-thepain-of-animation-debugging