튜토리얼
Shotomatic Team
약 24분

Puppeteer로 웹사이트 스크린샷을 자동화하는 방법

Puppeteer와 JavaScript로 웹사이트 스크린샷을 자동화합니다. 재사용 가능한 스크립트로 단일 페이지, 전체 페이지, URL 일괄 작업, 모바일 뷰포트를 캡처하세요.

Puppeteer 웹사이트 스크린샷 스크립트 코드가 표시된 노트북

이 글에서는 Puppeteer 스크린샷 스크립트를 단계별로 만듭니다. 먼저 뷰포트 하나를 캡처하고, 전체 페이지 캡처, 재사용 가능한 URL 실행기, 모바일 뷰포트를 차례로 추가합니다.

Puppeteer는 첫 스크립트를 작게 유지할 수 있어 이 작업에 잘 맞습니다. 브라우저를 실행하고 URL을 연 뒤 page.screenshot()을 호출하면 됩니다. 공식 Puppeteer 개요는 Puppeteer를 Chrome 또는 Firefox를 제어하는 JavaScript API로 설명하며 기본 실행 모드는 헤드리스입니다.

예제에서는 Shotomatic 도구 페이지인 https://www.shotomatic.com/tools를 사용합니다. 같은 페이지가 뷰포트, 전체 페이지, 일괄 처리, 모바일 캡처에서 어떻게 달라지는지 확인할 수 있습니다.

핵심 요약: 코드로 스크린샷을 자동화하려면 Puppeteer를 사용하세요. 뷰포트를 정하고, 필요한 페이지 상태까지 기다린 뒤 page.screenshot()을 호출합니다. 그다음 전체 페이지 캡처, 일괄 처리, 재시도, 보고서를 추가하세요. 스크립트를 관리하지 않고 공개 URL 목록만 캡처한다면 Shotomatic Website Capture 같은 노코드 방식이 더 간단할 수 있습니다.

알림: 저희가 Shotomatic을 만들기 때문에 예제에서는 공개 도구 페이지를 대상으로 사용합니다. 캡처할 권한이 있는 다른 공개 웹사이트에도 같은 Puppeteer 방식을 적용할 수 있습니다. 노코드 URL 목록 캡처가 필요하다면 Shotomatic의 Website Capture를 사용하세요.

Puppeteer가 적합한 경우

스크린샷 작업이 코드에 속하고 주로 브라우저 제어가 필요할 때 Puppeteer가 잘 맞습니다.

다음 작업에 사용하세요.

  • Node.js 스크립트에서 공개 페이지 캡처
  • 스크린샷에 Chrome의 렌더링 동작 재사용
  • 캡처 전 로그인 또는 팝업 닫기
  • 뷰포트, 전체 페이지, PNG, JPEG, WebP 스크린샷 저장

스크린샷은 이미지 라이브러리가 아니라 실제 헤드리스 브라우저에서 생성됩니다. 이것이 Puppeteer가 이 작업에 유용한 주된 이유입니다.

스크린샷 스크립트에 Puppeteer를 설치합니다

일반 Node.js 프로젝트에 Puppeteer를 설치하세요.

$ mkdir website-screenshots-puppeteer
$ cd website-screenshots-puppeteer
$ npm init -y
$ npm i puppeteer
$ mkdir scripts screenshots

표준 puppeteer 패키지는 설치할 때 호환되는 Chrome for Testing을 다운로드합니다. Puppeteer 공식 설치 문서에 따르면 chrome-headless-shell 바이너리도 다운로드됩니다. 브라우저를 직접 관리하거나 원격 브라우저에 연결한다면 puppeteer-core를 사용하세요.

최신 패키지 관리자는 설치 스크립트를 차단하기도 합니다. Puppeteer는 설치되었지만 브라우저가 없다면 공식 구성 문서에 나온 대로 브라우저를 별도로 설치하세요.

$ npx puppeteer browsers install

웹사이트 스크린샷 하나를 캡처합니다

scripts/capture-one.mjs를 만드세요.

import puppeteer from "puppeteer";
import { mkdir } from "node:fs/promises";

const url = "https://www.shotomatic.com/tools";
const outputPath = "screenshots/shotomatic-tools.png";

const browser = await puppeteer.launch();
const page = await browser.newPage();

try {
  await mkdir("screenshots", { recursive: true });

  await page.setViewport({
    width: 1440,
    height: 1000,
    deviceScaleFactor: 1,
  });

  await page.goto(url, {
    waitUntil: "networkidle2",
    timeout: 45_000,
  });

  await page.screenshot({ path: outputPath });
  console.log(`Saved ${outputPath}`);
} finally {
  await browser.close();
}

