Puppeteer로 웹 페이지 캡쳐하기(feat. Docker, Cloudflare)

조경민·2024년 5월 3일
0
post-thumbnail

Puppeteer

Puppeteer는 크롬/크로미움 브라우저의 DevTools 프로토콜의 API를 활용하여 웹 페이지의 콘텐츠를 스크래핑 하거나 이미지로 캡처하는 등의 기능을 지원하는 Node.js 라이브러리입니다. 기본적으로는 크롬의 Headless 모드로 실행되지만 마치 크롬 브라우저를 실제로 실행하는 것과 같은 Headful한 작업을 처리할 수 있습니다. 대표적인 용례는 다음과 같습니다.

  • 폼 제출, UI 테스팅, 키보드 입력 등의 자동화
  • 최신 JavaScript 및 브라우저를 사용한 테스트 환경 구축
  • 시간대별 브라우저 동작 기록을 통한 성능 이슈 진단
  • 크롬 확장프로그램 테스트
  • 웹 페이지 화면 캡쳐 및 PDF 생성
  • SPA/SSR 형식으로 로드된 웹 페이지의 콘텐츠 스크래핑

실습

Puppeteer를 활용하여 웹 브라우저를 실제로 실행하지 않고 Ubuntu 컨테이너 상에서 Express 웹 서버를 구동한 후 브라우저에서 주소를 입력해 스크린샷을 캡처하는 실습을 진행해보겠습니다. Cloudflare R2 스토리지를 연동하여 캡처된 이미지를 외부에서 조회할 수 있도록 세팅하게 되며, 버킷과 토큰 등 세팅에 필요한 값에 대한 가이드는 이전의 포스트 Spring Boot, React, Cloudflare R2를 활용한 파일 업로드/다운로드 기능 구현하기를 참고해주세요.

버전 정보

  • Node.js v18

GitHub 저장소

실습은 아래 저장소의 Spring Boot, React 어플리케이션을 통해 진행됩니다. 저장소를 clone 하거나 fork 해주세요.

따라하기

Peppeteer 세팅하기

웹 페이지 캡처 동작 및 저장이 일어나는 app.js 파일의 내용은 다음과 같습니다.

require("dotenv").config();

const puppeteer = require("puppeteer");
const AWS = require("aws-sdk");
const { v4: uuidv4 } = require("uuid");
const express = require("express");

const fs = require("fs");
const path = require("path");

const app = express();
const port = process.env.PORT || 3000;

const awsAccessKey = process.env.AWS_ACCESS_KEY || "";  // Cloudflare R2 액세스 키
const awsSecretKey = process.env.AWS_SECRET_KEY || "";  // Cloudflare R2 시크릿 키
const awsS3EndpointUrl = process.env.AWS_S3_ENDPOINT_URL || ""; // Cloudflare R2 엔드포인트 URL
const awsS3Bucket = process.env.AWS_S3_BUCKET || "";    // Cloudflare R2 버킷 이름

app.use(express.urlencoded({ extended: true }));
app.use(express.static(path.join(__dirname, 'public')));
app.use((req, res, next) => {
  if (req.url.endsWith('.css')) {
      res.type('text/css');
  }
  next();
});

app.set("views", path.join(__dirname, "views"));
// views 라는 이름의 디렉토리를 참조
app.set("view engine", "ejs");
// EJS 템플릿 엔진 사용

app.get("/", (req, res) => {
  res.render("index", { pageUrl: "" });
});
// 루트 경로(/) 접속 시 index.ejs 렌더링

app.post("/", async (req, res) => {
  const { pageUrl } = req.body;

  const browser = await puppeteer.launch({
    headless: true,
    args: [
      '--no-sandbox', // 샌드박스 비활성화
      '--disable-setuid-sandbox', // 샌드박스 비활성화
      '--disable-dev-shm-usage', // 메모리 부족 방지
      '--disable-gpu', // GPU 비활성화
    ],
    ignoreHTTPSErrors: true,
  });

  const page = await browser.newPage();
  await page.goto(pageUrl, { 
    waitUntil: "networkidle0", // 페이지가 완전히 로드된 상태까지 대기
    timeout: 120000  // 타임아웃 기준 2분
  }); 
  await page.setViewport({ width: 1920, height: 1024 });  // 뷰포트 설정

  const screenshotPath = "screenshot.png";
  await page.screenshot({ path: screenshotPath });

  await browser.close();

  const s3 = new AWS.S3({
    accessKeyId: awsAccessKey,
    secretAccessKey: awsSecretKey,
    endpoint: awsS3EndpointUrl,
  });

  const awsS3Filename = generateRandomFilename();

  const uploadParams = {
    Bucket: awsS3Bucket,
    Key: awsS3Filename,
    Body: fs.createReadStream(screenshotPath),
  };

  s3.upload(uploadParams, function (err, data) {
    if (err) {
      console.error("에러 발생:", err);
      res.status(500).send("파일 업로드 중에 오류가 발생했습니다.");
    } else {
      res.send(`스크린샷이 업로드되었습니다.`);
    }
  });
});

