튜토리얼
Shotomatic Team
약 23분

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

JavaScript와 Playwright로 웹사이트 스크린샷을 자동화하세요. 한 페이지, 전체 페이지, 모바일 화면, URL 일괄 캡처와 재시도를 구현합니다.

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

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

스크린샷을 만드는 코드와 제어 코드를 함께 관리해야 할 때 Playwright가 잘 맞습니다. 실제 브라우저 렌더링, 분리된 브라우저 컨텍스트, 명시적인 뷰포트 제어, 기기 에뮬레이션, 원하는 페이지 상태를 기다리는 기능을 사용할 수 있습니다.

예제는 모든 방식의 결과를 같은 페이지에서 비교할 수 있도록 Shotomatic 기능 페이지인 https://www.shotomatic.com/features를 사용합니다. 뷰포트, 전체 페이지, 일괄 처리, 모바일 캡처 결과를 차례로 확인할 수 있습니다.

요약: 스크린샷 자동화가 코드 안에 있어야 한다면 Playwright를 사용하세요. 브라우저 컨텍스트를 만들고 뷰포트나 기기를 설정한 뒤 필요한 페이지 상태를 기다리고 page.screenshot()을 호출합니다. 다음으로 전체 페이지 캡처, 일괄 처리, 재시도, 보고 기능을 추가하세요. 스크립트를 작성하고 관리하는 일까지 맡고 싶지 않다면 Shotomatic Website Capture가 더 간단할 수 있습니다.

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

Playwright가 적합한 경우

스크린샷이 브라우저 동작을 따라야 한다면 Playwright를 사용하세요.

다음 작업에 적합합니다.

  • CI 안에서 스크린샷 캡처
  • 스크린샷을 찍기 전 로그인
  • 특정 선택자나 앱 상태가 될 때까지 대기
  • Chromium, Firefox, WebKit에서 같은 스크린샷 작업 실행
  • 명시적인 뷰포트 설정으로 반응형 스크린샷 생성
  • 내부 도구에서 스크린샷 생성

스크린샷을 쓸 사람과 스크립트를 관리할 사람이 다르면 불편해집니다. 작업이 코드로 관리돼야 한다면 Playwright부터 시작하세요.

스크린샷 스크립트용 Playwright 설치하기

일반 Node.js 스크립트에서는 Playwright 라이브러리와 자동화할 브라우저를 설치합니다.

$ mkdir website-screenshots-playwright
$ cd website-screenshots-playwright
$ npm init -y
$ npm i -D playwright
$ npx playwright install chromium

Playwright 라이브러리 문서도 같은 기본 순서를 사용합니다. 패키지와 브라우저를 설치하고 Playwright를 가져온 뒤 브라우저를 실행해 페이지와 상호 작용합니다. 나중에 Firefox나 WebKit이 필요하면 해당 브라우저도 설치하세요.

스크립트와 결과를 담을 폴더를 만듭니다.

$ mkdir scripts screenshots

웹사이트 스크린샷 한 장 캡처하기

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

const { chromium } = require("playwright");
const fs = require("node:fs/promises");

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

(async () => {
  await fs.mkdir("screenshots", { recursive: true });

  const browser = await chromium.launch();
  const context = await browser.newContext({
    viewport: { width: 1440, height: 1000 },
    deviceScaleFactor: 1,
  });

  const page = await context.newPage();

  try {
    await page.goto(url, { waitUntil: "load", timeout: 45_000 });
    await page.screenshot({ path: outputPath });
    console.log(`Saved ${outputPath}`);
  } finally {
    await context.close();
    await browser.close();
  }
})();

실행합니다.

$ node scripts/capture-one.js

이 코드는 화면에 보이는 뷰포트를 캡처합니다. Playwright 스크린샷 API는 전체 페이지, 요소, 버퍼, 클리핑, 이미지 품질 옵션도 지원합니다.

스크립트를 실행하면 screenshots/shotomatic-features.png가 생성됩니다.

Shotomatic 기능 페이지가 표시된 Playwright capture-one 예제의 스크린샷 결과
위 스크립트가 1440×1000 뷰포트에서 캡처한 기능 페이지 결과입니다.

전체 페이지 스크린샷 캡처하기

전체 페이지 스크린샷을 만들려면 fullPage: true를 전달합니다.

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

문서 전체를 위에서 아래까지 담아야 할 때 전체 페이지 캡처를 사용하세요. 특정 화면 크기에서 방문자에게 보이는 부분이 중요하다면 뷰포트 스크린샷을 사용합니다.

Shotomatic 기능 페이지를 Playwright로 전체 페이지 캡처한 결과
화면 아래 섹션까지 렌더링된 뒤 같은 `/features` 페이지를 전체 캡처한 결과입니다.

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

