Communitygithub.com

UI 데모 영상 레코더

Playwright를 사용하여 세련된 UI 데모 영상을 녹화합니다. 사용자가 웹 애플리케이션의 데모, 둘러보기, 화면 녹화 또는 튜토리얼 비디오 작성을 요청할 때 사용합니다. 보이는 커서, 자연스러운 템포, 전문적인 느낌의 WebM 영상을 생성합니다.

UI 데모 영상 레코더란 무엇인가요?

이 기술은 Playwright의 비디오 녹화 기능을 기반으로 하여 사용자의 브라우저 상호작용을 자연스럽고 전문적인 데모 영상으로 제작합니다. 주요 특징으로는 보이는 커서 오버레이, 부드러운 마우스 이동과 타이핑, 자막 바, 그리고 대시보드 패닝 등의 기능을 제공합니다. 이 기술은 **발견 -> 예행연습 -> 녹화**의 3단계 프로세스를 따르며, 실제 녹화 전에 선택자가 올바르게 동작하는지 검증하여 녹화 실패를 방지합니다. 주로 Playwright를 지원하는 환경(Claude Code, Cursor 등)에서 사용되며, 문서, 온보딩, 또는 이해관계자 프레젠테이션을 위한 고품질 데모 비디오를 제작해야 할 때 유용합니다. 스토리텔링 흐름(로그인, 컨텍스트, 작업, 변형, 결과)을 따르고, 각 단계 간의 템포(예: 버튼 클릭 후 2초, 최종 동작 후 3초)를 조절하여 프로페셔널한 느낌을 줍니다.

지원 대상~Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/affaan-m/everything-claude-code/tree/main/skills/ui-demo

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

UI 데모 영상 레코더은(는) 무엇을 하나요?

Playwright의 비디오 녹화와 커서 오버레이, 자연스러운 템포, 스토리텔링 흐름을 결합하여 웹 애플리케이션의 세련된 데모 영상을 녹화합니다.

언제 사용하나요

  • 사용자가 "데모 영상", "화면 녹화", "둘러보기", "튜토리얼"을 요청할 때
  • 사용자가 기능이나 워크플로우를 시각적으로 보여주고 싶을 때
  • 문서, 온보딩, 또는 이해관계자 프레젠테이션을 위한 비디오가 필요할 때

3단계 프로세스

모든 데모는 발견 -> 예행연습 -> 녹화의 세 단계를 거칩니다. 절대로 바로 녹화 단계로 넘어가지 마세요.


1단계: 발견

스크립트를 작성하기 전에 먼저 대상 페이지를 탐색하여 실제로 무엇이 있는지 이해합니다.

이유

직접 보지 않고 스크립트를 작성할 수 없습니다. 필드는 <input>이 아닌 <textarea>일 수 있고, 드롭다운은 <select>가 아닌 커스텀 컴포넌트일 수 있으며, 댓글 상자는 @멘션이나 #태그를 지원할 수 있습니다. 가정은 조용히 녹화를 망칩니다.

방법

플로우의 각 페이지로 이동하여 상호작용 요소를 덤프합니다:

// 플로우의 각 페이지에서 데모 스크립트를 작성하기 전에 실행하세요
const fields = await page.evaluate(() => {
  const els = [];
  document.querySelectorAll('input, select, textarea, button, [contenteditable]').forEach(el => {
    if (el.offsetParent !== null) {
      els.push({
        tag: el.tagName,
        type: el.type || '',
        name: el.name || '',
        placeholder: el.placeholder || '',
        text: el.textContent?.trim().substring(0, 40) || '',
        contentEditable: el.contentEditable === 'true',
        role: el.getAttribute('role') || '',
      });
    }
  });
  return els;
});
console.log(JSON.stringify(fields, null, 2));

