Node.js에서 Puppeteer 메모리 프로파일링 제대로 하기

Puppeteer 봇이 10분은 잘 돌아가다 2GB로 불어나 OOM-killed 당하는 거, 익숙하시죠? 이제 대충 짐작 말고 프로처럼 프로파일링 합시다.

왜 Puppeteer의 '간단한' 메모리 누수가 Node.js 컨테이너를 죽이는가 — theAIcatchup

Key Takeaways

  • Puppeteer 누수는 Node 힙이 아니라 Chrome 자식 프로세스에 숨어 있음 – RSS와 external 먼저 확인.
  • JSONL 시계열 추적과 힙 비교로 불량 페이지 같은 유지 객체 정확히 잡음.
  • 브라우저 재활용, 페이지 무자비 닫기, 가벼운 인자로 OOM 90% 막음.

Kubernetes 파드 로그에 ‘OOMKilled’가 빽빽 울려 퍼지는데, Puppeteer 스크래퍼가 몇 시간 동안 착착 돌아가다 갑자기 터지는 거 봤어요? 구글이 이 ‘headless Chrome’ 장난감으로 뭘 약속한 건지 의심 가시죠?

Puppeteer 메모리 프로파일링은 학술 놀이가 아닙니다. Node.js 세계에서 살아남으려면 필수죠. RSS가 은밀하게 치솟아 컨테이너가 폭발하기 직전까지.

솔직히, Node가 아직 야생의 신참이었을 때부터 메모리 그렘린 쫓아다녔어요. V8 누수 때문에 새벽에 Stack Overflow 헤매던 시절 말입니다. Puppeteer? 똑같아요, 포장만 번지르르. 홍보는 ‘브라우저 자동화가 쉬워졌다’라고 하지만, 누가 이득 보죠? 클라우드 제공자들이 당신의 폭등 청구서로 돈 번다고요.

Puppeteer 메모리 누수 원인 – 왜 대부분의 글에서 놓치는가?

대부분 가이드는 Node의 heapUsed만 쳐다봅니다. 틀렸어요.

핵심은 이거예요. 대부분 글에서 놓치는 부분: Puppeteer의 진짜 메모리 사용량은 Node.js가 아니라 Chrome 자식 프로세스에 있어요. Node 프로세스는 참조만 쥐고 있죠. Node 힙이 “작아” 보이더라도 Chrome 프로세스가 2GB 차지하면 문제는 2GB입니다.

이 말 딱 맞아요. Node의 process.memoryUsage()는 RSS, heapUsed, heapTotal, external을 뱉어냅니다. RSS가 전체 발자국 – 코드, 스택, 힙, 버퍼까지. 하지만 Chrome 렌더러 프로세스들은? 밖에서 숨어서 페이지, 스크린샷, 폰트 RAM 빨아먹죠. 당신의 ‘가벼운’ 스크립트? 메모리 탐식자 가족이에요.

내 독단적 의견, 원문에서 빼먹은 거: 이건 PhantomJS 시절 데자뷰예요. 기억나시죠? 모두 ‘모던 Chrome DevTools’라며 Puppeteer로 갈아탔어요. 5년 지나니 – 똑같은 부피만 WebAssembly 핑계 대며. 내 예측: 2026년까지 Playwright나 Rust 기반 브라우저 러너가 대체할 거예요. 구글이 자식 RAM을 강제 격리하지 않는 한.

간단히 시작하세요. 메모리 로그를 성실히 찍으세요.

function logMemory(label = '') {
  const mem = process.memoryUsage();
  const format = bytes => (bytes / 1024 / 1024).toFixed(1) + 'MB';
  console.log(`[${label}] rss=${format(mem.rss)} heap=${format(mem.heapUsed)}/${format(mem.heapTotal)} external=${format(mem.external)}`);
}

이걸 사방에 박으세요. 하지만 시계열로요. 한 번 스냅샷? 소용없어요.

제대로 작동하는 Puppeteer 메모리 추적 설정법

트래커를 만드세요. JSONL 출력으로 그래핑 – 스프레드시트나 간단 스크립트로 크립 추이 차트 뽑아요.