실행합니다.

$ node scripts/capture-one.mjs

이 스크립트를 실행하면 screenshots/shotomatic-tools.png가 만들어집니다.

Puppeteer capture-one 예제에서 출력한 Shotomatic 도구 페이지 스크린샷
위 스크립트로 1440×1000 뷰포트에서 캡처한 도구 페이지입니다.

Puppeteer 공식 스크린샷 가이드도 브라우저 실행, 페이지 열기, URL 이동, page.screenshot() 호출이라는 같은 기본 흐름을 사용합니다. type 옵션을 직접 전달하지 않으면 path의 확장자로 출력 형식이 정해집니다. Puppeteer의 ScreenshotOptionsImageFormat 문서에는 fullPage, path, quality, 지원되는 png, jpeg, webp 형식이 정리되어 있습니다.

이동하기 전에 뷰포트를 설정하세요. Puppeteer의 page.setViewport() 문서는 일부 사이트, 특히 모바일 레이아웃이 로드 후 크기 변경에 다르게 반응할 수 있어 이 순서가 중요하다고 설명합니다.

전체 페이지 스크린샷을 캡처합니다

전체 페이지 스크린샷에는 fullPage: true를 전달합니다. Puppeteer의 ScreenshotOptionsfullPage를 페이지 전체를 캡처하는 옵션으로 정의합니다.

await page.screenshot({
  path: "screenshots/shotomatic-tools-full-page.png",
  fullPage: true,
});

스크롤 가능한 문서 전체가 필요할 때 전체 페이지 캡처를 사용하세요. 특정 화면 크기에서 보이는 모습이 중요하다면 뷰포트 스크린샷을 사용합니다.

Shotomatic 무료 도구 페이지의 Puppeteer 전체 페이지 스크린샷
화면 아래 구간까지 렌더링한 뒤 같은 `/tools` 페이지를 전체 캡처한 결과입니다.

전체 페이지 캡처도 페이지 상태의 영향을 받습니다. fullPage: true는 캡처 영역을 바꿀 뿐, 지연 로딩 이미지, 애니메이션, 화면에 들어올 때 나타나는 구간을 강제로 렌더링하지 않습니다. 아래 지연 로딩 구간에서 페이지 상태가 중요한 이유를 설명합니다.

재사용 가능한 URL 실행기를 만듭니다

스크린샷 하나가 정상적으로 만들어지면 대상 URL을 목록을 받는 스크립트로 옮기세요. 결과를 쉽게 비교할 수 있도록 예제에는 여전히 /tools 하나만 사용합니다. 나중에 같은 배열에 URL을 더 추가할 수 있습니다.

scripts/capture-batch.mjs를 만드세요.

import puppeteer from "puppeteer";
import { mkdir } from "node:fs/promises";

const urls = ["https://www.shotomatic.com/tools"];

const outputDir = "screenshots";

function filenameForUrl(url, index) {
  const parsed = new URL(url);
  const rawName = `${parsed.hostname}${parsed.pathname}`;

  const slug = rawName
    .replace(/^www\./, "")
    .replace(/\/$/, "")
    .replace(/[^a-z0-9]+/gi, "-")
    .replace(/^-+|-+$/g, "")
    .toLowerCase();

  return `${String(index + 1).padStart(2, "0")}-${slug || "home"}.png`;
}

async function captureUrl(browser, url, index) {
  const context = await browser.createBrowserContext();
  const page = await context.newPage();
  const outputPath = `${outputDir}/${filenameForUrl(url, index)}`;

  try {
    await page.setViewport({
      width: 1440,
      height: 1000,
      deviceScaleFactor: 1,
    });

    await page.goto(url, {
      waitUntil: "networkidle2",
      timeout: 45_000,
    });

    await page.waitForSelector("body", {
      visible: true,
      timeout: 15_000,
    });

    await page.screenshot({
      path: outputPath,
      fullPage: true,
    });

    console.log(`Captured ${url} -> ${outputPath}`);
    return { url, ok: true, outputPath };
  } catch (error) {
    console.error(`Failed ${url}: ${error.message}`);
    return { url, ok: false, error: error.message };
  } finally {
    await context.close();
  }
}

await mkdir(outputDir, { recursive: true });

const browser = await puppeteer.launch();

