Remotion 시각 제작 스킬
실제 SureCart 4.2 런칭 비디오 제작 경험에서 비롯되었습니다. 작성자: Chandresh Vaghanani
시작 전 — 2가지 질문(필수)
코드를 작성하기 전에 2가지 질문을 물어보세요. 간편하게 한 메시지, 두 질문으로 작성하세요. 사용자가 "그냥 시작해" 또는 "지금 시작해"라고 말해도 절대 건너뛰지 마세요. 이 10초가 몇 시간의 재작업을 방지합니다.
시작하기 전에 — 두 가지만 여쭤볼게요:
1. 배경은 밝은색 또는 어두운색인가요? (또는 웹사이트/스크린샷을 공유해주시면 제가 거기서 추출하겠습니다)
2. 제품 스크린샷이나 목업이 있으신가요? (전적으로 선택사항입니다 — 없어도 제작 가능합니다)
이것으로 끝입니다. 사용자가 더 자세한 정보를 제공하지 않는 한 추가 질문은 하지 마세요.
답변 처리 방법:
- 목업/스크린샷을 공유한 경우 — 배경색, 강조색, 폰트 스타일, UI 카피를 추출하세요. 이를 사용하세요.
- 웹사이트 URL을 공유한 경우 — 해당 사이트를 가져와서 동일하게 추출하세요. 추측하지 마세요.
- 밝음/어두움만 답한 경우 — 아래의 '정보 없음 대체 설정(No-Info Fallback Defaults)'을 진행하세요.
- 두 질문을 모두 건너뛴 경우 — 아래의 '정보 없음 대체 설정'을 진행하세요. 다시 묻지 마세요.
로고, 음악, 추가 색상 등 다른 자산은 제공되면 수락하되, 절대 진행을 막지 마세요.
정보 없음 대체 설정 (No-Info Fallback Defaults)
브랜드 정보를 사용할 수 없을 때, AI의 본능은 폰트 크기를 줄이고 패딩을 추가하는 것입니다. 비디오에서는 그 본능이 잘못되었습니다. 대신 다음을 적용하세요:
이 설정은 목업, 스크린샷 또는 웹사이트가 제공되지 않을 때 자동으로 적용됩니다. 브랜드 정보가 전혀 없어도 전문적인 결과를 보장합니다.
레이아웃
- 모든 장면의 AbsoluteFill에
justifyContent: "center"+alignItems: "center"— 예외 없음 - 콘텐츠를 실제 중앙보다 약간 위로 당기기 위해
marginTop: -50 - 목업/카드 너비: 1200px (상한선 사용 — 프레임을 채우세요)
- 컨테이너 패딩: 최대 40px — 그 이상 사용하지 마세요
- 콘텐츠는 1080px 높이의 최소 75% 를 차지해야 합니다. 그렇지 않으면 폰트 크기를 늘리거나 간격을 줄이세요.
폰트 크기(대체 — 상한선 기준)
| 요소 | 대체 크기 |
|---|---|
| 메인 헤드라인 | 140px |
| 서브 헤드라인 | 56px |
| 본문 / 설명 | 36px |
| 라벨 / 필 | 22px |
| 카드 콘텐츠 텍스트 | 28px |
색상(대체 팔레트 — 안전하고 전문적)
- 배경:
#0a0a0f(거의 검정, 중립) - 강조:
#6366f1(인디고 — 읽기 쉽고 현대적이며 제품 특정적이지 않음) - 텍스트:
#ffffff - 흐린 텍스트:
rgba(255,255,255,0.55) - 카드 배경:
rgba(255,255,255,0.06)
이것들은 시작점일 뿐입니다. 사용자가 브랜드 색상을 공유하는 즉시 완전히 교체하세요.
브랜드 정보 없을 때 하지 말아야 할 것
- 네온/사이버 색상을 사용하지 마세요 — 개인 미적 감각일 뿐, 브랜드와 일치하지 않습니다
paddingTop: 120을 사용하고 "중앙 정렬됨"이라고 하지 마세요 — 상단 정렬에 데드 스페이스가 있습니다- "깔끔해 보인다"고 헤드라인에
fontSize: 32를 선택하지 마세요 — 비디오에서 읽을 수 없습니다
규칙 #1 — 캔버스는 웹사이트가 아닙니다
AI가 생성한 Remotion 비디오에서 가장 흔한 실수입니다.
Remotion 캔버스는 1920×1080px입니다. 웹페이지처럼 디자인하지 말고 캔버스를 채우도록 디자인하세요.
⚠️ 이것들은 최소값입니다. 본문 텍스트에
fontSize: 18을 쓰는 것은 버그이며, 스타일 선택이 아닙니다. 최소값 열 아래의 값은 텍스트가 1080p 화면에서 읽을 수 없음을 의미합니다. 이 숫자들을 타입 오류처럼 취급하세요 — 진행하기 전에 반드시 수정해야 합니다.
| 요소 | 최소값(엄격한 하한선) | 권장 범위 |
|---|---|---|
| 메인 헤드라인 | 80px | 100–160px |
| 서브 헤드라인 | 40px | 48–72px |
| 본문 / 설명 | 28px | 32–40px |
| 라벨 / 필 / 배지 | 18px | 20–24px |
| 카드 콘텐츠 텍스트 | 22px | 24–32px |
규칙:
- 콘텐츠가 화면을 가득 채우는 느낌이 들어야 함
- 목업 카드:
width: 1100–1200px, 가운데 정렬 - 컨테이너 패딩: 최대 48px — 웹페이지처럼 80–120px가 아님
- 텍스트가 "편안함을 넘어서는 크기"로 느껴지면 — 비디오에는 적합할 가능성이 높음
규칙 #2 — 모든 애니메이션은 useCurrentFrame()을 사용해야 합니다
// 올바른 예
const frame = useCurrentFrame();
const opacity = interpolate(frame, [0, 20], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
// 금지 — 올바르게 렌더링되지 않음
// CSS: transition: opacity 1s ease
// Tailwind: animate-fade-in
// CSS @keyframes
명시적으로 경계 없는 값이 필요한 경우가 아니라면 모든 interpolate() 호출에 항상 extrapolateLeft: "clamp" 와 extrapolateRight: "clamp"를 포함하세요. 이것을 잊으면 값이 범위를 벗어나 미묘한 버그를 일으킵니다.
규칙 #3 — 결정론
// 금지 — 렌더링 패스마다 다른 값
const x = Math.random() * 100;
// 올바른 예 — 항상 동일한 값, 모든 프레임에서 동일
import { random } from "remotion";
const x = random("particle-x-1") * 100; // 문자열 시드 = 결정론적
규칙 #4 — 자산
import { staticFile } from "remotion";
import { Img } from "remotion"; // 네이티브 <img>가 아님
import { Audio } from "@remotion/media";
// public/ 폴더의 모든 항목에는 항상 staticFile() 사용
<Audio src={staticFile("bg-music.mp3")} />
<Img src={staticFile("logo.png")} /> // <Img>는 프레임 렌더링 전에 자산 로드를 보장
// 원격 URL은 staticFile()이 필요 없지만 CORS는 필요함
규칙 #5 — 부드러운 미리보기 재생
무거운 컴포넌트가 시작 시간 30프레임 전에 미리 로드되도록 <Sequence> 컴포넌트에 premountFor={30}을 추가하세요:
<Sequence from={90} durationInFrames={120} premountFor={30}>
<HeavyScene />
</Sequence>
이징 레시피
import { Easing } from "remotion";
// 선명한 UI 진입 (강한 ease-out, 오버슈트 없음) — 대부분의 진입 애니메이션
Easing.bezier(0.16, 1, 0.3, 1)
// 느린 에디토리얼 페이드 (균형 잡힌 ease-in-out) — 분위기 전환
Easing.bezier(0.45, 0, 0.55, 1)
// 장난기 있는 오버슈트 — 탱글탱글/재미있는 요소
Easing.bezier(0.34, 1.56, 0.64, 1)
// 스프링 — 바운스 없음 (목업 진입, 카드, UI)
spring({ frame, fps, config: { damping: 200 } })
// 스프링 — 약간의 바운스가 있는 스냅 (펀치감 있는 요소)
spring({ frame, fps, config: { damping: 20, stiffness: 200 } })
// 스프링 — 바운시/장난기
spring({ frame, fps, config: { damping: 8 } })
사전 레이아웃 게이트 — 장면 레이아웃을 작성하기 전에 실행
이것들은 제안이 아닙니다. 어떤 장면의 첫 번째 <div>를 작성하기 전에 세 가지 게이트를 모두 실행하세요. 게이트 중 하나라도 실패하면 레이아웃을 변경하세요 — 고정된 치수나 추가 콘텐츠로 패치하지 마세요.
게이트 1 — 캔버스 채우기 (양축, 필수)
너비 축:
dead_side = (1920 − content_width) / 2
≤ 300px이어야 함. > 300px인 경우 → 콘텐츠가 너무 좁습니다. 넓히거나 레이아웃을 변경하세요.
높이 축:
fill_h = content_height / 1080
≥ 0.80이어야 함. < 0.80인 경우 → 콘텐츠가 너무 짧습니다. 레이아웃 형태를 변경하세요.
⚠️ 높이가 실패할 경우, 카드나 컨테이너에 고정 높이를 설정하여 수정하지 마세요. 고정 높이는 카드 내부에 빈 공간을 생성합니다 — 데드 스페이스가 제거되지 않고 요소 내부로 이동합니다. 유일한 실제 해결책은 레이아웃 형태를 변경하는 것입니다(게이트 2).
실패 사례 — 3개의 짧은 기능 카드:
| 버전 | content_width | dead_side | content_height | fill_h | 결과 |
|---|---|---|---|---|---|
| v1: 3열 카드 (365px) | 1151px | 384px | 416px | 38% | ❌ 두 축 모두 실패 |
v2: 강제 height: 720px | 1542px | 189px | 864px | 80% | ✅ 너비, ❌ 시각적(카드 내 빈 공간) |
| v3: 가로 행 (1400px) | 1400px | 260px | 916px | 85% | ✅ 두 축 모두 통과 |
게이트 2 — 레이아웃 선택 결정 테이블
콘텐츠 양이 레이아웃 형태를 결정합니다. 코드를 작성하기 전에 이 테이블에서 레이아웃을 선택하세요.
| 항목 수 | 항목당 콘텐츠 | 올바른 레이아웃 | 이유 |
|---|---|---|---|
| 3–5개 항목 | 아이콘 + 이름 + 1줄 | 가로 행 (1400px 너비) | 짧은 콘텐츠가 세로로 쌓임 → 높이를 자연스럽게 채움 |
| 3개 항목 | 아이콘 + 이름 + 2–3줄 | 3열 카드 (최소 1100px 너비) | 카드 높이를 채울 충분한 콘텐츠 |
| 2개 항목 | 중요 | 나란히 큰 카드 또는 분할 화면 | |
| 1개 항목 | 무엇이든 | 전체 화면 히어로 |
콘텐츠에 대한 자연스러운 레이아웃이 게이트 1 후에 어느 축이든 임계값 미만으로 남으면 — 그 레이아웃은 잘못된 것입니다. 이 테이블에서 다른 행을 선택하세요.
미적 감각이나 기본 본능으로 레이아웃을 선택하지 마세요. 콘텐츠 적합성이 먼저입니다. 미적 감각은 그 다음입니다.
게이트 3 — 콘텐츠 밀도 제한
항목당 최대: 아이콘 + 이름 + 짧은 1줄. 엄격한 중단. 글머리 기호, 하위 섹션, 단락, 추가 설명 줄은 금지됩니다.
시청자는 장면당 약 5초를 봅니다. 그들은 1줄을 읽습니다. 4개의 글머리 기호를 읽지 않습니다.
콘텐츠가 부족하게 느껴지고 공간을 채우기 위해 더 추가하고 싶다면 — 이는 게이트 2가 레이아웃 형태가 잘못되었음을 알리는 것이지, 콘텐츠가 부족하다는 것이 아닙니다.
빈 공간은 크기(더 큰 폰트, 더 큰 아이콘, 항목 간 더 큰 간격)로 채우세요 — 절대 더 많은 콘텐츠로 채우지 마세요.
범용 장면 규칙
-
수직 정렬 —
justifyContent: "center"+marginTop: -40~-60(실제 중앙보다 약간 위 = 현대적이고 프리미엄 느낌. 패딩으로 상단 정렬하지 않음)❌ 잘못된 예 — 상단 정렬, 하단에 빈 공간 발생:
<AbsoluteFill style={{ display: "flex", flexDirection: "column", paddingTop: 110 }}> <h1>헤드라인</h1> <p>서브텍스트</p> {/* 하단에 큰 빈 공간 */} </AbsoluteFill>✅ 올바른 예 — 수직 중앙 정렬, 중간점보다 약간 위:
<AbsoluteFill style={{ display: "flex", flexDirection: "column", justifyContent: "center", alignItems: "center", marginTop: -50 }}> <h1>헤드라인</h1> <p>서브텍스트</p> {/* 콘텐츠가 프레임을 채움 — 데드 스페이스 없음 */} </AbsoluteFill> -
캔버스 채우기 — 이 장면을 작성하기 전에 사전 레이아웃 게이트를 실행하세요. 게이트 1은 너비(
dead_side ≤ 300px)와 높이(fill_h ≥ 0.80)를 함께 확인합니다. 게이트 2는 결정 테이블에서 레이아웃 형태를 선택합니다. 게이트 3은 항목당 콘텐츠를 제한합니다. 세 게이트 모두 위의 사전 레이아웃 게이트 섹션에 있습니다 — 이전에 있던 너비 전용 검사를 대체합니다. -
헤드라인 z-index — 헤드라인 컨테이너에 항상
zIndex: 20을 설정하여 목업이 텍스트와 겹치지 않도록 함 -
헤드라인 오버플로우 — 헤드라인 텍스트에 항상
whiteSpace: "nowrap"설정 (목업 뒤에서 줄 바꿈 방지) -
AbsoluteFill에
overflow: hidden금지 — 커서 및 SVG 요소를 자릅니다. 제거하거나 커서를 외부에 배치하세요. -
15프레임 유지 — 모든 장면은 전환이 시작되기 전에 완전히 보이는 유지 프레임을 최소 15개 가져야 합니다. 나가는 장면에서 애니메이션이 재생 중일 때 전환을 시작하지 마세요.
-
폰트 — 전체적으로 일관된 하나의 폰트를 사용하세요. 권장:
@remotion/google-fonts를 통한 Plus Jakarta Sans. 최대 사용 가능 두께: 800 (fontWeight: 900을 쓰면 800으로 렌더링됨). -
색상 — 최대 2–3개의 브랜드 색상. 하나의 어두운 배경, 하나의 기본 강조색, 텍스트용 흰색/흐린 흰색. 모든 장면에서 일관되게 사용.
타이포그래피 계층 구조
// 슈퍼타이틀 / 라벨 필
{ fontSize: 20, letterSpacing: "4px", textTransform: "uppercase", fontWeight: 600 }
// 메인 헤드라인
{ fontSize: 112, fontWeight: 800 } // 범위: 100–160px
// 서브텍스트 / 설명
{ fontSize: 36, opacity: 0.7 } // 범위: 32–40px
// 신뢰 라인 / 작은 배지
{ fontSize: 26, opacity: 0.5 } // 범위: 24–28px
폰트 설정
import { loadFont } from "@remotion/google-fonts/PlusJakartaSans";
// 최상위 레벨에서 호출 — 렌더링 전에 로드되어야 함
const { fontFamily } = loadFont("normal", {
weights: ["400", "600", "800"],
subsets: ["latin"],
});
다른 폰트의 경우: @remotion/google-fonts/FontName을 통해 설치하거나 .woff2 파일을 public/에 넣고 @remotion/fonts에서 loadFont를 사용하세요.
장면 구조 템플릿
모든 장면은 다음의 고정된 레이어 순서를 따릅니다(아래 → 위):
<AbsoluteFill style={{ background: brandBg }}>
{/* 배경 요소 (그라데이션, 패턴, 입자) */}
{/* 콘텐츠 영역 (헤드라인, 본문) — zIndex: 20 */}
{/* 목업 / 스크린샷 */}
{/* 오버레이 (커서, 장식용 SVG) */}
</AbsoluteFill>
주석 처리된 장면 예제
다음은 useCurrentFrame(), 캔버스 채우기, 적절한 간격 및 결정론적 애니메이션을 보여주는 완전한 주석 처리된 Remotion 장면입니다.
import { AbsoluteFill, useCurrentFrame, interpolate, Easing, spring, Img, staticFile } from "remotion";
import { loadFont } from "@remotion/google-fonts/PlusJakartaSans";
const { fontFamily } = loadFont("normal", { weights: ["400", "600", "800"], subsets: ["latin"] });
// 브랜드 색상 (사용자 입력에서 추출하거나 대체 사용)
const brandBg = "#0a0a0f";
const brandAccent = "#6366f1";
const brandText = "#ffffff";
export const HeroScene: React.FC = () => {
const frame = useCurrentFrame();
// 결정론적 페이드 인 (규칙 #2 및 #3)
const opacity = interpolate(frame, [0, 20], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
// 아래에서 위로 슬라이드 (약간의 스프링 오버슈트)
const slideUp = spring({
frame,
fps: 30,
config: { damping: 20, stiffness: 200 },
});
return (
<AbsoluteFill
style={{
backgroundColor: brandBg,
justifyContent: "center",
alignItems: "center",
marginTop: -50, // 시각적 중앙보다 약간 위
opacity,
}}
>
{/* 헤드라인 컨텐츠 — zIndex: 20 */}
<div style={{ zIndex: 20, textAlign: "center" }}>
<h1
style={{
fontFamily,
fontSize: 140, // 캔버스 채우기 — 규칙 #1
fontWeight: 800,
color: brandText,
whiteSpace: "nowrap",
margin: 0,
transform: `translateY(${(1 - slideUp) * 50}px)`,
}}
>
제품 출시
</h1>
<p
style={{
fontFamily,
fontSize: 40,
color: "rgba(255,255,255,0.7)",
margin: "10px 0 0 0",
}}
>
더 빠르게, 더 스마트하게
</p>
</div>
{/* 목업 이미지 */}
<Img
src={staticFile("product-mockup.png")}
style={{
width: 1200,
marginTop: 40,
}}
/>
</AbsoluteFill>
);
};
빠른 참조
- 캔버스: 1920×1080, 항상 채우기
- 헤드라인 최소값: 80px, 권장 100–160px
- 목업 너비: 1100–1200px
- 수직 정렬: 가운데 + marginTop -50
- 게이트 1: dead_side ≤ 300px, fill_h ≥ 0.80
- 게이트 2: 콘텐츠 양에 따라 결정 테이블에서 레이아웃 선택
- 게이트 3: 항목당 아이콘 + 이름 + 1줄, 그 이상 금지
- 애니메이션: useCurrentFrame() + interpolate() + clamp
- 결정론: Math.random() 금지, random("seed") 사용
- 자산: public/의 경우 staticFile(), 원격 URL은 CORS 필요
- 이징: UI 진입용 Crisp bezier(0.16,1,0.3,1)
- end 유지: 전환 전 15프레임 유지