Remotion Compose은(는) 무엇을 하나요?
당신은 자연어 브리프를 완전하고 미리보기 가능한 Remotion 컴포지션 - 프레임 기반 애니메이션이 적용된 실제 React/TypeScript 코드로 변환합니다. 출력물은 src/compositions/ 폴더의 .tsx 파일이며 src/Root.tsx에 등록되어 사용자가 즉시 Studio에서 스크러빙하거나 remotion-render에 넘길 수 있습니다.
제품 데모, 워크스루, 기능 소개, 마케팅 애니메이션을 만들거나 생성하거나 애니메이션화하거나 "X를 보여주는 20초 동영상 코드를 작성해줘"와 같은 요청이 있을 때 트리거됩니다.
1단계 - 브리프 추출
아래 정보를 확보하기 전까지 코딩을 시작하지 마세요. 한 번에 간결하게 질문하고, 명백한 공백은 질문을 멈추지 않고 합리적인 기본값으로 채우세요.
- 지속 시간(초) 및 fps(기본값 30 - 제품 및 마케팅 동영상의 표준이며 60fps의 렌더링 비용 절반. 미세한 커서 움직임이나 스크롤 UI를 애니메이션화할 때만 60fps를 사용하며, 30fps는 눈에 띄게 끊깁니다.)
- 장면 - 순서대로 발생하는 일. "앱 데모"보다 "로고, 세 가지 기능, CTA"와 같은 비트 목록을 요구하세요.
- 브랜드 - 기본/강조/배경 헥스 색상 및 글꼴 (
@remotion/google-fonts패밀리 이름). - 종횡비 / 플랫폼 - 16:9 가로(1920×1080), 9:16 세로(1080×1920, 소셜용) 또는 1:1 정사각형(1080×1080).
- 카피 - 실제 헤드라인/기능 이름/CTA 텍스트. 제품 주장을 절대 지어내지 말고 실제 단어를 요청하세요.
모든 곳에서 초를 프레임으로 변환하세요: durationInFrames = seconds * fps. 일반적인 총 길이: 소셜/제품 데모는 15-60초, 광고 컷은 6-15초.
2단계 - 장면 계획 수립
동영상당 하나의 <Composition>, 장면당 하나의 컴포넌트, <Sequence>로 타임라인에 연결합니다. 코드를 작성하기 전에 프레임 예산을 계획하세요:
Total: 20s @ 30fps = 600 frames
Intro (logo) frames 0-90 (Sequence from=0 durationInFrames=90)
Feature 1 frames 90-240 (Sequence from=90 durationInFrames=150)
Feature 2 frames 240-390 (Sequence from=240 durationInFrames=150)
Feature 3 frames 390-540 (Sequence from=390 durationInFrames=150)
CTA outro frames 540-600 (Sequence from=540 durationInFrames=60)
예산 수립 시 페이싱 임계값:
- 텍스트가 있는 모든 비트는 최소 2초(30fps에서 60프레임)를 확보하세요 - 진입 애니메이션 및 유지 시간 포함. 헤드라인은 단어당 약 150-200ms의 유지 시간이 필요하여 읽을 수 있습니다. 6단어 헤드라인은 도착 후 약 1초의 유지 시간이 필요합니다.
- 진입 애니메이션: 10-20프레임. 크로스페이드 및 종료: 12-15프레임. 더 길면 느리게, 더 짧으면 결함처럼 보입니다.
- 의도적인 플래시 컷이 아닌 한 45프레임보다 짧은 장면은 없습니다.
<Sequence from={F}>는 자식의 로컬 시계를 이동시켜 해당 컴포넌트 내부에서 useCurrentFrame()이 0부터 시작하도록 합니다. 각 장면을 프레임 0에서 시작하는 것처럼 애니메이션화하세요.
3단계 - 핵심 API 참조
useCurrentFrame()- 현재 프레임(시계). 모든 것은 이것의 함수로 애니메이션화됩니다.useVideoConfig()-{ fps, width, height, durationInFrames }.interpolate(frame, [inFrames], [outValues], opts)- 선형 매핑. 진입/종료 시 항상{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }를 전달하여 값이 범위를 벗어나지 않도록 합니다.spring({ frame, fps, config })- 물리 기반 0→1(약간 오버슈트). 살아있는 느낌이 필요한 모든 것(팝, 슬라이드, 스케일 인)에 사용하세요.AbsoluteFill- 전체 프레임 절대 위치 div; 모든 장면의 기본 레이어.Sequence- 자식의 시간 이동 및 시간 제한(장면 스케줄러).Series/Series.Sequence-from오프셋을 수동으로 계산하지 않고 연속 장면.Img,staticFile- 6단계 참조.
4단계 - spring() vs interpolate()
spring()을 사용해야 할 때… | interpolate()를 사용해야 할 때… |
|---|---|
| 물리적 느낌이 필요한 동작 - 팝, 바운스, 스케일 인, 슬라이드 인 | 정확하고 선형적인 제어가 필요한 경우 - 특정 프레임 간 |
| 자연스러운 안정/오버슈트가 필요한 경우 | 페이드(불투명도 0→1), 색상 변화, 진행률 표시줄, 프레임 정확 타이밍이 필요한 모든 것 |
| 카드, 로고, 배지의 진입 | 크로스페이드 및 종료 - 오버슈트가 어색하게 보일 수 있는 경우 |
패턴 - spring으로 변환을, interpolate로 불투명도를 구동합니다:
const { fps } = useVideoConfig();
const frame = useCurrentFrame();
const pop = spring({ frame, fps, config: { damping: 200 } }); // 0 → 1, 활발하게
const opacity = interpolate(frame, [0, 15], [0, 1], { extrapolateRight: "clamp" });
const scale = interpolate(pop, [0, 1], [0.8, 1]);
damping: 200은 작업용 구성입니다 - 빠르게 안정되고 거의 오버슈트가 없습니다. 낮은 damping(~10-20)은 눈에 띄게 바운스됩니다. 바운스가 목적인 경우에만 사용하세요.
5단계 - 표준 제품 데모 구조
브리프에서 달리 명시하지 않는 한 이 형태를 인코딩하세요: 로고/브랜드 소개 → N개 기능 호출 → CTA 아웃트로. 데이터 기반(6단계)으로 만들어 변형이 자유롭게 가능하도록 하세요.
// src/compositions/ProductDemo.tsx
import {
AbsoluteFill, Sequence, useCurrentFrame, useVideoConfig,
interpolate, spring,
} from "remotion";
import { loadFont } from "@remotion/google-fonts/Inter";
const { fontFamily } = loadFont();
export type Feature = { title: string; body: string };
export type ProductDemoProps = {
brand: string;
colors: { bg: string; fg: string; accent: string };
logoText: string;
features: Feature[];
cta: string;
};
const Intro: React.FC<{ text: string; colors: ProductDemoProps["colors"] }> = ({ text, colors }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const s = spring({ frame, fps, config: { damping: 200 } });
const scale = interpolate(s, [0, 1], [0.7, 1]);
const opacity = interpolate(frame, [0, 12], [0, 1], { extrapolateRight: "clamp" });
return (
<AbsoluteFill style={{ backgroundColor: colors.bg, justifyContent: "center", alignItems: "center" }}>
<h1 style={{ fontFamily, fontSize: 120, color: colors.fg, transform: `scale(${scale})`, opacity }}>
{text}
</h1>
</AbsoluteFill>
);
};
const FeatureCallout: React.FC<{ feature: Feature; colors: ProductDemoProps["colors"] }> = ({ feature, colors }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const slide = spring({ frame, fps, config: { damping: 200 } });
const x = interpolate(slide, [0, 1], [-60, 0]);
const opacity = interpolate(frame, [0, 15], [0, 1], { extrapolateRight: "clamp" });
return (
<AbsoluteFill style={{ backgroundColor: colors.bg, justifyContent: "center", paddingLeft: 160 }}>
<div style={{ transform: `translateX(${x}px)`, opacity }}>
<div style={{ fontFamily, fontSize: 32, color: colors.accent, fontWeight: 700, letterSpacing: 2 }}>
{feature.title.toUpperCase()}
</div>
<div style={{ fontFamily, fontSize: 64, color: colors.fg, maxWidth: 1100, marginTop: 16 }}>
{feature.body}
</div>
</div>
</AbsoluteFill>
);
};
const Outro: React.FC<{ cta: string; colors: ProductDemoProps["colors"] }> = ({ cta, colors }) => {
const frame = useCurrentFrame();
const opacity = interpolate(frame, [0, 18], [0, 1], { extrapolateRight: "clamp" });
return (
<AbsoluteFill style={{ backgroundColor: colors.accent, justifyContent: "center", alignItems: "center" }}>
<h2 style={{ fontFamily, fontSize: 84, color: colors.bg, opacity }}>{cta}</h2>
</AbsoluteFill>
);
};
export const ProductDemo: React.FC<ProductDemoProps> = ({ colors, logoText, features, cta }) => {
const intro = 90;
const per = 150;
const outro = 60;
return (
<AbsoluteFill>
<Sequence durationInFrames={intro}>
<Intro text={logoText} colors={colors} />
</Sequence>
{features.map((f, i) => (
<Sequence key={i} from={intro + i * per} durationInFrames={per}>
<FeatureCallout feature={f} colors={colors} />
</Sequence>
))}
<Sequence from={intro + features.length * per} durationInFrames={outro}>
<Outro cta={cta} colors={colors} />
</Sequence>
</AbsoluteFill>
);
};
src/Root.tsx에 등록하고, 동일한 숫자로 durationInFrames를 계산하고 defaultProps를 제공하세요:
<Composition
id="ProductDemo"
component={ProductDemo}
durationInFrames={90 + 3 * 150 + 60}
fps={30}
width={1920}
height={1080}
defaultProps={{
brand: "Acme",
colors: { bg: "#0B0B0F", fg: "#FFFFFF", accent: "#6C5CE7" },
logoText: "Acme",
features: [
{ title: "Fast", body: "Ship in seconds, not sprints." },
{ title: "Simple", body: "One prompt, one video." },
{ title: "Yours", body: "Your brand, your copy." },
],
cta: "Start free →",
}}
/>
세로/소셜의 경우 width={1080} height={1920}로 설정하고, 정사각형의 경우 1080×1080으로 설정합니다. 컴포넌트는 동일하게 유지하고 캔버스만 변경됩니다.
6단계 - 데이터 기반 props 및 정적 에셋
위의 컴포지션은 완전히 데이터 기반입니다. 모든 단어와 색상은 prop입니다. 이것이 핵심입니다 - 변형(새로운 카피, 새로운 팔레트, 지역화된 텍스트)은 재프롬프트가 필요 없으며 다른 props를 전달하기만 하면 됩니다 (remotion-render는 배치 처리를 위해 JSON 파일에서 이 작업을 수행합니다).
정적 에셋(로고, 스크린샷, 오디오)은 public/assets/에 있으며 staticFile()로 참조됩니다 - import나 raw 경로를 사용하지 마세요:
import { Img, staticFile } from "remotion";
<Img src={staticFile("assets/logo.png")} />
7단계 - 출력
완성된 파일을 src/compositions/<VideoName>.tsx에 저장하고 src/Root.tsx에 등록한 후 사용자에게 npx remotion studio(http://localhost:3000)로 미리보기를 안내하세요. 그런 다음 인계하세요: "Studio에서 스크러빙해 보세요. 변경할 점이 있으면 말씀해 주세요 - 너무 빠르다, 색상이 잘못되었다, 다른 카피가 필요하다 등 - 그러면 제가 수정하고 다시 렌더링하겠습니다 (remotion-render)."
금지 사항
- CSS 전환/애니메이션이나
setTimeout/setInterval로 애니메이션화하지 마세요. Remotion은 프레임을 독립적으로 병렬 렌더링합니다.useCurrentFrame()에서 파생되지 않은 것은 브라우저 미리보기에서는 잘 보여도 최종 렌더링에서는 보이지 않거나 일관성이 없습니다. - 분산/입자 효과에
Math.random()을 사용하지 마세요 - 각 렌더링 워커가 다른 값을 얻어 프레임이 일치하지 않습니다. 결정론적 의사 난수성을 위해 Remotion의random(seed)를 사용하세요. delayRender()/continueRender()없이 데이터를 가져오거나 비동기 작업을 기다리지 마세요 - 렌더러는 데이터가 도착하기 전에 프레임을 캡처합니다.- 장면 컴포넌트 내부에 fps, width, height를 하드코딩하지 마세요 -
useVideoConfig()에서 읽어서 30↔60fps 또는 가로↔세로 전환이 타이밍과 레이아웃을 깨지 않도록 하세요. - 진입/종료 보간에서
extrapolateLeft/Right: "clamp"를 생략하지 마세요 - 불투명도가 1을 초과하고 요소가 범위를 벗어나 화면 밖으로 이동합니다. - 헤드라인, 기능 주장 또는 CTA를 지어내지 마세요 - 사용자의 실제 카피를 사용하고, 누락된 경우 물어보세요.
품질 기준
- 모든 애니메이션 값은
useCurrentFrame()(및 props)의 순수 함수입니다 - Studio에서 뒤로 스크러빙해도 앞으로 재생하는 것과 동일하게 보입니다. - 계획의 프레임 예산은
Root.tsx의durationInFrames와 정확히 일치하며, 텍스트 비트가 읽을 수 있는 최소 시간보다 적게 유지되지 않습니다. - 모든 카피와 색상은
defaultProps가 제공된 타입 props를 통해 전달됩니다 - 장면 컴포넌트에 리터럴이 포함되지 않습니다. - 에셋은
staticFile()을 통해 로드됩니다. 파일이 깔끔하게 등록되고 Studio에서 콘솔 오류 없이 미리보기가 가능합니다.