모니터에 소리 지르는 개발자에게 묻다: 모션 캔버스 코드를 리모션으로 욱여넣는 법 (feat. 컴포넌트 뫼비우스의 띠)
뭐 하나 쉬운 것이 없다.
My subtle dyslexic arse with a lack of dopamine gave up proofreading after multiple tries. If you see grammatical errors or some awkward sentences, move on. Use the context clues to understand my post, thanks.
아, 또 누가 모니터 앞에서 머리를 쥐어뜯고 있나.
누구긴 누구야. 나지.
모션 캔버스(Motion Canvas) 코드를 리모션(Remotion)으로 옮기고 싶다고? 참신한 고통을 찾아 나서는군…누가?
아무도 하라고 시키지 않았는데 이걸 하고 있는 내가.
Q1: 모션 캔버스 애니메이션, 리모션으로 어떻게 옮기냐고?
A: 왜 굳이 잘 돌아가는(아마도?) 모션 캔버스 프로젝트를 리모션으로 바꾸려는 건데…
핵심 차이부터 말하자면, 모션 캔버스는 제너레이터 기반의 비동기적인 방식으로 애니메이션을 다루는 경향이 있다. 반면 리모션은 철저히 프레임 기반이다. 모든 것은 현재 프레임 번호(useCurrentFrame)와 보간 함수(interpolate)로 결정되지. 이게 첫 번째 고통의 시작이었다.
가져온 코드 조각들을 보자. 이건 이미 리모션으로 변환된 결과물 같은데, 이걸 예시 삼아보자.
유틸리티 및 스타일:
이런 자잘한 것들은 비교적 옮기기 쉽다. 그냥 자바스크립트/타입스크립트 코드니까.
// styles.ts 같은 스타일 정의 파일
// 그냥 색상이나 폰트 정보 담은 객체지 뭐.
export const Colors = { whiteLabel: 'rgba(255, 255, 255, 0.54)', background: '#141414', // ... 기타 색상들 ...
};
export const BaseFont = { fontFamily: 'JetBrains Mono', fontWeight: 700, fontSize: 28, };
핵심 구조 변환: 문제는 모션 캔버스의 ‘씬(Scene)’ 개념을 리모션의 ’컴포지션(Composition)’으로 바꿔야 한다는 거다. 각 씬은 독립적인 리모션 컴포지션이 된다.
Root.tsx 같은 파일에서 이걸 정의한다.
import {Composition} from 'remotion';
// 각 씬에 해당하는 리액트 컴포넌트들을 임포트
import {Color} from './scenes/Color'; import {Light} from './scenes/Light'; import {Layers} from './scenes/Layers'; import {Shadows} from './scenes/Shadows'; export const MotionCanvasRoot: React.FC = () => { return ( <>
<Composition id="motion-canvas-color"
// 고유 ID. 나중에 렌더링할 때 쓴다.
component={Color} // 이 컴포지션을 렌더링할 리액트 컴포넌트
durationInFrames={300} // 총 프레임 수
fps={30} // 초당 프레임 수
width={1920} // 캔버스 너비
height={1080} // 캔버스 높이
/>
<Composition id="motion-canvas-light" component={Light} durationInFrames={300} fps={30} width={1920} height={1080} /> {/* ... 다른 컴포지션들도 마찬가지 ... */}
</>
);
};
진짜 고통 - 애니메이션 로직 변환: 이게 진짜 골치 아픈 부분이었다. 모션 캔버스의 yield* waitFor(1) 이나 tween() 같은 코드를 전부 리모션 방식으로 바꿔야 한다. 예를 들어, 1초 동안 x 좌표를 0에서 100으로 움직이는 애니메이션이 있다면, 리모션에서는 대충 이런 식이 되겠지.
// (어딘가 컴포넌트 내부)
import { useCurrentFrame, interpolate, Easing } from 'remotion'; const frame = useCurrentFrame(); const duration = 30; // 1초 (30fps 기준) const x = interpolate( frame, [0, duration], // 입력 프레임 범위 [0, 100], // 출력 값 범위
{ easing: Easing.linear, extrapolateLeft: 'clamp', extrapolateRight: 'clamp' } );
// 이제 이 'x' 값을 스타일이나 속성에 적용하면 된다.
모션 캔버스의 모든 애니메이션 로직을 이런 식으로 프레임 기반 계산으로 바꿔야 한다.
Q2: 새 컴포지션 세트, 기존 구조에 어떻게 끼워 넣냐고?
A: 좋아, 이번엔 이미 만들어진 리모션 컴포지션 세트(Compositions/Example1/)를 메인 렌더링 파일(ExampleSet.tsx)에 통합하는 문제로다.
이미 Main.tsx와 CompositionExample.tsx라는 빈 껍데기 파일도 만들어 놨고. 이로서 첫걸음은 내딛었다.
1단계: 컴포지션 정의 몰아넣기 (CompositionExample.tsx)
새로 추가할 모든 <Composition> 태그들을 이 파일 안에 다 때려 박아. 여기가 새 예제들의 ‘본진’이 된다.
//src/remotion/Compositions/Example1/CompositionExample.tsx
import { Composition } from "remotion" // 필요한 컴포넌트, 스키마, 메타데이터 계산 함수 등 임포트
import { AnimationsIntroComposition } from "./Compositions/AnimationsIntro/AnimationsIntroComposition"
// ... 기타 임포트 ...
export const CompositionExample: React.FC = () => { // 기본값 설정 (calculateMetadata에서 덮어쓸 거지만 타입스크립트 달래주기용)
const overwrittenProperties = { durationInFrames: 1, fps: 1, width: 10, height: 10, }
return (
<>
<Composition
{...overwrittenProperties}
id="AnimationsIntro" // 각 컴포지션 고유 ID
component={AnimationsIntroComposition} // 렌더링할 컴포넌트 // schema, calculateMetadata, defaultProps 등 필요한 속성들 // ...
/>
<Composition
{...overwrittenProperties}
id="PopcornSoundVisualizer"
component={PopcornSoundVisualizerComposition} // ...
// ... (PopcornSoundVisualizer의 schema, calculateMetadata 등)
schema={popcornSoundVisualizerCompositionSchema}
calculateMetadata={calculatePopcornSoundVisualizerMetadata}
defaultProps={{
colorPalette: popcornSoundVisualizerColorPalettes.dark,
scene: "FinalScene" as const,
}}
/>
{/* 만약 더 추가할 컴포지션이 있다면 여기에 계속 추가하면 된다 */}
</>
)
}
CompositionExample.tsx
2단계: 메인 컨테이너 만들기 (Main.tsx)
이 파일은 그냥 방금 만든 CompositionExample 컴포넌트를 감싸서 내보내는 역할만 한다. 다른 예제 세트들과 구조를 맞추기 위한 의례적인 절차다.
import { CompositionExample } from './CompositionExample';
// 다른 예제 세트들처럼 컨테이너 컴포넌트를 만들어서 export 한다.
export const Example1Container: React.FC = () => {
return (
// 별다른 로직 없이 CompositionExample을 렌더링
<CompositionExample />
);
};
Main.tsx
3단계: 최종 목적지 - ExampleSet.tsx에 임포트
이제 모든 예제 컴포지션들을 모아놓는 ExampleSet.tsx 파일에서 방금 만든 Example1Container를 임포트해서 사용하면 끝이다.
// ... 다른 예제 세트들의 컨테이너 임포트 ...
import { Example1Container } from './Compositions/Example1/Main'; // <- 새로 추가된 부분
export const RemotionVideo = () => { // 혹은 네 파일의 최상위 컴포넌트 이름
return (
<>
{/* ... 기존에 있던 다른 예제 세트 컴포넌트들 ... */}
{/* 새로 만든 예제 세트 컨테이너를 여기에 추가 */}
<Example1Container />
{/* ... 만약 뒤에 더 있다면 ... */}
</>
);
};
ExampleSet.tsx
**4단계: 뒷정리
언급한 _ref 폴더 안의 Root.tsx, index.ts, render.ts 같은 파일들은 이제 메인 애플리케이션 흐름에서는 필요 없다.
왜 이따위로 하냐고?
- 일관성: 기존 구조랑 비슷하게 맞춰야 나중에 덜 헷갈린다. 물론 그래도 헷갈리다는 건 함정.
- 모듈성: 컴포지션 정의를 별도 파일로 빼두면 관리가 조금 더 용이해진다. 아주 조금.
- 최소 변경: 렌더링 경로니 뭐니 복잡한 거 건드릴 필요 없이, 그냥 임포트하고 렌더링만 하면 되니까.
다른 리모션 포스팅을 보고 싶다면
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-figma-to-jitter-and-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
[[ _notes/KR/WebDevelopment/KR-thepain-of-animation-debugging ]]