무엇을 찾아야 하나요

  • 양식 필드: <select>, <input>, 커스텀 드롭다운, 또는 콤보박스인가요?
  • 선택 옵션: 옵션 값과 텍스트를 모두 덤프하세요. 플레이스홀더의 value="0" 또는 value=""는 비어 있지 않은 것처럼 보일 수 있습니다. Array.from(el.options).map(o => ({ value: o.value, text: o.text }))를 사용하세요. 텍스트에 "선택"이 포함되거나 값이 "0"인 옵션은 건너뛰세요.
  • 리치 텍스트: 댓글 상자가 @멘션, #태그, 마크다운, 또는 이모지를 지원하나요? 플레이스홀더 텍스트를 확인하세요.
  • 필수 필드: 어떤 필드가 양식 제출을 막나요? required 속성, 레이블의 *를 확인하고, 빈 상태로 제출을 시도하여 유효성 검사 오류를 확인하세요.
  • 동적 콘텐츠: 다른 필드를 채운 후에 필드가 나타나나요?
  • 버튼 레이블: "제출", "요청 제출", 또는 "보내기"와 같은 정확한 텍스트.
  • 테이블 열 헤더: 테이블 기반 모달의 경우, 모든 숫자 입력이 동일한 의미를 가진다고 가정하지 말고 각 input[type="number"]를 해당 열 헤더에 매핑하세요.

출력

각 페이지에 대한 필드 맵으로, 스크립트에서 올바른 선택자를 작성하는 데 사용합니다. 예시:

/purchase-requests/new:
  - 예산 코드: <select> (페이지의 첫 번째 선택, 4개 옵션)
  - 원하는 배송일: <input type="date">
  - 컨텍스트: <textarea> (input 아님)
  - BOM 테이블: span.cursor-pointer -> input 패턴의 인라인 편집 가능 셀
  - 제출: <button> text="제출"

/purchase-requests/N (세부정보):
  - 댓글: <input placeholder="메시지를 입력하세요..."> @사용자 및 #PR 태그 지원
  - 보내기: <button> text="보내기" (입력 내용이 있을 때까지 비활성화)

2단계: 예행연습

녹화 없이 모든 단계를 실행합니다. 모든 선택자가 확인되는지 검증합니다.

이유

조용한 선택자 실패는 데모 녹화가 실패하는 주된 이유입니다. 예행연습을 통해 녹화 시간을 낭비하기 전에 이를 발견할 수 있습니다.

방법

로그를 남기고 실패 시 명확히 알리는 ensureVisible 래퍼를 사용하세요:

async function ensureVisible(page, locator, label) {
  const el = typeof locator === 'string' ? page.locator(locator).first() : locator;
  const visible = await el.isVisible().catch(() => false);
  if (!visible) {
    const msg = `예행연습 실패: "${label}"을(를) 찾을 수 없음 - 선택자: ${typeof locator === 'string' ? locator : '(locator object)'}`;
    console.error(msg);
    const found = await page.evaluate(() => {
      return Array.from(document.querySelectorAll('button, input, select, textarea, a'))
        .filter(el => el.offsetParent !== null)
        .map(el => `${el.tagName}[${el.type || ''}] "${el.textContent?.trim().substring(0, 30)}"`)
        .join('\n  ');
    });
    console.error('  보이는 요소:\n  ' + found);
    return false;
  }
  console.log(`예행연습 성공: "${label}"`);
  return true;
}

예행연습 스크립트 구조

const steps = [
  { label: '로그인 이메일 필드', selector: '#email' },
  { label: '로그인 제출', selector: 'button[type="submit"]' },
  { label: '새 요청 버튼', selector: 'button:has-text("새 요청")' },
  { label: '예산 코드 선택', selector: 'select' },
  { label: '배송일', selector: 'input[type="date"]:visible' },
  { label: '설명 필드', selector: 'textarea:visible' },
  { label: '항목 추가 버튼', selector: 'button:has-text("항목 추가")' },
  { label: '제출 버튼', selector: 'button:has-text("제출")' },
];

