정적 HTML 추출
웹 애플리케이션에서 자체 포함된 정적 HTML 파일을 추출합니다.
사용할 전략
진행하기 전에 반드시 사용자에게 어떤 전략을 사용할지 물어봐야 합니다. 옵션을 명확히 제시하고, 전략 A를 기본값으로 권장하며, 각 옵션에 대한 간단한 장단점 요약을 제공하여 사용자가 정보에 기반한 결정을 내릴 수 있도록 도와주세요.
| 전략 A (Puppeteer) | 전략 B (브라우저 서브에이전트) | |
|---|---|---|
| 사용 시기 | 앱이 로컬에서 실행되고, 인증 장벽이 없음 | 페이지와 먼저 상호작용(클릭, 양식 작성)이 필요함 |
| 충실도 | 최고 — 계산된 스타일이 해석됨 | 높음 — 렌더링된 DOM |
| 설정 | 제로 — 모의 객체 불필요 | 제로 — 모의 객체 불필요 |
| 프레임워크 | 모든 프레임워크 | 모든 프레임워크 |
| 출력 | 파일로 직접 기록 — 크기 제한 없음 | 에이전트 컨텍스트에서 잘릴 수 있음 |
[!WARNING] 체크포인트 — 사용자 확인 필요. 진행하기 전에 반드시 사용자에게 어떤 전략을 선호하는지 물어봐야 합니다. 위의 비교 표를 제시하고, 전략 A를 기본값으로 권장하며, 명시적인 승인을 기다리세요. 직접 결정을 내리거나 사용자가 확인할 때까지 진행하지 마세요.
전략 A: Puppeteer 스냅샷 (권장)
Headless Chrome을 실행하고, 완전히 렌더링된 DOM을 캡처하며, 모든 CSS가 인라인되고 이미지가 base64로 변환된 자체 포함 HTML 파일을 생성합니다. 모든 프레임워크에서 작동합니다 — MockPage.jsx가 필요하지 않습니다.
사전 요구 사항
- 앱이 로컬에서 실행 중 (예:
npm run dev) - Node.js에
puppeteer사용 가능 (확인:node -e "require('puppeteer')")
워크플로
-
앱을 시작하고 포트를 기록합니다.
[!WARNING] 체크포인트 — 사용자 확인 필요. 로컬 서버를 시작한 후 반드시 잠시 멈추고 스냅샷 스크립트를 실행하거나 브라우저 서브에이전트를 실행하기 전에 사용자에게 확인을 요청해야 합니다. 사용자가 앱이 실행 중이고 올바르게 렌더링되는지 확인할 수 있도록 URL과 포트를 보고합니다. 사용자가 확인할 때까지 스냅샷 단계로 진행하지 마세요.
-
스냅샷 스크립트 실행:
npx tsx <SKILL_DIR>/scripts/snapshot.ts \ --url http://localhost:5173 \ --output .stitch/home.html \ --wait 2000 -
여러 페이지 — 각 경로에 대해 한 번씩 실행:
npx tsx <SKILL_DIR>/scripts/snapshot.ts \ --url http://localhost:5173 --output .stitch/home.html --wait 2000 npx tsx <SKILL_DIR>/scripts/snapshot.ts \ --url http://localhost:5173/pricing --output .stitch/pricing.html --wait 2000 npx tsx <SKILL_DIR>/scripts/snapshot.ts \ --url http://localhost:5173/dashboard --output .stitch/dashboard.html --wait 2000 --html-class dark -
개발 서버 정리: 스냅샷 추출을 위해 로컬 개발 서버를 시작한 경우, 추출이 완료되면 서버 프로세스를 중지하거나 백그라운드 작업을 종료해야 합니다.
스크립트 플래그
| 플래그 | 기본값 | 설명 |
|---|---|---|
--url | (필수) | 캡처할 URL |
--output | (필수) | 출력 파일 경로 |
--wait | 1000 | 네트워크 유휴 후 추가 대기 시간(ms). 지연 로딩 앱의 경우 값을 늘리세요. |
--viewport | 1280x800 | 뷰포트 크기 (WIDTHxHEIGHT 형식) |
--html-class | — | <html> 요소에 추가할 클래스(예: dark) |
--remove-fixed | false | 고정/스티키 요소 제거 (쿠키 배너, 채팅 위젯) |
--full-height | false | 전체 스크롤 높이로 뷰포트 크기 조정 |
--title | — | 페이지 제목 재정의 (경로 이름으로 설정, 예: /dashboard 또는 /settings/profile) |
--auth-script | — | 인증을 위한 async (page) => void 함수를 기본 내보내기하는 JS/TS 모듈 경로 |
--inline-canvas | false | <canvas> 요소 (ECharts, Chart.js, D3)를 base64 <img> 태그로 변환 |
자동 수행 작업
document.styleSheets의 모든 CSSOM 규칙 캡처 (동적 Vite/Tailwind 개발 스타일 및 CSS-in-JS 유지)- 모든
<link rel="stylesheet">를<style>블록으로 인라인 <img>의src및srcset을 base64 데이터 URI로 변환 (외부 글꼴 제외)- 동일 출처 및 상대 아이콘 글꼴 파일(
@font-face)을 base64 데이터 URI로 인라인 처리하여 합자가 ASCII 텍스트로 렌더링되지 않도록 방지 <source srcset>URL을 base64로 인라인- 실패/죽은
srcset항목을 제거하여 브라우저가 인라인된src로 폴백하도록 함 <script>태그, Vite HMR 개발 스타일 블록 (createHotContext,import.meta.hot), 개발 오버레이 제거- 인라인 전에 상대 CSS
url()경로를 절대 경로로 변환
프레임워크 참고 사항
| 프레임워크 | 참고 사항 |
|---|---|
| React + Vite | 별다른 설정 없이 작동합니다. --wait 1000. |
| Next.js | SSR 수화를 위해 --wait 3000. URL: http://localhost:3000. /_next/image의 <img srcset>는 자동으로 base64로 인라인됩니다. |
| Angular (@angular/cli / v17+) | ng serve와 함께 별다른 설정 없이 작동합니다 (기본 URL: http://localhost:4200). Angular Material / PrimeNG 애니메이션 수화 및 지연 로딩 경로를 위해 --wait 2000. |
| Vue / Nuxt | 별다른 설정 없이 작동합니다. |
| Svelte / SvelteKit | 별다른 설정 없이 작동합니다. |
| Storybook | 스토리 URL 사용: --url http://localhost:6006/?path=/story/... |
| SSR (Webpack) | 더 긴 --wait이 필요할 수 있습니다. |
문제 해결
| 문제 | 해결 방법 |
|---|---|
| 이미지 누락 | --wait 증가 |
| 서버 중단 후 이미지가 깨져서 표시됨 | srcset이 인라인되었는지 확인 — 로그에서 "Inlined N images" 확인. srcset URL이 실패하면 자동으로 제거되어 src(인라인됨)가 사용됩니다. |
| 아이콘이 텍스트 / Serif 스타일 없는 글꼴로 표시됨 | snapshot.ts가 document.styleSheets에서 CSSOM을 캡처하고(0단계) 동일 출처 아이콘 글꼴(@font-face)이 base64 데이터 URI로 인라인되었는지 확인하세요. |
Next.js /_next/image가 인라인되지 않음 | 스냅샷 실행 시 개발 서버가 실행 중인지 확인하세요 — 스크립트가 실행 중인 서버에서 최적화된 이미지를 가져옵니다. |
| 다크 모드가 적용되지 않음 | --html-class dark |
| 출력에 쿠키 배너가 포함됨 | --remove-fixed |
| 페이지에 로그인 필요 | --auth-script ./auth.ts 사용 (아래 인증 필요 페이지 참조) |
| 차트/그래프가 빈 상자로 표시됨 | --inline-canvas를 사용하여 <canvas>를 base64 <img>로 직렬화 |
Cannot find module 'puppeteer' | npm install -g puppeteer |
인증 필요 페이지
로그인 가드가 있는 앱 (Vue Router beforeEach, React ProtectedRoute 등)의 경우, Puppeteer 세션에서 실행되는 작은 인증 스크립트를 만드세요:
// auth-myapp.ts
import type { Page } from 'puppeteer';
export default async function authenticate(page: Page) {
// 예제 1: 로그인 양식 작성 및 제출
await page.type('#username', 'admin');
await page.type('#password', 'password123');
await page.click('#login-button');
await page.waitForNavigation({ waitUntil: 'networkidle2' });
// 예제 2: 쿠키/localStorage 직접 주입
// await page.evaluate(() => {
// localStorage.setItem('token', 'mock-jwt-token');
// });
// 예제 3: 모듈 주입을 통해 앱 자체 로그인 API 호출 (Vue/Vite)
// await page.evaluate(() => {
// return new Promise((resolve) => {
// const script = document.createElement('script');
// script.type = 'module';
// script.textContent = `
// import { useUserStore } from '/src/store/modules/user.ts';
// import { fetchLogin } from '/src/api/auth.ts';
// const res = await fetchLogin({ userName: 'Admin', password: '123456' });
// useUserStore().setToken(res.token, res.refreshToken);
// window.dispatchEvent(new CustomEvent('auth-done'));
// `;
// document.head.appendChild(script);
// window.addEventListener('auth-done', () => resolve(true), { once: true });
// });
// });
}
그런 다음 다음과 같이 사용하세요:
npx tsx <SKILL_DIR>/scripts/snapshot.ts \
--url http://localhost:5173/#/dashboard \
--output .stitch/dashboard.html \
--auth-script ./auth-myapp.ts \
--inline-canvas \
--wait 5000
스크립트는 먼저 --url로 이동하고(로그인 페이지로 리디렉션될 수 있음), 인증 함수를 실행한 다음, 인증된 세션으로 원래 --url로 다시 이동합니다.
전략 B: 브라우저 서브에이전트 캡처
캡처하기 전에 페이지와 상호작용(버튼 클릭, 양식 작성, 탭 이동)이 필요할 때 사용합니다. 브라우저 서브에이전트는 완전한 제어를 제공하지만, 대규모 페이지의 경우 출력이 잘릴 수 있습니다.
워크플로
-
앱을 로컬에서 시작합니다.
-
브라우저 서브에이전트를 사용하여 이동합니다.
-
필요에 따라 상호작용합니다(클릭, 스크롤, 양식 작성).
-
DOM 추출:
document.documentElement.outerHTML[!WARNING] 대규모 페이지는 잘릴 수 있습니다. 이를 처리하려면:
- 추출 전에
<style>태그 제거:document.querySelectorAll('style').forEach(el => el.remove()) - 스타일을 정적으로 다시 추가 (Tailwind CDN 링크, 소스 CSS)
- 추출 전에
-
파일로 저장합니다.
부록: 정적 폴백 (MockPage.jsx)
[!NOTE] 이 방법은 앱을 로컬에서 실행할 수 없는 경우(종속성 손상, 백엔드 누락, 우회 불가능한 인증 장벽)의 최후의 수단입니다. React 컴포넌트를 수동으로 단일 JSX 파일로 평탄화해야 합니다. 가능하면 전략 A를 사용하세요.
사용 시기
- 앱을 로컬에서 전혀 실행할 수 없음
- 페이지에 우회 방법이 없는 인증이 필요함
- 탐색으로 도달하기 불가능한 특정 UI 상태(오류 화면, 빈 상태)가 필요함
빠른 참조
npx tsx <SKILL_DIR>/scripts/extract_inline_html.ts \
--index-css src/css/App.css \
--extra-css index.html \
--outdir .stitch \
--page src/MockPage.jsx:Page.html:"Page Title"
주요 플래그: --no-tailwind (Tailwind를 사용하지 않는 앱), --html-class dark (다크 모드), --css-files (추가 CSS 파일).
자동 감지: Tailwind 설정은 자동으로 감지됩니다. @apply 지시문은 자동으로 <style type="text/tailwindcss">를 사용합니다.
MockPage.jsx 규칙
- 전체 레이아웃 포함 — 헤더, 사이드바, 푸터 (먼저
App.js읽기) - 모든 조건문 평탄화 — 하나의 상태를 선택하고, 모든 삼항 연산자와
&&가드를 제거 - 모든 데이터 하드코딩 —
{variable}을 구체적인 값으로 바꾸고,.map()루프를 펼침 - 로고 유지 — 로컬 경로와 함께
<img>사용 (후처리에서 인라인 처리됨) - 부동 요소 제거 — 쿠키 배너, 채팅 위젯, 피드백 버튼
후처리
로컬 이미지 인라인:
npx tsx <SKILL_DIR>/scripts/post_process.ts \
.stitch/Page.html --base-dir <app-directory>