정적 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로 인라인하여 합자(ligature)가 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(0단계)에서 CSSOM을 캡처하고 동일 출처 아이콘 글꼴(@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>