스케일링 헤드리스 브라우저: 컨텍스트 관리 vs. 인스턴스

발행: (2026년 1월 8일 오전 07:00 GMT+9)
18 분 소요
원문: Dev.to

Source: Dev.to

Introduction

엔드‑투‑엔드 테스트, 웹 스크래핑, 혹은 합성 모니터링 등 모든 브라우저 자동화 프로젝트의 라이프사이클에는 명확한 한계점이 존재합니다.

  • 처음에는 시스템이 완벽하게 동작합니다. 몇 개의 스크립트가 실행되어 작업을 수행하고 종료합니다.
  • 비즈니스 요구가 더 높은 처리량을 요구하면서(동시 세션을 10개에서 1,000개로 확장) 인프라가 한계에 부딪힙니다:
    • CPU가 100 %까지 급증합니다
    • 메모리 사용량이 급증하여 OOM(Out‑of‑Memory) 킬러가 프로세스를 종료하기 시작합니다
    • “Flaky”(불안정한) 타임아웃이 일반화됩니다

많은 엔지니어들의 직관은 수평 확장하는 것입니다: 더 많은 파드, 서버, 그리고 브라우저 컨테이너를 추가하는 것이죠. 하지만 이 방법은 현대 웹 브라우저의 막대한 무게가 만든 확고한 한계에 부딪힙니다. 표준 Chromium 인스턴스는 단순한 프로그램이 아니라, 자체 커널과 유사한 자원 관리, 복잡한 네트워킹 스택, 그리고 그래픽 렌더링 파이프라인을 갖춘 사실상의 보조 운영 체제입니다.

이 확장 병목 현상의 해결책은 단순히 “더 많은 하드웨어”가 아닙니다. 브라우저 라이프사이클을 관리하는 방식에 근본적인 변화를 요구합니다. 우리는 비용이 많이 드는 Instance‑per‑Session 모델(과거 Selenium과 연관된)에서 벗어나, Playwright와 같은 최신 프레임워크가 주창하는 Context‑based 아키텍처를 채택해야 합니다.

왜 순진한 스케일링이 실패하는가 – chromium.launch()를 호출하면 무슨 일이 일어나는가

현대 브라우저(Chrome, Firefox)는 안정성과 보안을 위해 설계된 다중 프로세스 아키텍처에 의존합니다. 단일 브라우저 인스턴스를 시작한다고 해서 하나의 OS 프로세스만 생성되는 것이 아니라 프로세스 트리가 생성됩니다:

Process typeRole
Browser Process중앙 코디네이터; 애플리케이션 상태를 관리하고 다른 프로세스를 조정하며 네트워크 요청 및 디스크 접근을 처리합니다.
GPU Process래스터화와 컴포지팅 명령을 처리합니다(헤드리스 모드에서도 SwiftShader와 같은 소프트웨어 래스터라이저를 통해 수행).
Utility Processes네트워크 서비스, 오디오 서비스, 스토리지 서비스 등 – 각각 샌드박스화되어 있습니다.
Renderer Processes탭/iframe당 하나씩 존재하며 V8 JavaScript 엔진과 Blink 렌더링 엔진을 포함합니다.

새로운 브라우저 인스턴스를 실행할 때마다 OS는 이 모든 코디네이터 프로세스에 대한 메모리를 할당하고, 공유 라이브러리(libGLES, libnss, …)를 로드하며, GPU 인터페이스를 초기화하고, 프로세스 간 통신(IPC) 파이프를 구축해야 합니다.

  • 콜드 부팅 시 RAM 사용량: 시작 직후 페이지가 로드되기 전까지 50 MB – 150 MB 정도가 즉시 차지됩니다.
  • CPU 비용: 셰이더 컴파일, V8 격리(Isolate) 초기화 등으로 수백 밀리초가 소요됩니다.

아키텍처가 들어오는 요청마다 새로운 브라우저 인스턴스를 생성하는 (Instance‑per‑Session 모델) 경우, 이 “고정 세금”을 반복해서 지불하게 됩니다. 100개의 동시 작업을 처리한다면 100개의 GPU 프로세스, 100개의 네트워크 서비스가 할당되고, 시스템 자원을 포화시키는 대규모 중복이 발생합니다.

브라우저 컨텍스트 – 가벼운 대안

Browser Context(Chrome에서 개념화하고 Puppeteer & Playwright에서 제품화)는 단일 브라우저 인스턴스 내에서 가벼운 논리적 격리 경계 역할을 하며, 시크릿(Incognito) 창에 비유할 수 있습니다.

const browser = await chromium.launch();
const context = await browser.newContext();   // 격리된 컨텍스트 생성