let allOk = true;
for (const step of steps) {
  if (!await ensureVisible(page, step.selector, step.label)) {
    allOk = false;
  }
}
if (!allOk) {
  console.error('예행연습 실패 - 녹화 전에 선택자를 수정하세요');
  process.exit(1);
}
console.log('예행연습 통과 - 모든 선택자가 확인되었습니다');

예행연습 실패 시

  1. 보이는 요소 덤프를 읽습니다.
  2. 올바른 선택자를 찾습니다.
  3. 스크립트를 업데이트합니다.
  4. 예행연습을 다시 실행합니다.
  5. 모든 선택자가 통과할 때만 진행합니다.

3단계: 녹화

발견과 예행연습이 통과된 후에만 녹화를 시작합니다.

녹화 원칙

1. 스토리텔링 흐름

비디오를 스토리로 계획하세요. 사용자가 지정한 순서를 따르거나, 다음 기본 순서를 사용하세요:

  • 시작: 로그인하거나 시작점으로 이동
  • 컨텍스트: 주변을 둘러보며 시청자가 방향을 잡을 수 있도록 함
  • 작업: 주요 워크플로우 단계 수행
  • 변형: 설정, 테마 또는 현지화와 같은 보조 기능 보여주기
  • 결과: 결과, 확인 또는 새로운 상태 보여주기

2. 템포

  • 로그인 후: 4초
  • 탐색 후: 3초
  • 버튼 클릭 후: 2초
  • 주요 단계 사이: 1.5-2초
  • 최종 동작 후: 3초
  • 타이핑 지연: 문자당 25-40ms

3. 커서 오버레이

마우스 움직임을 따라가는 SVG 화살표 커서를 주입합니다:

