demo-reel은(는) 무엇을 하나요?
웹 앱을 스크립트로 워크스루하여 실제 화면 녹화처럼 보이는 비디오로 만듭니다: 부드러운 60fps 모션, 하이라이트가 있는 가시적 커서, 클릭 웨이브, 캡션. 이 도구는 실제 앱을 헤드리스 Chromium에서 구동합니다 — 푸티지는 제품 자체이며 모형이 아닙니다.
비디오가 부드러운 이유
일반 화면 녹화는 브라우저가 페인트를 할 때마다 프레임을 캡처합니다. 느린 머신(또는 소프트웨어 WebGL)에서는 약 10–15fps이고 끊깁니다. 대신 demo-reel은 가상 시계(scripts/inject.js)를 주입합니다: requestAnimationFrame, performance.now 및 Date는 리코더가 지정할 때만 진행되며, CSS/웹 애니메이션은 document.getAnimations()를 통해 단계적으로 진행됩니다. 각 프레임은 정확히 1/fps만큼 진행되고 스크린샷으로 캡처된 후 ffmpeg로 전달됩니다. 프레임 렌더링에 시간이 얼마나 걸리든 결과는 완벽하게 부드럽게 재생됩니다.
타이머(setTimeout/setInterval)는 의도적으로 실제 시간을 유지합니다. 이를 가짜로 만들면 로더와 스케줄러의 제로-딜레이 타이머 체인이 영원히 회전합니다(이것이 Playwright 자체 page.clock이 많은 WebGL 페이지에서 멈추는 이유입니다).
요구 사항
- Node 18+, ffmpeg가 PATH에 있어야 함.
- 이 스킬 디렉토리에서 한 번 실행:
npm install && npx playwright install chromium
워크플로우
- 앱을 제공합니다 사용자가 보는 방식으로 — 이상적으로는 프로덕션 빌드(
vite build && vite preview,next build && next start)를 로컬 또는 모의 데이터와 함께 사용합니다. 개발 서버도 작동하지만 느리고 개발 오버레이가 표시될 수 있습니다. 실제 또는 민감한 데이터를 기록하지 마십시오 — 데모 데이터를 시드하세요. - 시나리오를 작성합니다 (
*.scenario.mjs, 아래 API 참조).examples/tasks.scenario.mjs에서 시작하세요. 이야기로 계획하세요: 20–60초, 캡션당 하나의 아이디어, 각 결과 후 1–2초 머뭄으로 시청자가 읽을 수 있게 합니다. - 녹화:
node scripts/record.mjs my.scenario.mjs --out demo.mp41080p에서 60초 클립은 대략 3–10분이 걸립니다(무거운 WebGL 페이지가 가장 느림). 백그라운드에서 실행하세요. - QA:
scripts/sheet.sh demo.mp4는 접촉 시트(2.5초마다 한 프레임)를 작성합니다. 확인하세요: 캡션이 화면의 내용과 일치하는지, 로딩 스피너가 없는지, 캡션에 가려진 부분이 없는지, 배지/커서가 보이는지. 연속된 프레임 몇 개를 추출하여 부드러움을 점검하세요 — 움직임 중에는 각 프레임이 약간씩 달라야 합니다. - 전달 ("비디오 사용" 참조).
시나리오 API
export default {
size: "1920x1080", // 뷰포트 = 비디오 크기
fps: 60,
theme: { captionPosition: "bottom", haloFill: "rgba(250,204,21,.30)" }, // 선택 사항
async run(d) {
await d.open("http://localhost:4173/"); // 탐색 + 안정화 (기록되지 않음)
d.badge("NEW", "#15803d"); // 모서리 배지 (null이면 숨김)
d.caption("새로운 대시보드"); // 캡션 필 (null이면 숨김)
await d.hold(1500); // 1.5초 그대로 기록
await d.click(d.page.getByRole("button", { name: "Filters" }));
d.caption("필터는 하나의 패널에 있습니다"); // 작업 후에 캡션 설정
await d.hold(1200);
await d.type("Stockholm"); // 사람 속도로 타이핑
await d.press("Enter");
await d.scroll(600); // 부드러운 휠 스크롤
await d.step("선택적 부분", async () => { /* 실패는 기록, 녹화는 계속 */ });
},
};
| 호출 | 기능 |
|---|---|
d.open(url, { settle }) | 탐색, 페이지 안정화 (기록되지 않음, 기본값 2500ms) |
d.hold(ms) | 현재 페이지를 그대로 기록 |
d.settle(ms) | 시간을 기록 없이 진행 — 로딩 상태, 지연 청크, 보여주고 싶지 않은 레이아웃 애니메이션 건너뛰기 |
d.move(target, { ms }) / d.hover | 커서를 대상으로 (이즈 적용되어) 이동 |
d.click(target, { ms, button }) | 이동, 웨이브와 함께 클릭. button: "right"는 컨텍스트 메뉴용 |
d.doubleClick(target) | 더블 클릭 |
d.type(text, { delay }) | 한 번에 한 키씩 타이핑 (기본값 90ms/키) |
d.press(key) | 키보드 단축키, 예: "Escape", "Control+K" |
d.scroll(dy, { ms }) | 부드러운 휠 스크롤 |
d.caption(text) / d.badge(text, color) / d.cursor(bool) | 오버레이 상태; 전체 페이지 탐색 후에도 유지 |
d.step(name, fn) | 이름이 있는 단계; 오류는 기록되지만 치명적이지 않음 |
d.page, d.point(target), d.width, d.height | 이스케이프 해치 |
대상은 Playwright Locator, CSS 선택자 문자열, { x, y }, 또는 비동기 함수 (page) => ({ x, y })일 수 있습니다.
Canvas 렌더링 UI(그래프, 지도, 차트)는 항목별 DOM 요소가 없습니다 — 라이브러리에 화면 좌표를 요청하는 함수를 전달하세요. Cytoscape.js 예:
const node = (label) => (page) => page.evaluate((label) => {
const el = [...document.querySelectorAll("div")].find((e) => e._cyreg?.cy);
const n = el._cyreg.cy.nodes().filter((n) => n.data("label") === label)[0];
const r = el.getBoundingClientRect(), p = n.renderedPosition();
return { x: r.left + p.x, y: r.top + p.y };
}, label);
await d.click(node("Server A"));
좋은 데모 작성하기
- 캡션은 진실을 말해야 합니다. 푸티지가 보여주는 것만 주장하세요. 보여주고 싶은 버그가 촬영본에서 재현되지 않으면 캡션을 변경하세요 — 유지하지 마세요.
- 작업 후에 캡션을 설정하고, 전에 하지 마세요 — 그렇지 않으면 다음 화면용 캡션이 이전 화면 위에 나타납니다.
- 결과를 보여주고 클릭을 보여주지 마세요. 각 결과에서 1–2초 머물고,
d.settle로 로딩을 잘라내세요. - Before/After: 동일한 이야기와 단계로 두 시나리오를 기록하고, 배지
BEFORE/AFTER(빨강/초록)를 사용하며, 별도 파일과 선택적 연결본을 유지하세요. - 히어로 순간(랜딩 페이지, 애니메이션): 5–10초를 주세요 — 시청자가 받아들일 시간이 필요합니다.
- 커서를 조용히 유지: 이동당 600–900ms, 머무는 동안 콘텐츠에서 멀리 두세요.
- 실제 타이머는 프레임 렌더링 중(~50–300ms 실시간/프레임) 계속 실행되므로, 타이머 기반 UI(토스트, 디바운스된 검색, 자동 사라짐 툴팁)는 실제보다 비디오에서 더 빨리 사라질 수 있습니다. 화면에 표시되기 직전에 트리거하거나 덜 머무세요.
비디오 사용
- 파워포인트: 삽입 → 비디오 → 이 장치. 재생 탭: 시작: 자동 및 중지될 때까지 반복. H.264
yuv420pMP4(demo-reel이 작성하는 형식)는 Windows와 macOS에서 재생됩니다. - 키노트: 드래그 앤 드롭; 동영상 → 반복: 반복.
- 웹:
<video src="demo.mp4" autoplay loop muted playsinline></video>. - GIF(README, 채팅):
ffmpeg -i demo.mp4 -vf "fps=20,scale=960:-1:flags=lanczos,split[a][b];[a]palettegen[p];[b][p]paletteuse" demo.gif - 같은 크기/fps의 클립 연결:
printf "file 'a.mp4'\nfile 'b.mp4'\n" > l.txt && ffmpeg -f concat -safe 0 -i l.txt -c copy both.mp4
문제 해결
| 증상 | 해결 방법 |
|---|---|
| 빈 / "로딩 중…" 프레임 | d.open에서 더 긴 settle, 또는 탐색 클릭 후 await d.settle(1500) |
| 대상이 보이지 않음 | 화면 밖이거나 닫힌 메뉴 안에 있음 — 먼저 열거나 스크롤; 캔버스 항목은 포인트 함수 사용 |
| WebGL/3D가 검게 렌더링됨 | 기본 실행 플래그 유지(--use-angle=swiftshader); 느리지만(~0.25s/프레임) 올바름 |
| 비디오가 계획보다 너무 짧거나 긺 | 지속 시간은 정확함: hold + 이동의 합계. settle 시간은 기록되지 않음 |
| 파워포인트에서 색상이 바래 보임 | -pix_fmt yuv420p 없이 재인코딩하지 마세요; demo-reel의 출력은 이미 올바름 |
| 텍스트가 약간 흐림 | --png는 무손실 프레임을 캡처(느리지만 더 큼) |