try {
  const results = [];

  for (const [index, url] of urls.entries()) {
    results.push(await captureUrl(browser, url, index));
  }

  const failed = results.filter((result) => !result.ok);

  if (failed.length > 0) {
    process.exitCode = 1;
    console.error(`${failed.length} screenshot(s) failed.`);
  }
} finally {
  await browser.close();
}

이 스크립트는 브라우저 하나를 계속 열어 두고 URL마다 별도 브라우저 컨텍스트를 만듭니다. Puppeteer의 BrowserContext 문서는 컨텍스트를 쿠키와 저장 공간이 분리된 사용자 세션으로 설명합니다. 캡처 중 페이지가 쿠키나 로컬 저장 공간을 설정할 때 도움이 됩니다.

반복문은 일부러 순차 실행합니다. 속도는 느리지만 처음 디버깅하기에는 더 쉽습니다.

큰 일괄 작업에 동시 실행을 추가합니다

URL이 몇 개라면 순차 캡처로 충분합니다. 목록이 커지면 작은 동시 실행 제한을 추가하세요.

처음에는 2~4개를 동시에 캡처하세요. 헤드리스 브라우저도 CPU와 메모리를 사용하며, 지나치게 공격적인 일괄 작업은 속도 제한이나 불안정한 페이지 로딩을 일으킬 수 있습니다.

다음 도우미를 추가합니다.

async function runWithConcurrency(items, limit, worker) {
  const results = new Array(items.length);
  let nextIndex = 0;

  async function runNext() {
    while (nextIndex < items.length) {
      const currentIndex = nextIndex;
      nextIndex += 1;
      results[currentIndex] = await worker(items[currentIndex], currentIndex);
    }
  }

  const workers = Array.from(
    { length: Math.min(limit, items.length) },
    () => runNext(),
  );

  await Promise.all(workers);
  return results;
}

그런 다음 순차 반복문을 바꿉니다.

const results = await runWithConcurrency(urls, 3, (url, index) =>
  captureUrl(browser, url, index),
);

이렇게 하면 스크립트가 제한 없이 페이지를 열지 않으면서 일괄 처리 속도를 높일 수 있습니다.

모바일 스크린샷을 캡처합니다

모바일 크기 스크린샷을 만들려면 page.goto() 전에 뷰포트를 설정하세요.

await page.setViewport({
  width: 390,
  height: 844,
  deviceScaleFactor: 2,
  isMobile: true,
  hasTouch: true,
});

await page.goto("https://www.shotomatic.com/tools", {
  waitUntil: "networkidle2",
});

await page.screenshot({
  path: "screenshots/shotomatic-tools-mobile-viewport.png",
});

대략적인 모바일 뷰포트면 충분할 때 사용하세요. 같은 도구 페이지를 이 뷰포트로 캡처한 결과는 다음과 같습니다.

Shotomatic 도구 페이지의 Puppeteer 모바일 뷰포트 스크린샷
Puppeteer의 모바일 뷰포트 결과입니다. 스크립트에서 deviceScaleFactor 2를 사용했으므로 파일 크기는 780×1688입니다.

더 구체적인 기기 사전 설정이 필요하다면 Puppeteer의 알려진 기기를 사용할 수 있습니다.

import puppeteer, { KnownDevices } from "puppeteer";

const iPhone = KnownDevices["iPhone 15"];

await page.emulate(iPhone);
await page.goto("https://www.shotomatic.com/tools", {
  waitUntil: "networkidle2",
});
await page.screenshot({
  path: "screenshots/shotomatic-tools-iphone-15.png",
  fullPage: true,
});

뷰포트, 사용자 에이전트, 터치 동작, 기기 배율이 모두 중요할 때 기기 에뮬레이션이 더 유용합니다. Puppeteer의 page.emulate() 문서는 사용자 에이전트와 뷰포트를 모두 설정한다고 설명하므로 이동 전에 사용하세요.

지연 로딩 콘텐츠를 처리합니다

많은 페이지가 이미지나 구간이 뷰포트 안에 들어온 뒤에야 이를 로드하거나 표시합니다. 전체 페이지를 너무 일찍 캡처하면 화면 아래 콘텐츠가 나타나지 않은 빈 공간이 스크린샷에 남을 수 있습니다.

지연 로딩 구간이 렌더링되기 전에 큰 빈 공간이 생긴 Puppeteer 전체 페이지 스크린샷
즉시 캡처한 결과입니다. 페이지 높이는 맞지만 콘텐츠 일부가 아직 렌더링되지 않았습니다.
지연 로딩 구간을 스크롤한 뒤 만든 Puppeteer 전체 페이지 스크린샷
스크롤 후 캡처한 결과입니다. 화면 아래 구간이 나타날 시간을 확보했습니다.

