인증된 페이지(로그인 제한 콘텐츠) 스크린샷 방법

발행: (2026년 2월 26일 오후 05:16 GMT+9)
12 분 소요
원문: Dev.to

Source: Dev.to


인증된 페이지(로그인 제한 콘텐츠) 스크린샷 찍는 방법

소개

웹사이트에서 로그인 후에만 접근할 수 있는 페이지를 스크린샷으로 저장해야 하는 경우가 종종 있습니다. 일반적인 스크린샷 도구는 인증된 세션을 인식하지 못하기 때문에, 로그인된 상태를 재현하는 별도의 절차가 필요합니다. 이 글에서는 PuppeteerPlaywright를 활용해 로그인된 페이지를 자동으로 열고, 원하는 영역을 캡처하는 방법을 단계별로 설명합니다.

왜 일반 스크린샷 도구로는 안 될까?

  • 쿠키와 세션: 로그인 후 브라우저에 저장되는 쿠키와 세션 토큰이 없으면 서버는 요청을 인증되지 않은 사용자로 간주합니다.
  • CSRF 토큰: 일부 사이트는 요청마다 동적으로 생성되는 CSRF 토큰을 요구합니다.
  • 동적 렌더링: 로그인 후에만 로드되는 JavaScript 기반 UI 요소가 존재합니다.

따라서, 헤드리스 브라우저를 사용해 실제 브라우저와 동일한 환경을 구축해야 합니다.


1. 사전 준비

  1. Node.js (v14 이상) 설치
  2. Puppeteer 혹은 Playwright 패키지 설치
npm install puppeteer   # 또는
npm install playwright
  1. 로그인에 사용할 자격 증명(아이디·비밀번호) 혹은 API 토큰을 안전하게 보관합니다.
    • 환경 변수(.env)에 저장하고 dotenv 패키지로 로드하는 것이 일반적입니다.

2. Puppeteer 로 로그인 후 스크린샷 찍기

핵심 흐름

  1. 브라우저 인스턴스 실행
  2. 로그인 페이지로 이동
  3. 폼에 자격 증명 입력 → 제출
  4. 로그인 성공 여부 확인(예: 특정 요소 존재 여부)
  5. 목표 페이지로 이동
  6. page.screenshot() 호출

예시 코드

const puppeteer = require('puppeteer');
require('dotenv').config();

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

  // 1️⃣ 로그인 페이지 이동
  await page.goto('https://example.com/login', { waitUntil: 'networkidle2' });

  // 2️⃣ 폼에 입력
  await page.type('#email', process.env.USER_EMAIL);
  await page.type('#password', process.env.USER_PASSWORD);
  await Promise.all([
    page.click('button[type="submit"]'),
    page.waitForNavigation({ waitUntil: 'networkidle2' }),
  ]);

  // 3️⃣ 로그인 성공 확인
  if (await page.$('.profile-avatar') === null) {
    console.error('로그인에 실패했습니다.');
    await browser.close();
    return;
  }

  // 4️⃣ 인증된 페이지로 이동
  await page.goto('https://example.com/protected/content', {
    waitUntil: 'networkidle2',
  });

  // 5️⃣ 스크린샷 저장
  await page.screenshot({ path: 'protected-page.png', fullPage: true });

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

  • waitUntil: 'networkidle2' 옵션은 네트워크 요청이 500 ms 동안 2개 이하일 때 페이지 로딩이 완료됐다고 판단합니다. 동적 페이지에 유용합니다.
  • 로그인 후 쿠키를 파일에 저장하고 재사용하면 매번 로그인 과정을 건너뛸 수 있습니다.

3. Playwright 로 동일 작업 수행하기

Playwright는 멀티 브라우저(Chromium, Firefox, WebKit)를 지원하므로, 다양한 환경에서 테스트하고 싶을 때 유리합니다.

예시 코드

const { chromium } = require('playwright');
require('dotenv').config();

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

  // 로그인 페이지 이동
  await page.goto('https://example.com/login');

  // 폼 입력 및 제출
  await page.fill('#email', process.env.USER_EMAIL);
  await page.fill('#password', process.env.USER_PASSWORD);
  await Promise.all([
    page.click('button[type="submit"]'),
    page.waitForNavigation(),
  ]);

  // 로그인 성공 확인
  if (!(await page.$('.profile-avatar'))) {
    console.error('로그인에 실패했습니다.');
    await browser.close();
    return;
  }

  // 인증된 페이지로 이동
  await page.goto('https://example.com/protected/content');

  // 스크린샷 캡처
  await page.screenshot({ path: 'protected-page.png', fullPage: true });

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