browser.newContext() 로 컨텍스트를 만들면 브라우저는 새 GPU 프로세스나 새 네트워크 서비스를 생성하지 않습니다. 대신 실행 중인 브라우저 인스턴스의 기존 무거운 인프라를 재사용합니다. 각 컨텍스트는 다음을 제공합니다:

  • 격리된 쿠키 저장소 – 컨텍스트 A의 쿠키는 컨텍스트 B에서 보이지 않음
  • 격리된 스토리지localStorage, sessionStorage, IndexedDB가 분리됨
  • 격리된 캐시 – (선택적으로) 각 컨텍스트가 자체 캐시 상태를 유지할 수 있음

모든 컨텍스트는 브라우저의 읽기 전용 리소스를 공유합니다:

  • V8 엔진용 컴파일된 머신 코드
  • 폰트 캐시
  • GPU 셰이더 프로그램

리소스 영향

  • 생성 시간: 한 자릿수 밀리초 수준
  • 메모리 사용량: 메가바이트(MB)가 아니라 킬로바이트(KB) 수준

단일 브라우저 프로세스는 따라서 수십, 심지어 수백 개의 격리된 사용자 세션을 동시에 호스팅할 수 있습니다.

Playwright에서는 Chrome DevTools Protocol (CDP)(또는 Firefox/WebKit에 해당하는 프로토콜)를 통해 이를 구현합니다. Playwright는 브라우저 프로세스에 단일 지속적인 WebSocket 연결을 열고, 이 연결을 사용해 새로운 “Target”(페이지/컨텍스트)을 생성하는 명령을 전송합니다. 이는 과거에 단일 프로세스에 대한 세밀한 제어가 어려웠던 레거시 WebDriver (HTTP) 모델과는 크게 대조됩니다.

여러 컨텍스트 오케스트레이션

컨텍스트를 확장하는 것은 단순히 메모리 문제만이 아니라 오케스트레이션 문제입니다. Playwright(및 Puppeteer)는 본질적으로 비동기이기 때문에 호스트 언어의 이벤트 루프(Node.js 또는 Python asyncio)에 의존합니다.

하나의 브라우저 안에서 50개의 컨텍스트를 실행하면, 사실상 50개의 동시 자동화 흐름이 단일 WebSocket 파이프를 통해 명령을 전송하게 됩니다.

핵심 기법

기법설명
Command BatchingPlaywright는 단일 연결을 통해 서로 다른 컨텍스트의 명령을 다중화하여 오버헤드를 줄입니다.
Cooperative Multitasking대부분의 자동화 작업은 I/O‑bound(네트워크 대기, 셀렉터 대기)입니다. 단일 스레드 Node.js/Python 프로세스가 수백 개의 컨텍스트를 효율적으로 오케스트레이션할 수 있습니다.
CPU scheduling병목 현상은 종종 RAM에서 CPU 스케줄링으로 이동합니다. 컨텍스트가 브라우저 프로세스를 공유하더라도, 컨텍스트 내의 각 Page(탭)는 결국 HTML을 파싱하고, JavaScript를 실행하며, 레이아웃을 렌더링하기 위해 렌더러 프로세스를 필요로 합니다. 적절한 스로틀링과 백프레셔 처리가 필수적입니다.

핵심 요약

  • Instance‑per‑Session → 높은 RAM, 높은 CPU, 확장성 부족
  • Context‑based → 낮은 RAM, 낮은 세션당 오버헤드, 높은 동시성

컨텍스트 중심 아키텍처를 채택하는 것은 수백 개의 헤드리스 브라우저를 소규모 머신 클러스터에서 실행하려는 모든 전략의 핵심입니다. 기본 프로세스 모델을 이해하고 Playwright의 비동기 오케스트레이션을 활용함으로써, 자원 부족 병목 현상을 고효율·확장 가능한 자동화 플랫폼으로 전환할 수 있습니다.

Source:

브라우저 컨텍스트 vs. 전체 브라우저 인스턴스

Chromium은 가능한 경우 렌더러 프로세스를 공유하려고 시도합니다(process‑per‑site‑instance). 하지만 무거운 페이지는 자체 OS‑레벨 렌더러를 생성합니다.

  • 컨텍스트는 추가 Browser/GPU 프로세스를 시작하는 오버헤드를 절감해 주지만, 페이지 실행 자체에 드는 비용은 절감하지 못합니다.
  • 50개의 컨텍스트를 열고 50개의 무거운 싱글 페이지 애플리케이션(SPA)을 로드하면, 50개의 V8 엔진이 동시에 React/Vue 컴포넌트를 hydrate하려고 하면서 CPU 사용량이 급증합니다.

프로덕션‑레디 아키텍처

browser.newContext()를 무한히 반복해서 호출할 수 없습니다. 관리형 아키텍처가 필요합니다.

  1. 브라우저 인스턴스 – 오래 유지되지만 유한함.
  2. 컨텍스트 – 일회성 작업 단위.

개념적 라이프사이클