전체 페이지 스크린샷을 찍기 전에 문서를 스크롤해 일반적인 지연 로딩 동작을 시작하세요.

async function triggerLazyLoading(page) {
  await page.evaluate(async () => {
    const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
    const viewportHeight = window.innerHeight;
    const scrollHeight = document.body.scrollHeight;

    for (let y = 0; y < scrollHeight; y += viewportHeight) {
      window.scrollTo(0, y);
      await delay(150);
    }

    window.scrollTo(0, 0);
  });
}

page.screenshot() 전에 사용합니다.

await page.goto(url, {
  waitUntil: "networkidle2",
  timeout: 45_000,
});

await triggerLazyLoading(page);

await page
  .waitForFunction(
    () => Array.from(document.images).every((image) => image.complete),
    { timeout: 15_000 },
  )
  .catch(() => {});

await page.screenshot({
  path: outputPath,
  fullPage: true,
});

이 방법만으로 부족한 페이지도 있습니다. 사용자 지정 스크롤 컨테이너, 애니메이션, 지연 API 호출, 가상화 콘텐츠에는 페이지별 선택자나 상태가 필요할 수 있습니다.

알맞은 대기 방식을 선택합니다

스크린샷 안정성 문제는 대부분 타이밍에서 생깁니다.

networkidle2는 많은 스크린샷 스크립트에서 괜찮은 출발점이며 Puppeteer 자체 스크린샷 가이드도 기본 예제에서 사용합니다. 다만 페이지 준비 완료를 보장하는 조건으로 여기면 안 됩니다. 어떤 사이트는 백그라운드 요청을 계속 열어 두고, 다른 사이트는 네트워크가 조용해진 뒤 주요 콘텐츠를 렌더링합니다.

일반적인 대기 방법은 다음과 같습니다.

  • 평범한 공개 페이지에는 waitUntil: "networkidle2" 사용
  • 특정 요소가 준비 상태를 증명할 때 page.waitForSelector() 사용
  • 앱 상태에 따라 준비 여부가 정해질 때 page.waitForFunction() 사용
  • 이미지가 지연 로딩될 때 캡처 전 스크롤
  • 고정 대기 시간을 주된 규칙으로 사용하지 않기

예를 들면 다음과 같습니다.

await page.goto(url, {
  waitUntil: "networkidle2",
  timeout: 45_000,
});

await page.waitForSelector("main", {
  visible: true,
  timeout: 15_000,
});

await page.screenshot({
  path: outputPath,
  fullPage: true,
});

대상 사이트를 직접 관리한다면 일반적인 네트워크 조건보다 선택자 대기가 대체로 안정적입니다.

재시도와 보고서를 추가합니다

실제 스크린샷 일괄 작업은 임시 네트워크 오류, 느린 페이지, 리디렉션, 속도 제한, 쿠키 배너, 타사 스크립트 같은 흔한 이유로 실패합니다.

URL마다 재시도를 추가하세요.

async function withRetries(task, retries = 2) {
  let lastError;

  for (let attempt = 0; attempt <= retries; attempt += 1) {
    try {
      return await task();
    } catch (error) {
      lastError = error;
      console.warn(`Attempt ${attempt + 1} failed: ${error.message}`);
    }
  }

  throw lastError;
}

캡처 작업을 감쌉니다.

await withRetries(async () => {
  await page.goto(url, {
    waitUntil: "networkidle2",
    timeout: 45_000,
  });

  await page.waitForSelector("body", {
    visible: true,
    timeout: 15_000,
  });

  await page.screenshot({
    path: outputPath,
    fullPage: true,
  });
});

다시 실행할 작업이라면 URL, 출력 경로, 성공 상태, 오류 메시지, 타임스탬프가 포함된 JSON 보고서를 작성하세요. 스크린샷은 페이지를 보여 주고, 보고서는 실행 결과를 설명합니다.

실용적인 폴더 구조

다시 실행할 작업은 입력, 출력, 보고서를 나누세요.

website-screenshots-puppeteer/
  scripts/
    capture-batch.mjs
  urls/
    product-pages.txt
    competitor-pages.txt
  screenshots/
    2026-06-23-product-pages/
    2026-06-23-competitors/
  reports/
    2026-06-23-product-pages.json

이 구조는 작업을 다시 실행하고 디버깅하기 쉽게 만듭니다. 각 스크린샷 세트와 이를 만든 URL 목록도 함께 관리할 수 있습니다.