4. 쿠키/세션을 재활용하는 방법

매번 로그인 절차를 수행하면 속도가 느려지고, 로그인 제한(예: 2FA) 때문에 자동화가 어려워질 수 있습니다. 이때는 쿠키 파일을 저장하고 다음 실행 시 로드하는 것이 좋습니다.

Puppeteer 예시

// 로그인 후 쿠키 저장
const cookies = await page.cookies();
const fs = require('fs');
fs.writeFileSync('cookies.json', JSON.stringify(cookies, null, 2));

// 다음 실행 시 쿠키 로드
const savedCookies = JSON.parse(fs.readFileSync('cookies.json'));
await page.setCookie(...savedCookies);

Playwright 예시

// 로그인 후 스토리지(state) 저장
await context.storageState({ path: 'state.json' });

// 다음 실행 시 스토리지 로드
const context = await browser.newContext({ storageState: 'state.json' });

5. 자주 발생하는 오류와 해결책

오류원인해결 방법
TimeoutError: waiting for selector로그인 후 페이지 전환이 느리거나, 선택자가 잘못됨waitForSelector에 충분한 timeout 지정하거나, 올바른 CSS 선택자 확인
401 Unauthorized쿠키/세션이 유효하지 않음쿠키 저장/로드 로직을 검증하고, 로그인 단계가 정상적으로 완료됐는지 확인
net::ERR_CERT_COMMON_NAME_INVALIDHTTPS 인증서 오류 (자체 서명)ignoreHTTPSErrors: true 옵션을 브라우저 런치 시 추가
ElementHandle is detached from the DOM페이지가 리로드된 뒤에 요소에 접근요소를 다시 찾거나, waitForNavigation 후에 작업 수행

6. 결론

  • 헤드리스 브라우저(Puppeteer / Playwright)를 사용하면 로그인된 상태를 그대로 재현할 수 있습니다.
  • 쿠키·스토리지(state)를 저장해 두면 매번 로그인 과정을 건너뛰어 성능을 크게 향상시킬 수 있습니다.
  • 오류가 발생하면 네트워크 대기, 선택자 정확도, HTTPS 옵션 등을 점검해 보세요.

이제 인증된 페이지의 스크린샷을 자동으로 생성할 수 있는 기반이 마련되었습니다. 필요에 따라 CI 파이프라인에 통합하거나, 정기적인 모니터링용 스크립트로 활용해 보세요. 🚀

Overview

공개 URL을 캡처하는 것은 간단합니다. 로그인 필요가 있는 대시보드, 인보이스, 혹은 어떤 페이지를 캡처하려면 헤드리스 브라우저 자동화가 한계에 부딪힙니다: 먼저 로그인하고, 세션 쿠키를 보존하고, 스크린샷 요청에 전달하며, 세션 만료를 처리해야 합니다.

PageBolt는 쿠키와 헤더를 직접 전달할 수 있게 해줍니다 — 로그인 자동화가 필요 없습니다.

세션 쿠키 전달

앱이 쿠키 기반 세션을 사용하는 경우(대부분의 서버‑렌더링 앱이 그렇습니다):

const res = await fetch("https://pagebolt.dev/api/v1/screenshot", {
  method: "POST",
  headers: {
    "x-api-key": process.env.PAGEBOLT_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    url: "https://yourapp.com/dashboard",
    cookies: [
      {
        name: "session",
        value: "your-session-token-here",
        domain: "yourapp.com",
      },
    ],
    fullPage: true,
    blockBanners: true,
  }),
});

const image = Buffer.from(await res.arrayBuffer());

헤더를 통해 Bearer 토큰 전달

SPA 및 Authorization 헤더를 사용하는 API의 경우:

const res = await fetch("https://pagebolt.dev/api/v1/screenshot", {
  method: "POST",
  headers: {
    "x-api-key": process.env.PAGEBOLT_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    url: "https://yourapp.com/reports/q4-2024",
    headers: {
      Authorization: `Bearer ${userToken}`,
    },
    fullPage: true,
  }),
});

여러 쿠키 (예: CSRF + 세션)

일부 프레임워크에서는 세션 쿠키와 CSRF 토큰을 모두 필요로 합니다:

body: JSON.stringify({
  url: "https://yourapp.com/admin/users",
  cookies: [
    { name: "session_id", value: sessionId, domain: "yourapp.com" },
    { name: "csrf_token", value: csrfToken, domain: "yourapp.com" },
  ],
}),

Source:

앱 내부에서 스크린샷 생성 (서버‑사이드)

