Puppeteer는 크롬/크로미움 브라우저의 DevTools 프로토콜의 API를 활용하여 웹 페이지의 콘텐츠를 스크래핑 하거나 이미지로 캡처하는 등의 기능을 지원하는 Node.js 라이브러리입니다. 기본적으로는 크롬의 Headless 모드로 실행되지만 마치 크롬 브라우저를 실제로 실행하는 것과 같은 Headful한 작업을 처리할 수 있습니다. 대표적인 용례는 다음과 같습니다.
Puppeteer를 활용하여 웹 브라우저를 실제로 실행하지 않고 Ubuntu 컨테이너 상에서 Express 웹 서버를 구동한 후 브라우저에서 주소를 입력해 스크린샷을 캡처하는 실습을 진행해보겠습니다. Cloudflare R2 스토리지를 연동하여 캡처된 이미지를 외부에서 조회할 수 있도록 세팅하게 되며, 버킷과 토큰 등 세팅에 필요한 값에 대한 가이드는 이전의 포스트 Spring Boot, React, Cloudflare R2를 활용한 파일 업로드/다운로드 기능 구현하기를 참고해주세요.
실습은 아래 저장소의 Spring Boot, React 어플리케이션을 통해 진행됩니다. 저장소를 clone 하거나 fork 해주세요.
웹 페이지 캡처 동작 및 저장이 일어나는 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
라는 패키지가 사용됩니다.
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
세팅합니다.
클라우드타입에 로그인 후 우측 네비바의 ➕ 버튼을 눌러 새 프로젝트 창을 띄우고 프로젝트 이름과 표시 이름을 입력한 뒤 생성하기 버튼을 누릅니다.
프로젝트 설정에 진입하여 변수-시크릿 항목에 다음과 같이 R2 토큰 정보를 입력 후 저장합니다.
클라우드타입의 프로젝트 페이지에서 ➕ 버튼을 누르고 Dockerfile을 선택한 후, 미리 fork 해놓은 puppeteer-dockerfile 를 선택합니다. 기타 설정은 아래를 참고하여 입력한 후 배포하기 버튼을 클릭합니다.
환경변수(Environment Variables)
AWS_ACCESS_KEY
: 열쇠 아이콘 => aws-access-keyAWS_SECRET_KEY
: 열쇠 아이콘 => aws-secret-keyAWS_S3_BUCKET
: 생성한 R2 버킷이름AWS_S3_ENDPOINT_URL
: 열쇠 아이콘 => aws-s3-endpoint포트(Port): 3000
배포 완료 후 접속하기 버튼을 눌러 서비스에 접속하면 주소를 입력할 수 있는 필드가 나타닙니다. https://cloudtype.io
주소를 입력 후 캡처하기 버튼을 누릅니다.
스크린샷 캡처 및 R2 스토리지 저장이 완료되면 다음과 같이 페이지가 전환됩니다.