async function injectCursor(page) {
  await page.evaluate(() => {
    if (document.getElementById('demo-cursor')) return;
    const cursor = document.createElement('div');
    cursor.id = 'demo-cursor';
    cursor.innerHTML = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
      <path d="M5 3L19 12L12 13L9 20L5 3Z" fill="white" stroke="black" stroke-width="1.5" stroke-linejoin="round"/>
    </svg>`;
    cursor.style.cssText = `
      position: fixed; z-index: 999999; pointer-events: none;
      width: 24px; height: 24px;
      transition: left 0.1s, top 0.1s;
      filter: drop-shadow(1px 1px 2px rgba(0,0,0,0.3));
    `;
    cursor.style.left = '0px';
    cursor.style.top = '0px';
    document.body.appendChild(cursor);
    document.addEventListener('mousemove', (e) => {
      cursor.style.left = e.clientX + 'px';
      cursor.style.top = e.clientY + 'px';
    });
  });
}

오버레이는 탐색 시 파괴되므로 모든 페이지 탐색 후 injectCursor(page)를 호출하세요.

4. 마우스 움직임

커서를 절대로 순간이동시키지 마세요. 클릭하기 전에 대상으로 이동하세요:

async function moveAndClick(page, locator, label, opts = {}) {
  const { postClickDelay = 800, ...clickOpts } = opts;
  const el = typeof locator === 'string' ? page.locator(locator).first() : locator;
  const visible = await el.isVisible().catch(() => false);
  if (!visible) {
    console.error(`경고: moveAndClick 건너뜀 - "${label}"이(가) 표시되지 않음`);
    return false;
  }
  try {
    await el.scrollIntoViewIfNeeded();
    await page.waitForTimeout(300);
    const box = await el.boundingBox();
    if (box) {
      await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2, { steps: 10 });
      await page.waitForTimeout(400);
    }
    await el.click(clickOpts);
  } catch (e) {
    console.error(`경고: moveAndClick 실패 "${label}": ${e.message}`);
    return false;
  }
  await page.waitForTimeout(postClickDelay);
  return true;
}

모든 호출에는 디버깅을 위한 설명적인 label을 포함하세요.

5. 타이핑

즉시 채우지 않고 눈에 보이게 타이핑하세요:

async function typeSlowly(page, locator, text, label, charDelay = 35) {
  const el = typeof locator === 'string' ? page.locator(locator).first() : locator;
  const visible = await el.isVisible().catch(() => false);
  if (!visible) {
    console.error(`경고: typeSlowly 건너뜀 - "${label}"이(가) 표시되지 않음`);
    return false;
  }
  await moveAndClick(page, el, label);
  await el.fill('');
  await el.pressSequentially(text, { delay: charDelay });
  await page.waitForTimeout(500);
  return true;
}

6. 스크롤

점프 대신 부드러운 스크롤을 사용하세요:

await page.evaluate(() => window.scrollTo({ top: 400, behavior: 'smooth' }));
await page.waitForTimeout(1500);

7. 대시보드 패닝

대시보드나 개요 페이지를 보여줄 때, 주요 요소 위로 커서를 이동하세요:

async function panElements(page, selector, maxCount = 6) {
  const elements = await page.locator(selector).all();
  for (let i = 0; i < Math.min(elements.length, maxCount); i++) {
    try {
      const box = await elements[i].boundingBox();
      if (box && box.y < 700) {
        await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2, { steps: 8 });
        await page.waitForTimeout(600);
      }
    } catch (e) {
      console.warn(`경고: panElements 요소 ${i} 건너뜀 (선택자: "${selector}"): ${e.message}`);
    }
  }
}

8. 자막

뷰포트 하단에 자막 바를 주입합니다:

async function injectSubtitleBar(page) {
  await page.evaluate(() => {
    if (document.getElementById('demo-subtitle')) return;
    const bar = document.createElement('div');
    bar.id = 'demo-subtitle';
    bar.style.cssText = `
      position: fixed; bottom: 0; left: 0; right: 0; z-index: 999998;
      text-align: center; padding: 12px 24px;
      background: rgba(0, 0, 0, 0.75);
      color: white; font-family: -apple-system, "Segoe UI", sans-serif;
      font-size: 16px; font-weight: 500; letter-spacing: 0.3px;
      transition: opacity 0.3s;
      pointer-events: none;
    `;
    bar.textContent = '';
    bar.style.opacity = '0';
    document.body.appendChild(bar);
  });
}

async function showSubtitle(page, text) {
  await page.evaluate((t) => {
    const bar = document.getElementById('demo-subtitle');
    if (!bar) return;
    if (t) {
      bar.textContent = t;
      bar.style.opacity = '1';
    } else {
      bar.style.opacity = '0';
    }
  }, text);
  if (text) await page.waitForTimeout(800);
}

모든 탐색 후 injectCursor(page)와 함께 injectSubtitleBar(page)를 호출하세요.

사용 패턴:

await showSubtitle(page, '1단계 - 로그인 중');
await showSubtitle(page, '2단계 - 대시보드 개요');
await showSubtitle(page, '');

지침:

  • 자막 텍스트는 짧게, 가급적 60자 미만으로 유지하세요.
  • 일관성을 위해 N단계 - 동작 형식을 사용하세요.
  • UI가 스스로 설명할 수 있는 긴 일시 중지 중에는 자막을 지우세요.

스크립트 템플릿

'use strict';
const { chromium } = require('playwright');
const path = require('path');
const fs = require('fs');

const BASE_URL = process.env.QA_BASE_URL || 'http://localhost:3000';
const VIDEO_DIR = path.join(__dirname, 'screenshots');
const OUTPUT_NAME = 'demo-FEATURE.webm';
const REHEARSAL = process.argv.includes('--rehearse');

// 여기에 injectCursor, injectSubtitleBar, showSubtitle, moveAndClick,
// typeSlowly, ensureVisible, panElements를 붙여넣으세요.

(async () => {
  const browser = await chromium.launch({ headless: true });

  if (REHEARSAL) {
    const context = await browser.newContext({ viewport: { width: 1280, height: 720 } });
    const page = await context.newPage();
    // 플로우를 탐색하고 각 선택자에 대해 ensureVisible을 실행합니다.
    await browser.close();
    return;
  }

  const context = await browser.newContext({
    recordVideo: { dir: VIDEO_DIR, size: { width: 1280, height: 720 } },
    viewport: { width: 1280, height: 720 }
  });
  const page = await context.newPage();

  try {
    await injectCursor(page);
    await injectSubtitleBar(page);

    await showSubtitle(page, '1단계 - 로그인 중');
    // 로그인 동작

    await page.goto(`${BASE_URL}/dashboard`);
    await injectCursor(page);
    await injectSubtitleBar(page);
    await showSubtitle(page, '2단계 - 대시보드 개요');
    // 대시보드 패닝

    await showSubtitle(page, '3단계 - 주요 워크플로우');
    // 동작 순서

    await showSubtitle(page, '4단계 - 결과');
    // 최종 결과 보여주기
    await showSubtitle(page, '');
  } catch (err) {
    console.error('데모 오류:', err.message);
  } finally {
    await context.close();
    const video = page.video();
    if (video) {
      const src = await video.path();
      const dest = path.join(VIDEO_DIR, OUTPUT_NAME);
      try {
        fs.copyFileSync(src, dest);
        console.log('비디오 저장됨:', dest);
      } catch (e) {
        console.error('오류: 비디오 복사 실패:', e.message);
        console.error('  소스:', src);
        console.error('  대상:', dest);
      }
    }
    await browser.close();
  }
})();

사용법:

# 2단계: 예행연습
node demo-script.cjs --rehearse

# 3단계: 녹화
node demo-script.cjs

녹화 전 체크리스트

  • 발견 단계 완료
  • 모든 선택자가 통과한 예행연습
  • 헤드리스 모드 활성화
  • 해상도 1280x720으로 설정
  • 모든 탐색 후 커서 및 자막 오버레이 재주입
  • 주요 전환에 showSubtitle(page, 'N단계 - ...') 사용
  • 모든 클릭에 설명적인 레이블과 함께 moveAndClick 사용
  • 눈에 보이는 입력에 typeSlowly 사용
  • 조용한 캐치 없음; 실패는 명확하게 로그로 남기기

[생략됨 - 번역을 위해]

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/claude-api

Anthropic Claude API patterns for Python and TypeScript. Covers Messages API, streaming, tool use, vision, extended thinking, batches, prompt caching, and Claude Agent SDK. Use when building applications with the Claude API or Anthropic SDKs.

affaan-m/everything-claude-code

End-to-end marketing campaign planning and execution. Covers audience research, positioning, campaign angle definition, landing page copy, email sequences, social posts, ad copy, short-form video scripts, and content calendars. Use as the orchestration layer for multi-channel product launches. Use when planning or executing a multi-channel product launch, or producing landing page, email, social, or ad copy.

affaan-m/everything-claude-code

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/everything-claude-code-conventions

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/frontend-design

Create distinctive, production-grade frontend interfaces with high design quality. Use when the user asks to build web components, pages, or applications and the visual direction matters as much as the code quality.

affaan-m/gget

gget CLI and Python workflow for quick genomic database queries, sequence lookup, BLAST-style searches, enrichment checks, and reproducible bioinformatics evidence logs.

affaan-m/literature-review

Systematic literature-review workflow for academic, biomedical, technical, and scientific topics, including search planning, source screening, synthesis, citation checks, and evidence logging.

affaan-m/motion-ui

Production-ready UI motion system for React/Next.js. Use when implementing animations, transitions, or motion patterns.

affaan-m/project-guidelines-example

Example project-specific skill template based on a real production application.

affaan-m/pubmed-database

Direct PubMed and NCBI E-utilities search workflows for biomedical literature, MeSH queries, PMID lookup, citation retrieval, and API-backed literature monitoring.

affaan-m/scholar-evaluation

Structured scholarly-work evaluation for papers, proposals, literature reviews, methods sections, evidence quality, citation support, and research-writing feedback.

affaan-m/uspto-database

USPTO patent and trademark data workflow for official record lookup, PatentSearch queries, TSDR checks, assignment data, and reproducible IP research logs.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

관련 스킬