제품 데모 비디오
정적인 제품 스크린샷이나 디자인 파일을 세련되고 애니메이션된 데모로 변환합니다 — 화면 녹화기, 불안정한 실제 커서, UI 버전 차이 없이. 각 화면을 프레임으로 감싸고 합성 커서를 정확한 픽셀로 부드럽게 이동시킨 후 클릭 리플을 표시하고, 중요한 부분으로 확대하여 기능을 캡션으로 설명합니다. 입력이 정적이고 움직임이 코드이기 때문에 모든 값은 결정론적이며 스크린샷이 변경되어도 동일하게 재렌더링됩니다.
사용 시기
- 앱 / SaaS 데모, 기능 발표, 온보딩 둘러보기 (15–90초).
- 스크린샷이나 디자인만 있는 UI의 클릭 경로를 보여줄 때.
- 복잡한 대시보드의 한 초점 영역을 강조할 때 (영역 확대 + 콜아웃).
스크린샷이 화면 녹화보다 나은 이유
| 화면 녹화 | 스크린샷 기반 (이 스킬) |
|---|---|
| 실제 커서가 떨리고, 목표를 지나치거나 흔들림 | 커서가 부드러운 경로로 정확한 대상으로 이동 |
| 한 단계를 수정하려면 전체를 다시 녹화해야 함 | PNG 하나를 바꾸고 동일한 코드를 재렌더링 |
| 녹화 중 UI가 변경되면 마우스가 헤맴 | 각 화면은 깔끔한 최종 상태 에셋 |
| 픽셀화 없이 확대하기 어려움 | 스크린샷을 2배(DPR 2)로 내보내면 선명한 확대 가능 |
입력을 디스플레이 크기의 두 배(레티나 / deviceScaleFactor: 2)로 캡처하여 2배 확대해도 선명하게 유지됩니다. Playwright 캡처 스크립트는 references/screenshot-demo.md를 참조하세요.
데모 루프
모든 데모는 기능별로 동일한 비트를 반복합니다. 읽을 수 있을 만큼 충분히 유지하고, 의도적으로 움직이며, 클릭을 정확히 수행하고, 결과를 보여줍니다.
| 비트 | 시간 | 역할 |
|---|---|---|
| 화면 안정화 | 0.5–1초 | 시청자가 움직이기 전에 방향을 잡도록 함 |
| 커서가 대상으로 이동 | 0.4–0.8초 | 부드러운 이동 (ease-in-out), 직선이 아님 |
| 클릭 리플 + 상태 변경 | 0.2–0.3초 | 촉각적 피드백; 동일한 프레임에서 화면 반응 |
| 초점 영역으로 확대 | 1–2초 진입, 2–4초 유지 | 2배가 최적 — 충분한 디테일, 맥락 유지 |
| 기능 캡션 | 확대와 함께 유지 | 하나의 혜택 문구, 예: "원클릭 내보내기" |
카메라 또는 커서를 움직이되, 동시에 움직이는 경우는 드물게 하세요 — 두 가지 동시 움직임은 주의를 분산시킵니다.
스크린샷 프레임 처리
날 스크린샷은 버그 리포트처럼 보이고, 프레임이 있는 것은 제품처럼 보입니다. 모든 화면을 브랜드 배경의 브라우저 또는 기기 크롬으로 감싸세요.
// 브라우저 프레임 — 스크린샷을 넣으면 신뢰할 수 있는 창이 됩니다
const BrowserFrame = ({ url, children }) => (
<div style={{ borderRadius: 12, overflow: "hidden", background: "#1e1e22",
boxShadow: "0 40px 80px rgba(0,0,0,.35)" }}>
<div style={{ height: 40, display: "flex", alignItems: "center", gap: 8, padding: "0 14px" }}>
{["#ff5f57", "#febc2e", "#28c840"].map((c) => (
<span key={c} style={{ width: 12, height: 12, borderRadius: "50%", background: c }} />
))}
<div style={{ marginLeft: 12, flex: 1, height: 24, borderRadius: 6, background: "#2b2b30",
color: "#9aa", fontSize: 13, display: "flex", alignItems: "center", padding: "0 10px" }}>
{url}
</div>
</div>
<div style={{ background: "#fff" }}>{children}</div>
</div>
);
주소 표시줄에 실제 URL을 넣고(진정성), 부드러운 그림자와 그라데이션 배경을 추가하세요. 모바일의 경우 노치와 둥근 모서리가 있는 폰 프레임으로 바꾸세요. 모든 화면이 일치하도록 프레임 스타일을 하나의 테마 객체에 유지하세요.
애니메이션 커서 + 클릭 리플 (핵심 동작)
CSS 전환이나 벽시계가 아닌 현재 프레임에서 위치를 구동하세요 — 비디오 렌더러는 모든 프레임이 프레임 번호의 순수 함수여야 합니다. 그렇지 않으면 커서가 동기화되지 않고 깜빡입니다.
import { useCurrentFrame, interpolate, spring, useVideoConfig } from "remotion";
// `startFrame`과 `startFrame+dur` 사이에서 `from`에서 `to`로 부드럽게 이동
const useCursor = (from, to, startFrame, dur) => {
const frame = useCurrentFrame();
const t = interpolate(frame, [startFrame, startFrame + dur], [0, 1],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: (x) => x * x * (3 - 2 * x) }); // smoothstep
return { x: from.x + (to.x - from.x) * t, y: from.y + (to.y - from.y) * t };
};
export const Cursor = ({ from, to, startFrame, dur, clickAt }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const { x, y } = useCursor(from, to, startFrame, dur);
// 리플은 클릭 프레임에서 한 번만 확장됩니다
const since = frame - clickAt;
const showRipple = since >= 0 && since < 18;
const r = spring({ frame: since, fps, config: { damping: 18 }, durationInFrames: 18 });
return (
<>
{showRipple && (
<div style={{ position: "absolute", left: x, top: y, transform: "translate(-50%,-50%)",
width: 12 + r * 60, height: 12 + r * 60, borderRadius: "50%",
border: "2px solid rgba(80,140,255,.9)", opacity: 1 - r }} />
)}
<svg width="28" height="28" viewBox="0 0 28 28"
style={{ position: "absolute", left: x, top: y, filter: "drop-shadow(0 2px 3px rgba(0,0,0,.4))" }}>
<path d="M3 2 L3 21 L9 15 L13 24 L16 23 L12 14 L20 14 Z" fill="#fff" stroke="#222" strokeWidth="1.4" />
</svg>
</>
);
};
커서는 smoothstep을 사용하여 정지 상태에서 가속하고 대상으로 감속합니다(직선 커서는 로봇처럼 읽힙니다). 동일한 clickAt 프레임에서 리플 그리고 화면의 상태 변경을 실행하여 원인과 결과가 일치하도록 하세요. 여러 화면에 걸쳐 여러 클릭을 연결하는 전체 구성은 references/screenshot-demo.md를 참조하세요.
영역으로 확대 (UI용 Ken Burns 효과)
초점 요소로 확대하려면 화면을 확장하고 transform-origin을 해당 요소의 중심으로 설정하여 프레임 중심 대신 그 요소에 수렴하도록 합니다.
const ZoomToRegion = ({ target, scale = 2, startFrame, dur, children }) => {
const frame = useCurrentFrame();
const s = interpolate(frame, [startFrame, startFrame + dur], [1, scale],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: (x) => 1 - Math.pow(1 - x, 3) }); // easeOutCubic
return (
<div style={{ transform: `scale(${s})`, transformOrigin: `${target.x}px ${target.y}px`,
willChange: "transform" }}>
{children}
</div>
);
};
확대는 약 2배로 제한하세요(3배 이상은 맥락을 잃고 방향 감각을 잃게 합니다). 확대된 상태를 2–4초 유지 — 세부 정보를 읽을 수 있을 만큼 충분히 — 그런 다음 다음 화면으로 이동하기 전에 부드럽게 다시 축소합니다. 확대를 콜아웃과 함께 사용하고, 절대로 단독으로 사용하지 마세요.
콜아웃 주석 및 기능 캡션
변곡점에 주석을 달고, 그런 다음 지우세요. 콜아웃은 레이블 + 초점 요소를 가리키는 커넥터(화살표, 원, 또는 밑줄)입니다.
- 부드러운 링이나 어두워진 주변으로 요소를 강조합니다(
box-shadow: 0 0 0 9999px rgba(0,0,0,.45)는 스포트라이트 구멍을 만듭니다). - 비트당 하나의 혜택을 캡션으로 표시합니다 — 짧은 명사구("실시간 분석"), 문장이 아닙니다. 확대 시 들어가고, 다음 단계 전에 나갑니다.
- 캡션을 하단 1/3 지점과 타이틀 안전 영역 내에 유지하여 9:16으로 잘라도 살아남습니다.
스포트라이트 마스크, 화살표 커넥터, 타자기 효과 기능 캡션 트랙에 대해서는 references/annotation-and-transitions.md를 참조하세요.
화면 간 전환
단계 사이에 하나의 일관된 언어로 화면을 이동합니다. 하나를 선택하고 유지하세요:
| 전환 | 느낌 | 용도 |
|---|---|---|
| 푸시 / 슬라이드 | "다음 단계" 전진 동작 | 선형 둘러보기 |
| 크로스줌 | 운동량, 앱 같음 | 세부 보기로 점프 |
| 크로스페이드 | 차분함, 중립적 | 컨텍스트 전환 (설정 → 대시보드) |
| 공유 요소 매치 컷 | 매끄러움 | 동일한 카드가 화면 전체에 지속됨 |
나가는 화면의 클릭이 해결될 때까지 기다렸다가 전환하세요 — 절대로 움직임 중간에 자르지 마세요. references/annotation-and-transitions.md를 참조하세요.
출력 체크리스트
- 입력이 2배(DPR 2)로 캡처됨; 2배 확대 시 선명하게 유지.
- 모든 커서/확대 값은
useCurrentFrame()의 순수 함수임 — 애니메이션 상태에 CSS 전환 없음. - 커서가 부드러운 경로(smoothstep)로 이동; 리플과 상태 변경이 동일한 클릭 프레임에서 실행됨.
- 각 화면이 브라우저/기기 프레임으로 처리되고 실제 URL과 일관된 테마를 가짐.
- 한 번에 하나의 움직임(카메라 또는 커서); 확대 ≤ 2배, 2–4초 유지.
- 비트당 하나의 혜택 캡션, 타이틀 안전 하단 1/3 지점에 위치; 전환은 하나의 일관된 언어 사용.
전달 및 검증 (렌더링된 스틸 → MP4)
패키징된 헬퍼 (
scripts/):scripts/contact-sheet.sh sheet.png f-hook.png f-mid.png f-end.png로 스틸을 나란히 배열한 다음scripts/probe-mp4.sh out.mp4 [WxH] [fps]로 인코딩을 확인하세요.scripts/README.md를 참조하세요.
이것은 무거운 작업입니다: 전달물은 MP4 파일이며 라이브 페이지가 아닙니다. 데모는 사용자 에셋(스크린샷/디자인, 선택적 로고)을 사용하므로 검증은 주로 올바른 이미지가 실제로 로드되어 프레임에 들어갔는지에 관한 것입니다.
출력 계약:
- 컴포지션이 등록된 Remotion 프로젝트 (
<Composition>+ zodschema+defaultProps); 모든 커서/확대/전환 값은useCurrentFrame()의 순수 함수 (CSS 전환, 타이머,Date.now()없음). staticFile()을 통해 로드된 스크린샷,delayRender/continueRender로 게이트되어 프레임이 렌더링되기 전에 2배 PNG(및 모든 웹 폰트)가 있는지 확인 — 그렇지 않으면 화면이 데모 중간에 빈 상태로 나타납니다.- 전달물 = 렌더링된
out/*.mp4+ 프로젝트 (스크린샷이 변경되면 재렌더링).
검증 루프 — 스틸 렌더링 → 검사 → 인코딩. 저렴한 PNG를 먼저, 비디오는 올바른 경우에만.
# 프레임 정확한 스틸, 실제 props(실제 스크린샷 경로) 사용, defaultProps가 아님
npx remotion still ProductDemo out/f-settle.png --frame=15 --props=demo.json # 화면 프레임 처리, 이동 전
npx remotion still ProductDemo out/f-click.png --frame=90 --props=demo.json # 커서가 대상에 위치 + 리플
npx remotion still ProductDemo out/f-zoom.png --frame=150 --props=demo.json # 초점 영역으로 확대 + 캡션
# end frame = durationInFrames - 1 (npx remotion compositions가 읽음)
각 PNG를 검사하여 정확도 (올바른 스크린샷 로드됨; 커서가 정확한 픽셀에 있음; 클릭 리플과 상태 변경이 동일한 프레임에 있음; 캡션 텍스트가 올바름) 및 아티팩트 (이미지가 비어 있거나 로드되지 않음, 화면이 캔버스 밖에 있음, 확대가 픽셀화되거나 프레임을 벗어남, 캡션이 타이틀 안전 하단 1/3 지점 밖에 있음, 잘못된 종횡비/레터박싱)를 확인하세요.
# 스틸이 깨끗한 후에만:
npx remotion render ProductDemo out/demo.mp4 --props=demo.json
npx remotion render ProductDemo out/demo.gif --props=demo.json --codec=gif # README 첫 화면 증명
일괄 처리 (하나의 템플릿, 여러 제품): 제품별로 데모 스킨을 변경할 때, 카탈로그를 일괄 렌더링하기 전에 대표 제품 하나의 props를 스틸로 검증하세요 — 빈 이미지 또는 캔버스 밖 버그를 N번이 아니라 한 번에 잡습니다.
마치기 전에:
- 스틸이 안정화/클릭/종료 지점에서 깨끗하게 렌더링됨 — 오류 없음, 스크린샷이 실제로 로드됨(비어 있지 않음).
- 커서가 정확한 대상에 위치함; 리플과 상태 변경이 동일한 프레임에 있음; 캡션 텍스트가 올바르고 타이틀 안전 하단 1/3 지점에 있음.
- 모든 모션이 프레임 기반 — CSS 전환 / 타이머 /
Date.now()/Math.random()없음. - 전달된 props (실제 스크린샷 경로)가 올바르게 렌더링됨, 단지
defaultProps가 아님. - 전체 MP4가 인코딩되어 재생됨; (선택 사항) README용 GIF 렌더링됨.
참조 파일
references/screenshot-demo.md— 완전히 실행 가능한 Remotion 데모: Playwright로 2배 스크린샷 캡처, 브라우저/폰 프레임 컴포넌트, 여러 화면에 걸쳐 커서를 이동하고 클릭 리플을 포함하는 다단계 컴포지션, 그리고 렌더링 명령어.references/annotation-and-transitions.md— 스포트라이트 마스크, 화살표/원 콜아웃 커넥터, 타자기 효과 기능 캡션 트랙, 그리고 프레임 기반 구현과 다중 종횡비(16:9 / 9:16) 안전 영역 노트가 포함된 네 가지 화면 간 전환.