단계설명
브라우저 시작chromium.launch()에 최적 플래그 사용(예: --disable-dev-shm-usage, --no-sandbox).
컨텍스트 임대애플리케이션이 컨텍스트를 요청합니다. 풀은 현재 브라우저에 “슬롯”이 남아 있는지 확인합니다(예: MAX_CONTEXTS_PER_BROWSER = 20).
실행컨텍스트가 생성되고 작업이 실행된 뒤 컨텍스트를 닫습니다.
교체브라우저 인스턴스가 N개의 컨텍스트(예: 1000)를 제공했거나 M분 동안 살아있으면, 새 컨텍스트를 받지 않고(드레인) 활성 컨텍스트가 끝난 뒤 정상적으로 종료합니다.

“브라우저 회전 내에서 컨텍스트 회전” 은 고규모 스크래핑에서 업계 표준입니다. 이는 컨텍스트의 빠른 시작 속도와 새 브라우저 인스턴스의 안정성을 균형 있게 맞춥니다.

컨텍스트 기반 스케일링의 위험

크래시 파급 범위

모델브라우저 프로세스 크래시 영향
인스턴스‑퍼‑세션크래시가 1 세션에 영향을 미칩니다.
컨텍스트‑기반크래시가 20‑50 세션에 영향을 미칩니다 (같은 브라우저 내 모든 컨텍스트).

완화 방안

  • browser.on('disconnected') 이벤트를 수신합니다.
  • 중단된 모든 작업을 새 인스턴스에서 다시 시도합니다.

시끄러운 이웃 CPU / 메모리 경쟁

컨텍스트 A가 메모리 누수가 있는 페이지나 암호화 채굴 스크립트를 로드하면, 같은 브라우저에서 실행 중인 컨텍스트 B의 CPU 사이클을 소모해 속도가 느려질 수 있습니다. 별도의 Docker 컨테이너와 달리, 컨텍스트별로 자원을 제한하는 cgroup이 없습니다.

완화 방안

  • 엄격한 타임아웃과 적극적인 페이지 닫기 로직을 구현합니다.
  • page.route 를 사용해 자동화 작업에 필요하지 않은 무거운 리소스(이미지, 폰트, 미디어)를 차단합니다.

공유 지문

컨텍스트는 쿠키를 격리하지만, 브라우저의 지문은 공유합니다:

  • 동일한 User‑Agent (별도로 재정의하지 않는 한)
  • 동일한 WebGL 벤더 문자열
  • 동일한 Canvas 해시

고급 안티봇 보호가 적용된 사이트를 스크래핑할 경우, 같은 브라우저에서 50개의 컨텍스트가 모두 동일하게 보입니다.

완화 방안

  • camoufox 같은 라이브러리나 수동 CDP 주입을 사용해 컨텍스트별로 지문 특성을 재정의합니다.
  • 매우 민감한 대상에 대해서는 각 세션이 고유한 지문을 갖도록 인스턴스‑기반 스케일링으로 전환합니다.

올바른 스케일링 모델 선택

모델격리CPU / 메모리 효율성운영 복잡도
Instance‑per‑Session완벽낮음 (각 인스턴스가 자체 리소스를 사용함)낮음
Context‑based부분적 (쿠키는 격리되고, 지문은 공유됨)높음 (수십 배 향상)높음 (오케스트레이션, 회전, 완화 필요)

추천 접근 방식

  • 자동화 사용 사례의 95 % (CI/CD 테스트, 내부 스크래핑, 스크린샷 생성) → Contexts가 최적의 선택입니다.
  • 고위험, 고가치 작업 (고유 지문, 절대적인 안정성) → Isolated instances.

하이브리드 전략이 종종 최고의 결과를 제공합니다:

  1. 대량 처리량을 위해 Contexts를 사용합니다.
  2. 고유 지문이 필요하거나 충돌 관련 다운타임을 전혀 허용할 수 없는 작업을 위해 Isolated instances를 예약합니다.

앞으로 전망 (2026 +)

브라우저 엔진이 무거워지고 클라우드 컴퓨팅 비용이 주요 KPI로 남아 있는 상황에서, processcontext의 구분을 마스터하는 것이 자동화 엔지니어의 핵심 역량이 될 것입니다.

  • Process‑level isolation → 최대 신뢰성, 비용 상승.
  • Context‑level isolation → 최대 효율성, 정교한 라이프사이클 관리 필요.

현명하게 선택하고, 견고한 회전 및 완화 방안을 구현하면 헤드리스‑브라우저 자동화의 전체 잠재력을 활용할 수 있습니다.

Back to Blog

관련 글

더 보기 »

카운터

bash mkfifo counter.fifo t=0 b=0 while read cmd; do case '$cmd' in inc_t) t++ ;; inc_b) b++ ;; get) echo '$t $b' ;; esac done counter.fifo echo '' echo '덤핑 중...'