대상 URL을 일괄 스크립트로 옮기기

한 페이지 스크립트가 작동하면 URL 목록을 받고 예측 가능한 파일 이름으로 저장하는 스크립트로 대상을 옮기세요. 결과를 쉽게 따라갈 수 있도록 아래 예제도 /features 하나만 사용합니다. 나중에 같은 배열에 URL을 더할 수 있습니다.

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

const { chromium } = require("playwright");
const fs = require("node:fs/promises");

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

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.newContext({
    viewport: { width: 1440, height: 1000 },
    deviceScaleFactor: 1,
  });

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

  try {
    await page.goto(url, { waitUntil: "load", timeout: 45_000 });
    await page.locator("body").waitFor({ state: "visible", 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();
  }
}

(async () => {
  await fs.mkdir(outputDir, { recursive: true });

  const browser = await chromium.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 5개는 단순 반복문으로도 충분합니다. 50개나 500개라면 작은 동시 실행 제한을 두는 편이 좋습니다.

페이지 수백 개를 한꺼번에 열지 마세요. 브라우저 자동화는 메모리를 많이 사용하며 너무 많은 요청을 보내면 사이트가 속도를 제한하거나 제대로 응답하지 않을 수 있습니다. 동시 캡처 2~4개로 시작하고 결과가 안정적일 때만 늘리세요.

이 가이드의 Puppeteer 버전에서 사용한 것과 같은 헬퍼를 추가합니다.

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),
);

이 방식은 브라우저 하나를 계속 열어 두고 캡처마다 분리된 브라우저 컨텍스트를 만들며 동시에 활성화되는 페이지 수를 제한합니다. Playwright 라이브러리 문서도 Node 스크립트에서 명시적인 브라우저 컨텍스트를 사용하므로 캡처마다 독립된 수명 주기를 가집니다.

모바일 또는 태블릿 스크린샷 캡처하기

Playwright에는 일반적인 휴대폰과 태블릿을 위한 기기 에뮬레이션 설명자가 포함돼 있습니다. 예를 들면 다음과 같습니다.

const { chromium, devices } = require("playwright");

(async () => {
  const browser = await chromium.launch();
  const iPhone = devices["iPhone 13"];

  const context = await browser.newContext({
    ...iPhone,
  });

  const page = await context.newPage();
  await page.goto("https://www.shotomatic.com/features", {
    waitUntil: "load",
  });
  await page.screenshot({
    path: "screenshots/shotomatic-features-iphone-13.png",
  });

  await context.close();
  await browser.close();
})();

iPhone 13 설명자로 같은 기능 페이지를 캡처한 결과입니다.

Shotomatic 기능 페이지를 모바일 뷰포트에서 Playwright로 캡처한 결과
Playwright의 iPhone 13 기기 설명자를 사용한 모바일 뷰포트 결과입니다.

사용자 에이전트, 터치 지원, 뷰포트, 기기 배율이 중요하다면 기기 설명자를 사용하세요. 특정 크기만 필요하다면 일반 뷰포트를 사용합니다.

const context = await browser.newContext({
  viewport: { width: 390, height: 844 },
  deviceScaleFactor: 2,
  isMobile: true,
});

지연 로딩 콘텐츠 처리하기

많은 페이지는 이미지나 섹션이 뷰포트 안에 들어온 뒤에야 불러옵니다. 너무 일찍 전체 페이지를 캡처하면 아래쪽 콘텐츠가 나타나지 않은 빈 공간까지 스크린샷에 남을 수 있습니다.

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

전체 페이지를 캡처하기 전에 문서 전체를 스크롤해 지연 로딩 이미지와 화면 진입 섹션을 불러옵니다.

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(600);
    }

    window.scrollTo(0, 0);
    await delay(300);
  });
}

page.screenshot()보다 먼저 실행하세요.

await page.goto(url, { waitUntil: "load", timeout: 45_000 });
await triggerLazyLoading(page);
await page
  .waitForFunction(
    () =>
      Array.from(document.images).every(
        (image) => image.complete && image.naturalWidth > 0,
      ),
    null,
    { timeout: 15_000 },
  )
  .catch(() => {});
await page.screenshot({ path: outputPath, fullPage: true });

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

알맞은 대기 전략 선택하기

스크린샷이 불안정한 원인은 대부분 타이밍입니다.

Playwright 탐색 문서에 따르면 page.goto()는 기본적으로 load 이벤트를 기다립니다. 하지만 최신 페이지는 이 이벤트 뒤에도 데이터를 가져오거나 렌더링할 수 있습니다. 스크린샷에는 보통 다음 전략 중 하나를 사용합니다.

  • 일반적인 공개 페이지에서는 waitUntil: "load"로 시작합니다
  • 필요한 콘텐츠가 렌더링됐음을 증명하는 특정 로케이터를 기다립니다
  • 준비 여부가 앱 상태에 달렸다면 page.waitForFunction()을 사용합니다
  • 이미지가 지연 로딩되면 캡처 전에 스크롤합니다
  • 고정 시간 대기를 주된 규칙으로 사용하지 않습니다

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