function generateRandomFilename() { // 저장할 파일의 이름을 난수로 설정
  const uuid = uuidv4();
  return `${uuid}.png`;
}

app.listen(port, () => {
  console.log(`서버가 포트 ${port}에서 실행 중입니다.`);
});
  • browser는 Puppeteer에 의해 생성된 브라우저 객체로 볼 수 있으며, launch()라는 메서드를 통해 구동됩니다.

  • page는 브라우저에서 탭을 띄우는 것과 비슷하게 실제로 웹 페이에 접속하는 객체를 의미합니다. 접속 대상 주소나 뷰포트 등의 설정이 이 page 객체에 대하여 적용됩니다.

  • 컨테이너 내부에 스크린샷으로 생성된 이미지 파일이 저장되면 브라우저의 할 일이 끝났으므로 close() 메서드를 통해 종료를 하고 이어서 생성된 이미지 파일을 Cloudflare의 R2 스토리지로 전송하게 됩니다. R2는 AWS SDK와 호환성을 가지므로 NPM의 aws-sdk 라이브러리를 활용하였습니다.

  • 저장할 스크린샷 파일의 이름에 난수를 활용하고 확장자를 지정하기 위해 generateRandomFilename() 이라는 함수를 만들었습니다. 여기서 uuid라는 패키지가 사용됩니다.

Dockerfile 세팅하기

Linux 컨테이너에서 Puppeteer를 활용하기 위해서는 Dockerfile을 알맞게 작성해주어야 합니다.

FROM node:18

RUN apt-get update && apt-get install -y \
    ca-certificates \
    fonts-liberation \
    libasound2 \
    libatk-bridge2.0-0 \
    libatk1.0-0 \
    libc6 \
    libcairo2 \
    libcups2 \
    libdbus-1-3 \
    libexpat1 \
    libfontconfig1 \
    libgbm1 \
    libgcc1 \
    libglib2.0-0 \
    libgtk-3-0 \
    libnspr4 \
    libnss3 \
    libpango-1.0-0 \
    libpangocairo-1.0-0 \
    libstdc++6 \
    libx11-6 \
    libx11-xcb1 \
    libxcb1 \
    libxcomposite1 \
    libxcursor1 \
    libxdamage1 \
    libxext6 \
    libxfixes3 \
    libxi6 \
    libxrandr2 \
    libxrender1 \
    libxss1 \
    libxtst6 \
    lsb-release \
    wget \
    xdg-utils \
    chromium \
    fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf libxss1 \
    --no-install-recommends \
    && rm -rf /var/lib/apt/lists/*

USER node

WORKDIR /app

COPY --chown=node package.json .
COPY --chown=node package-lock.json .

ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true
ENV PUPPETEER_EXECUTABLE_PATH /usr/bin/chromium

RUN npm install

COPY --chown=node . /app

CMD ["npm", "start"]
  • 컨테이너를 Non-Root 권한으로 실행하기 위해 USER node 명령을 추가합니다. 공식 node 컨테이너 이미지에서 node 사용자는 uid 1000 값을 가집니다.

  • PUPPETEER_EXECUTABLE_PATH 환경변수에 Chromium 브라우저 실행 경로를 /usr/bin/chromium 세팅합니다.

Dockerfile로 Node.js 서비스 배포

  1. 클라우드타입에 로그인 후 우측 네비바의 ➕ 버튼을 눌러 새 프로젝트 창을 띄우고 프로젝트 이름과 표시 이름을 입력한 뒤 생성하기 버튼을 누릅니다.

  2. 프로젝트 설정에 진입하여 변수-시크릿 항목에 다음과 같이 R2 토큰 정보를 입력 후 저장합니다.

    • aws-access-key: R2 토큰 액세스키 ID
    • aws-secret-key: R2 토큰 시크릿키
    • aws-s3-endpoint: R2 엔드포인트(https:// 제외)
  3. 클라우드타입의 프로젝트 페이지에서 ➕ 버튼을 누르고 Dockerfile을 선택한 후, 미리 fork 해놓은 puppeteer-dockerfile 를 선택합니다. 기타 설정은 아래를 참고하여 입력한 후 배포하기 버튼을 클릭합니다.

    • 환경변수(Environment Variables)

      • AWS_ACCESS_KEY: 열쇠 아이콘 => aws-access-key
      • AWS_SECRET_KEY: 열쇠 아이콘 => aws-secret-key
      • AWS_S3_BUCKET: 생성한 R2 버킷이름
      • AWS_S3_ENDPOINT_URL: 열쇠 아이콘 => aws-s3-endpoint
    • 포트(Port): 3000

  4. 배포 완료 후 접속하기 버튼을 눌러 서비스에 접속하면 주소를 입력할 수 있는 필드가 나타닙니다. https://cloudtype.io 주소를 입력 후 캡처하기 버튼을 누릅니다.

  5. 스크린샷 캡처 및 R2 스토리지 저장이 완료되면 다음과 같이 페이지가 전환됩니다.

  1. R2 대시보드에서 스크린샷 저장 위치로 설정한 버킷에 접속하면 저장된 스크린샷 파일을 확인할 수 있습니다.

Reference

profile
Live And Let Live!

0개의 댓글