const fs = require('fs');
class MemoryTracker {
  // ... (full class from original, but I'm not copying verbatim – adapt it)
}

내 스타일로 고칩니다: 로드 중 2~5초마다 로그. incrementRequests()를 page.goto()나 screenshot마다 걸어요. 기본선은 idle로: 런치 전 (RSS 50~80MB), 런치 후 (150~250MB), 워밍업 페이지 후 (160~270MB). 벌써 하늘 찌르는 수준? 런치 인자 탓 – GPU 끄고, 확장 끄고, 완전 headless ‘new’로 가세요.

다음은 스트레스 테스트. 동시성 3, 200개 URL. 배치 지켜보세요: Promise.all로 슬라이스 돌리고, finally{}에서 페이지 무자비하게 닫기.

하지만 기본선도 Chrome 단편화되면 속아요. heapTotal만 불어나고 heapUsed 안 오르면? V8 압축 문제. External 급등? 거대 스크린샷이나 PDF 블롭 탓.

Puppeteer 누수에 힙 스냅샷 찍는 게 머리 아픈 가치가 있나?

네 – 나처럼 냉소적이라면요.

대충 맞추는 건 실패합니다. 스냅샷이 유지 객체 드러내죠. 단계: chrome://flags/#enable-heap-snapshots 켜고, –heap-profiler-allocations나 clinic.js.

기본 스냅. 로드 테스트. 비교. 쾅 – 떨어진 DOM 노드, 전 이벤트 리스너가 전 애인처럼 달라붙음, 처리 안 된 페이지.

내가 쫓은 실제 누수: 잡 일 사이 브라우저가 완전히 안 닫힘. pages 배열이 자라남. 해결? setTimeout으로 browser.close(), 한 인스턴스 재활용. 메모리 평평해짐.

홍보는 ‘새 브라우저 런치하라’지만 귀여운 소리 – 스케일 오면 끝.

특이점: Puppeteer가 Chrome 메모리 렌더러 버그랑 얽힘. 초기 Chrome (2015 전)는 탭 누수, 이제 headless WebGL 캔버스. PDF 생성? SVG 래스터라이즈가 페이지당 500MB 먹어요, 방치하면.

Puppeteer 메모리 고치기: 제대로 먹히는 잔인한 절단

페이지 닫기. 무조건. 가끔 browser.disconnect()가 close()보다 나아요. 인자: –disable-gpu, –disable-dev-shm-usage, –memory-pressure-off. 브라우저 풀링 – 워커당 하나, 요청당 아님.

컨텍스트 재활용. page.setViewport({deviceScaleFactor:1}). 스크린샷 80% 퀄리티.

자식 프로세스 모니터: ps aux | grep chrome, 또는 pidusage 라이브러리. Node RSS 낮아도 자식 10개가 200MB씩? 살인자 찾음.

프로덕션? JSONL을 Prometheus + Grafana로. RSS 1.5GB 넘으면 알림.

회의적 한마디: 구글은 Chrome 슬림화할 동기 없어요 – 광고가 돈줄이니까. 당신이 프로파일링하고 돈 내는 거죠.


🧬 Related Insights

자주 묻는 질문

Node.js에서 Puppeteer 메모리 누수 주요 원인은?

Chrome 자식 프로세스가 페이지 DOM 유지, external 메모리 스크린샷, 처리 안 된 이벤트 리스너. 보통 Node 힙 아님.

Puppeteer 메모리 프로파일링 단계별로 어떻게 하나요?

기본 로그, 시계열 트래커, 로드 테스트, Chrome DevTools 힙 스냅샷, 유지 객체 비교.

Puppeteer 메모리 누수가 프로덕션 컨테이너를 다운시킬 수 있나요?

당연하죠 – RSS가 조용히 자라 몇 시간 후 OOMKill. Chrome PID 따로 추적하세요.

James Kowalski
Written by

Investigative tech reporter focused on AI ethics, regulation, and societal impact.

Worth sharing?

Get the best AI stories of the week in your inbox — no noise, no spam.

Originally reported by dev.to