await page.goto(url, { waitUntil: "load", timeout: 45_000 });
await page
  .getByRole("heading", { name: "Shotomatic Features" })
  .waitFor({ state: "visible", 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: "load", timeout: 45_000 });
  await page.screenshot({ path: outputPath, fullPage: true });
});

실제 운영에서는 URL, 결과 경로, 상태, 오류 메시지, 타임스탬프를 기록한 JSON 보고서도 작성하세요. 나중에 누군가 어느 페이지가 왜 실패했는지 물을 때 시간을 절약할 수 있습니다.

실용적인 폴더 구조

다시 실행할 스크린샷 작업이라면 입력, 결과, 스크립트를 분리하세요.

website-screenshots-playwright/
  scripts/
    capture-batch.js
  urls/
    launch-pages.txt
    competitor-pages.txt
  screenshots/
    2026-06-23-launch/
    2026-06-23-competitors/
  reports/
    2026-06-23-launch.json

이 구조는 작업을 다시 실행하기 쉽게 만들고 스크린샷 묶음을 원본 URL 목록과 함께 관리할 수 있게 합니다.

흔히 하는 실수

  • 너무 일찍 캡처하기: 브라우저가 load에 도달해도 필요한 콘텐츠가 보이지 않을 수 있습니다. 선택자를 기다리거나 페이지별 검사를 추가하세요.
  • 한꺼번에 너무 많은 페이지 열기: 동시 실행 수가 많으면 브라우저가 멈추고 메모리가 부족해지거나 사이트의 요청 제한에 걸릴 수 있습니다. 작게 시작하세요.
  • 모든 사이트에 같은 파일 이름 규칙 쓰기: URL에는 쿼리 문자열, 마지막 슬래시, 중복, 특수 문자가 들어갈 수 있습니다. 파일 이름을 항상 정리하고 번호를 포함하세요.
  • 브라우저 정리 잊기: 페이지, 컨텍스트, 브라우저를 닫으세요. 닫지 않은 컨텍스트는 오래 실행되는 작업에서 실제 문제를 만듭니다.
  • 전체 페이지 스크린샷을 시각적 테스트로 착각하기: 스크린샷 파일은 증거일 뿐입니다. 시각적 회귀 테스트에는 기준 이미지, 비교 임곗값, 검토 절차도 필요합니다.

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

Playwright만 웹사이트 스크린샷을 자동화할 수 있는 것은 아닙니다. 첫 스크립트를 만든 뒤 누가 작업을 관리할지에 따라 선택이 달라집니다.

집중된 브라우저 자동화 스크립트만 필요하다면 Puppeteer로 충분할 수 있습니다. 스크린샷이 더 큰 테스트 스위트와 함께 있고 Chromium, Firefox, WebKit을 모두 지원해야 하거나 Playwright Test의 실행기, 추적, 보고서를 이미 사용한다면 Playwright가 더 적합합니다.

제품에 스크린샷 캡처를 넣는다면 브라우저를 직접 관리하는 대신 스크린샷 API를 사용해 작업을 줄일 수 있습니다.

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

자주 묻는 질문

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

가능합니다. Playwright로 브라우저를 실행하고 URL을 연 뒤 뷰포트나 기기 옵션을 설정하고 page.screenshot()으로 페이지 스크린샷을 저장할 수 있습니다.

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

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

Playwright로 여러 URL의 스크린샷을 캡처할 수 있나요?

가능합니다. URL을 배열에 넣고 반복하면서 캡처마다 새 브라우저 컨텍스트를 만드세요. 일괄 작업이 커지면 동시 실행 제한과 오류 처리를 추가합니다.

웹사이트 스크린샷에는 Playwright와 Puppeteer 중 무엇이 좋나요?

여러 브라우저 지원, 기기 에뮬레이션, 스크린샷을 포함한 더 큰 테스트 작업이 필요하면 Playwright를 사용하세요. Chrome 중심 스크린샷 스크립트에는 Puppeteer도 실용적입니다.

언제 Playwright보다 코드 없는 스크린샷 작업이 더 나은가요?

개발자가 아닌 사람도 스크립트, 브라우저 의존성, CI 작업을 관리하지 않고 URL 목록의 스크린샷을 실행·검토·내보내야 할 때 코드 없는 방식이 더 적합합니다.

참고 자료

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

관련 글

글 더 보기

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

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

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