제품 데모 비디오은(는) 무엇을 하나요?
정적 제품 스크린샷이나 디자인을 세련된 애니메이션 데모로 변환합니다 — 화면 녹화기, 떨리는 실제 커서, 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배가 적정선 — 충분한 디테일, 맥락 유지 |
| 기능 캡션 | 확대와 함께 유지 | "원클릭 내보내기"와 같은 한 가지 혜택 문구 |
카메라 또는 커서를 움직이되, 거의 동시에 움직이지 마세요. 두 가지가 동시에 움직이면 주의가 분산됩니다.
스크린샷 프레이밍
원시 스크린샷은 버그 리포트처럼 보이지만, 프레임이 씌워진 스크린샷은 제품처럼 보입니다. 모든 화면을 브랜드 배경의 브라우저 또는 기기 크롬으로 감쌉니다.
// 브라우저 프레임 — 스크린샷을 넣으면 그럴듯한 창이 됨
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";
// `from`에서 `to`로 `startFrame`과 `startFrame+dur` 사이에서 부드럽게 이동
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(실제 스크린샷 경로)를 사용하여 프레임 정확한 스틸 생성
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 # 초점 영역으로 확대 + 캡션
# 종료 프레임 = durationInFrames - 1 (npx remotion compositions가 읽어옴)
각 PNG를 충실도(올바른 스크린샷 로드됨; 커서가 정확한 픽셀에 있음; 클릭 리플과 상태 변경이 같은 프레임에 있음; 캡션 텍스트가 올바름) AND 아티팩트(이미지가 비어 있거나 로드되지 않음, 화면이 캔버스 밖에 있음, 확대/축소가 픽셀화되거나 프레임을 벗어남, 캡션이 타이틀 안전 하단 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번이 아닌 한 번에 잡을 수 있습니다.
마치기 전에:
- 스틸이 settle / click / end 프레임에서 오류 없이 깨끗하게 렌더링되고, 스크린샷이 실제로 로드되었는지(비어 있지 않은지) 확인.
- 커서가 정확한 대상에 도달; 리플 + 상태 변경이 같은 프레임에 발생; 캡션 텍스트가 올바르고 타이틀 안전 하단 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) 안전 영역 참고 사항 포함).