자주 하는 실수

  • 페이지가 준비되기 전에 캡처: page.goto()가 끝났다고 필요한 콘텐츠까지 렌더링된 것은 아닙니다. 선택자나 앱 상태를 기다리세요.
  • 너무 늦게 뷰포트 설정: 페이지가 처음부터 올바른 레이아웃으로 로드되도록 이동 전에 뷰포트를 설정하세요.
  • fullPage를 지연 로딩 처리로 오해: 전체 페이지 캡처는 스크린샷 영역만 바꿉니다. 지연된 이미지가 모두 로드되도록 보장하지 않습니다.
  • 한 번에 너무 많은 페이지 열기: 낮은 동시 실행 수로 시작하세요. 브라우저 자동화가 컴퓨터나 대상 사이트에 과도한 부담을 줄 수 있습니다.
  • 파일명 재사용: URL은 끝의 슬래시, 쿼리 문자열, 경로 대소문자만 다를 수 있습니다. 인덱스를 넣고 이름을 안전하게 정리하세요.
  • 정리 생략: 컨텍스트와 브라우저를 닫으세요. 페이지와 세션이 남으면 장기 실행 작업이 불안정해집니다.

Puppeteer 스크린샷 스크립트의 대안

Puppeteer만 웹사이트 스크린샷을 자동화할 수 있는 것은 아닙니다.

코드로 처리하려면 Playwright가 가장 가까운 대안입니다. Playwright로 웹사이트 스크린샷을 자동화하는 방법에서 이 방식을 설명합니다. 스크린샷을 더 큰 테스트 스위트에 포함하거나 Chromium, Firefox, WebKit을 모두 확인해야 할 때, 또는 캡처 작업에 Playwright Test의 테스트 러너, 트레이스, 리포트를 함께 쓰고 싶을 때 잘 맞습니다.

제품에 스크린샷 캡처를 넣는다면 브라우저를 직접 관리하는 것보다 스크린샷 API가 일을 줄일 수 있습니다. 스크립트를 다른 사람이 안정적으로 실행해야 하는 순간부터 브라우저 바이너리, CI 메모리, 재시도, 정리, 로그인 상태를 직접 다뤄야 합니다.

스크립트를 직접 작성하고 유지하고 싶지 않다면 코드 없이 웹사이트 스크린샷을 자동화하는 방법을 읽어 보세요. URL 목록 캡처, 페이지별 설정, 결과 확인, 내보내기를 Puppeteer 스크립트 없이 진행하려면 Shotomatic의 Website Capture를 사용하세요.

자주 묻는 질문

Puppeteer로 웹사이트 스크린샷을 자동화할 수 있나요?

그렇습니다. Puppeteer는 브라우저를 실행하고 URL을 열며 뷰포트를 설정한 뒤 page.screenshot()으로 스크린샷을 저장할 수 있습니다.

Puppeteer로 전체 페이지 스크린샷을 찍을 수 있나요?

그렇습니다. page.screenshot()fullPage: true를 전달하면 보이는 뷰포트뿐 아니라 스크롤 가능한 페이지 전체를 캡처합니다.

Puppeteer로 URL을 여러 개 캡처할 수 있나요?

그렇습니다. 브라우저 하나를 열어 두고 URL마다 페이지 또는 브라우저 컨텍스트를 만드세요. 큰 일괄 작업에는 동시 실행 제한을 추가합니다.

스크린샷에는 Puppeteer가 Playwright보다 나은가요?

Chrome 중심의 스크린샷 스크립트에는 Puppeteer가 더 간단한 경우가 많습니다. Chromium, Firefox, WebKit을 모두 확인하거나 더 큰 테스트 작업이 필요하다면 Playwright가 대체로 알맞습니다.

Puppeteer 대신 Shotomatic을 써야 하는 경우는 언제인가요?

JavaScript 스크립트를 관리하지 않고 누군가 URL 목록 캡처를 실행하고 검토한 뒤 내보내야 한다면 Shotomatic을 사용하세요.

참고 자료

위 예제는 다음 공식 문서를 기준으로 확인했습니다.

관련 글

글 더 보기

코드 없이 웹사이트 스크린샷을 자동화하세요

Website Capture로 URL 목록을 캡처하고, 페이지마다 옵션을 지정하고, 결과를 확인해 내보낼 수 있습니다. 스크립트를 관리할 필요가 없습니다.

Puppeteer로 웹사이트 스크린샷을 자동화하는 방법 | 블로그 | Shotomatic