내부 도구에 가장 깔끔한 패턴: 서버에서 단기간 토큰을 생성하고, URL에 삽입한 뒤 페이지를 스크린샷하고, 토큰을 만료시킵니다.

// Express route — generates signed URL, screenshots it, returns PDF
app.get("/invoices/:id/pdf", requireAuth, async (req, res) => {
  const { id } = req.params;

  // Create a single‑use signed token
  const token = await db.signedTokens.create({
    invoiceId: id,
    userId: req.user.id,
    expiresAt: new Date(Date.now() + 60_000), // 1 minute
  });

  const signedUrl = `https://yourapp.com/invoices/${id}?token=${token.value}`;

  const pdfRes = await fetch("https://pagebolt.dev/api/v1/pdf", {
    method: "POST",
    headers: {
      "x-api-key": process.env.PAGEBOLT_API_KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ url: signedUrl }),
  });

  const pdf = Buffer.from(await pdfRes.arrayBuffer());

  // Expire the token immediately
  await db.signedTokens.expire(token.id);

  res.setHeader("Content-Type", "application/pdf");
  res.setHeader(
    "Content-Disposition",
    `attachment; filename="invoice-${id}.pdf"`
  );
  res.send(pdf);
});

이 방법은 서드‑파티 서비스에 장기 인증 정보를 전달하지 않으면서, 토큰을 60초 동안만 유효하게 하고 사용 후 즉시 무효화합니다.

사용자를 대신하여 대시보드 캡처

SaaS 앱에서 사용자의 계정 상태 스크린샷을 이메일로 보내야 할 경우:

async function captureUserDashboard(userId) {
  // DB에서 사용자의 현재 세션 토큰을 가져옵니다
  const user = await db.users.findById(userId);
  const sessionToken = await generateSessionToken(user);

  const res = await fetch("https://pagebolt.dev/api/v1/screenshot", {
    method: "POST",
    headers: {
      "x-api-key": process.env.PAGEBOLT_API_KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      url: `https://yourapp.com/users/${userId}/dashboard`,
      cookies: [
        {
          name: "session",
          value: sessionToken,
          domain: "yourapp.com",
        },
      ],
      fullPage: true,
      blockBanners: true,
      blockAds: true,
    }),
  });

  return Buffer.from(await res.arrayBuffer());
}

// 모든 사용자에게 주간 대시보드 스크린샷 전송
async function sendWeeklyDigest() {
  const users = await db.users.findActive();
  for (const user of users) {
    const image = await captureUserDashboard(user.id);
    await email.send({
      to: user.email,
      subject: "Your weekly summary",
      attachments: [{ filename: "dashboard.png", content: image }],
    });
  }
}

Python 예제

import os
import requests

def screenshot_authenticated(url: str, session_cookie: str) -> bytes:
    resp = requests.post(
        "https://pagebolt.dev/api/v1/screenshot",
        headers={"x-api-key": os.environ["PAGEBOLT_API_KEY"]},
        json={
            "url": url,
            "cookies": [
                {
                    "name": "session",
                    "value": session_cookie,
                    "domain": url.split("/")[2],
                }
            ],
            "fullPage": True,
            "blockBanners": True,
        },
    )
    resp.raise_for_status()
    return resp.content

image = screenshot_authenticated(
    "https://yourapp.com/dashboard",
    session_cookie="your-session-token"
)
with open("dashboard.png", "wb") as f:
    f.write(image)

What doesn’t work (and why)

The alternative — automating login via a sequence of clicks — is fragile because it depends on UI stability, timing, and can break whenever the login flow changes. Using PageBolt’s cookie/header injection is far more reliable and maintainable.

Challenges with Traditional Authentication

  • Login forms change layout → automation breaks
  • CAPTCHAs and bot detection block headless browsers
  • MFA flows are hard to automate
  • Session cookies from automated logins expire differently than API tokens

Solution

Passing a cookie or header directly sidesteps all of that. Generate a token server‑side where you control the auth logic, pass it to the capture request, and expire it immediately after.

Try it free — 100 requests/month, no credit card. → Get started in 2 minutes

0 조회
Back to Blog

관련 글

더 보기 »

현대 웹 앱에서 OTP 인증 시작하기

왜 OTP 인증이 중요한가 - 로그인 또는 회원가입 시 사용자 신원을 확인합니다 - 가짜 계정 생성을 방지합니다 - 추가적인 보안 계층을 제공합니다 - 일반적으로 사용됩니다

27일차 #100DaysOfCode — REST API

당신이 인식하든 못하든, 앱이 요청을 보내고 응답을 받을 때마다 이미 REST API를 사용하고 있습니다. 당신의 날씨 앱, 소셜 피드…