ego (lite)는 그저 브라우저일 뿐이고, ego는 여러 기기를 넘나드는 나만의 에이전트입니다.
대기자 명단 등록
웹 스크래핑PlaywrightJavaScript 렌더링브라우저 자동화ego (lite)

Playwright 웹 스크래핑: 렌더링된 카드를 기다리고, 필드를 추출하고, 건수를 검증하기

2026년 9월 18일12 분 분량
초록색 ego (lite) 마스코트가 파란 바닥에서 div, button, section 같은 HTML 태그를 진공청소기로 빨아들이는 모습

JavaScript로 렌더링되는 페이지는 응답이 성공이어도 스크레이퍼 행이 0건일 수 있습니다. HTML은 로드됐지만, 브라우저에서 사람이 보는 카드가 아직 DOM에 없을 수 있습니다. ego (lite)에서는 그 차이가 보입니다. 작업은 지켜볼 수 있는 Chromium Space 안에서 돌아가므로, 아직 렌더링 중인지, 셀렉터가 맞지 않는지, 필요한 데이터가 한 번도 나타나지 않았는지를 눈으로 확인할 수 있습니다.

Playwright는 그 확인된 동작을 안정적으로 반복할 수 있는 스크래핑으로 만듭니다. 대상 카드가 그려질 때까지 기다리고, 라이브 DOM에서 필드를 추출하고, 결과에 기대한 데이터가 들어 있는지 검증할 수 있습니다. 셀렉터와 대기 조건이 이미 알려져 있다면, 이를 신뢰할 수 있는 스크립트로 옮기는 것은 자연스럽습니다. 아직 눈으로 확인해야 하거나, 기존 로그인 세션이 필요하거나, 특정 단계에서 사람이 넘겨받아야 한다면 ego (lite)가 브라우저 상태를 보이게 두고 조작 가능하게 유지합니다. 종료 코드 0인 스크립트가 실제로는 아무것도 긁지 못했다는 사실을 숨기지 못합니다.

Playwright 웹 스크래핑이란

Playwright 웹 스크래핑은 실제 브라우저를 구동해 JavaScript가 페이지를 그리게 한 뒤, 렌더링된 노드를 읽는 일입니다. 스크레이퍼는 브라우저입니다. HTML 응답은 스크립트가 돌기 전까지의 껍데기일 뿐입니다.

Playwright는 Chromium, Firefox, WebKit을 실행하고 context를 연 뒤 page.goto, 대기, locator, page.evaluate를 제공합니다. 이 API는 테스트용입니다. 추출에도 통합니다. 둘 다 필요한 것은 사람이 보는 것과 맞는 DOM이기 때문입니다.

인접한 작업은 이 페이지에서 다루지 않습니다.

JavaScript에서 fetch와 브라우저 중 무엇을 고를지는 JavaScript 웹 스크래핑에서 다룹니다. 실행을 넘어 로그인 세션을 재사용하는 방법은 지속 브라우저 세션에서 다룹니다. X와 LinkedIn의 로그인 장벽은 로그인 장벽 너머의 AI 스크래핑에서 다룹니다. 번호 페이지, Load More, 무한 스크롤은 이 한 페이지가 이미 올바른 행을 반환한 뒤에 다룹니다.

HTTP 대신 Playwright로 스크래핑해야 하는 경우

스크래핑하려는 노드가 URL GET에 없을 때 Playwright를 씁니다. 값이 이미 응답 본문에 있거나, 페이지가 문서화된 JSON 엔드포인트를 제공한다면 HTTP를 씁니다.

quotes.toscrape.com/js는 그 갈림길을 의도적으로 만듭니다. URL GET이 반환하는 HTML에는 class="quote" 노드가 없습니다. 인용은 인라인 var data 배열에 있고, jQuery가 카드를 그립니다. 원시 응답의 .quote를 향한 CSS 파서는 아무것도 저장하지 못하고 셀렉터 버그처럼 보입니다.

그 갈림길은 2026-09-18에 OpenCode에서 확인했습니다. quotes.toscrape.com/js의 GET은 HTTP 200이었고 원시 HTML의 .quote 카드는 0건이었습니다. 오른쪽 headed Chromium 창에는 이미 그려진 목록이 있었습니다.

원시 HTML의 quote 카드 0건과 HTTP 200을 확인하는 OpenCode, 옆에 quotes.toscrape.com/js를 보여주는 headed Chromium
갈림길의 HTTP 쪽. 소스의 .quote 카드는 0건이고, 보이는 페이지에는 이미 목록이 있습니다. 왼쪽이 OpenCode, 오른쪽이 독립 Chromium입니다.

갈림길은 이렇게 봅니다.

경로할 수 있는 일할 수 없는 일
HTTP GET + HTML 파서서버가 보낸 바이트를 읽습니다. 저렴하고 빠르며, 카드가 이미 마크업에 있으면 충분합니다.페이지 스크립트를 실행하거나 나중에 나타나는 노드를 기다리지는 못합니다. quotes.toscrape.com/js에서는 원시 HTML에 .quote 카드가 보이지 않습니다.
인라인 페이로드 파싱quotes.toscrape.com/js에서는 var data를 정규식과 JSON.parse로 같은 HTML에서 인용을 복구할 수 있고, 브라우저는 필요 없습니다.배열 삽입을 그만두는 개편에는 버티지 못합니다. 내부 페이로드는 API 계약이 아닙니다.
Playwright 렌더링 DOM페이지 JavaScript를 실행하고 .quote를 기다린 뒤, 사람이 보는 카드를 추출합니다.저렴하지 않습니다. context마다 메모리를 쓰고, 정적 HTML에 여러 대를 두는 것은 잘못된 도구입니다.

이 데모에서 솔직히 덧붙이면, var data를 직접 파싱하면 Playwright는 필요 없습니다. 대부분의 JS 사이트는 첫 응답에 이름 있는 배열을 남기지 않습니다. 남기지 않을 때도 통하는 경로가 Playwright입니다.

실행, 이동, 콘텐츠 대기는 어떻게 하나

Playwright 스크래핑의 최소 구성은 실행, 새 context, goto, 카드 셀렉터 대기, 추출, close입니다. 대기를 건너뛰면 성공한 빈 파일이 나옵니다.

Playwright의 page.goto는 commit, domcontentloaded, load, networkidle에서 멈출 수 있습니다. waitForSelector는 목적물이 실제로 있는지를 확인하는 검사입니다. quotes.toscrape.com/js에서는 인라인 스크립트가 그 HTML 안에서 돌기 때문에 goto({ waitUntil: 'domcontentloaded' })만으로도 충분한 경우가 있습니다. 그건 운이지 규칙이 아닙니다.

import { chromium } from "playwright";

const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();

try {
  await page.goto("https://quotes.toscrape.com/js/", {
    waitUntil: "domcontentloaded",
  });
  await page.waitForSelector(".quote");
  const rows = await page.evaluate(() =>
    [...document.querySelectorAll(".quote")].map((el) => ({
      text: el.querySelector(".text")?.textContent?.trim() ?? "",
      author: el.querySelector(".author")?.textContent?.trim() ?? "",
      tags: [...el.querySelectorAll(".tag")].map((tag) =>
        tag.textContent?.trim(),
      ),
    })),
  );
  console.log(rows.length);
} finally {
  await context.close();
  await browser.close();
}

빈 스크래핑을 보려면 카드가 생기기 전 commit에서 추출하거나, 그리는 스크립트를 늦춥니다. 그 이른 추출은 0행을 반환합니다. waitForSelector('.quote') 이후에는 같은 페이지가 렌더링된 카드를 반환합니다. 빈 경우에도 예외는 나지 않습니다.

대기증명하는 것실패하는 방식
commit만탐색이 시작됐다는 것. 네거티브 테스트로는 쓸모가 있습니다.스크립트가 아직 돌지 않았을 수 있습니다. 그리기 전 commit에서 추출하면 인용은 0건입니다.
domcontentloaded최초 HTML이 파싱됐다는 것. 이 픽스처에서는 var data가 그 HTML 안에서 돌기 때문에 충분할 수 있습니다.늦은 XHR 카드는 아직 빠질 수 있습니다. '콘텐츠 준비 완료'로 보지 마세요.
waitForSelector('.quote')DOM에 카드가 적어도 한 장 있다는 것. 이 데모를 추출 가능한 행으로 만드는 대기입니다.낡은 class가 이전 목록에 맞을 수 있습니다. 기다리는 것은 데이터와 함께 바뀌는 필드이지, 절대 사라지지 않는 래퍼가 아닙니다.

context는 finally에서 닫습니다. 메모리를 쥐고 있는 것은 context입니다. 추출 도중 예외가 나도 Chromium은 놓아야 합니다.

렌더링된 페이지에서 필드를 추출하는 방법

추출은 page.evaluate로 페이지 안에서 하거나, 목록이 아니라 컨트롤 하나가 필요하면 locator를 씁니다. 둘 다 렌더링된 DOM을 읽습니다. goto 시점의 원본 HTML 스냅샷은 읽지 않습니다.

각 .quote를 text, author, tags로 매핑합니다. 공개 데모는 카드 10장을 그립니다. 필수 필드는 text와 author입니다. 고유 키는 author가 아니라 인용문입니다. Einstein이 두 번 이상 나오기 때문입니다.

그 추출도 같은 OpenCode 세션에서 확인했습니다. waitForSelector('.quote') 이후 headed 실행은 10행, 고유 text 10건, 빠진 author 0건을 출력했습니다. 원시 HTML의 카드는 여전히 0건이었습니다.

추출한 quote 행 10건을 보여주는 OpenCode와 quotes.toscrape.com/js의 headed Chromium
Playwright 추출. 대기 후 고유 카드 10건. 왼쪽이 OpenCode, 오른쪽이 라이브 목록입니다. Next 클릭도 로그인도 없습니다.

단일 필드라면 locator가 더 깔끔합니다. Playwright locators는 각 동작 전에 다시 쿼리하므로, 저장한 element handle이 깨지는 재렌더에도 버팁니다.

const title = await page.locator(".quote .text").first().innerText();
const author = await page.locator(".quote .author").first().innerText();

목록 추출에는 page.evaluate가 더 낫습니다. 실행은 in the page에서 이뤄지고 JSON을 반환합니다. mapper는 작게 유지하고 text, author, tags, 고유 키만 가져옵니다. 스크린샷과 PDF는 스크래핑 루프에 넣지 마세요.

CSS로 숨긴 텍스트도 DOM에는 남아 있습니다. innerText는 레이아웃을 따르고, textContent는 따르지 않습니다. 사이트가 CSS로 제목을 잘라도 textContent는 전체 문자열을 줍니다. 검증기를 쓰기 전에 어느 쪽을 모을지 정하세요.

페이지 자신의 데이터를 가로채는 방법

카드에서 파싱하는 대신 저장할 수 있는 JSON을 페이지가 이미 가져오고 있다면 가로채기가 유용합니다. quotes.toscrape.com/js에서는 그 요청이 한 번도 나가지 않고, 그게 핵심입니다.

그 데모의 인용은 API에서 오지 않습니다. HTML에 var data로 들어가 있고, jQuery가 .quote 노드로 그립니다. JSON을 찾는 response listener는 쓸모 있는 것을 보지 못합니다.

Playwright의 network docs는 page.route와 response listener를 보여 줍니다. 쓰세요. 그다음 페이지가 실제로 보낸 것, 아무것도 보내지 않은 경우까지 기록합니다.

const jsonResponses = [];
page.on("response", async (response) => {
  const type = response.headers()["content-type"] || "";
  if (type.includes("json")) {
    jsonResponses.push({
      url: response.url(),
      status: response.status(),
    });
  }
});
await page.goto(url, { waitUntil: "domcontentloaded" });
await page.waitForSelector(".quote");
console.log(jsonResponses.length);

JSON 본문이 없으면 렌더링된 카드로 돌아가거나, 이미 받은 인라인 스크립트를 파싱합니다. 튜토리얼이 항상 보여 준다고 엔드포인트를 만들지 마세요. Oxylabs, BrowserStack, ScraperAPI는 모두 가로채기를 보여 줍니다. 사이트가 요청을 한 번도 보내지 않을 때 무슨 일이 일어나는지는 말하지 않습니다.

필드, 중복, 건수를 검증하는 방법

스크래핑의 끝은 스크립트가 종료 코드 0으로 끝나는 것이 아닙니다. 파일이 기대 건수를 갖고, 필수 필드가 모두 있으며, 고유 키가 두 행을 한 행으로 합치지 않을 때입니다.

이 데모에서 통과하는 추출은 고유 text를 가진 10행을 쓰고, 빠진 author는 없습니다. 카드가 생기기 전 추출은 건수를 보지 않으면 같은 밝은 로그로 0행을 씁니다.

function validate(rows, expectedCount) {
  const missing = rows.filter((row) => !row.text || !row.author);
  const unique = new Set(rows.map((row) => row.text));
  if (rows.length !== expectedCount) {
    throw new Error(`expected ${expectedCount}, got ${rows.length}`);
  }
  if (missing.length) {
    throw new Error(`${missing.length} rows missing text or author`);
  }
  if (unique.size !== rows.length) {
    throw new Error("duplicate texts");
  }
}

짧은 체크리스트를 쓰고, 실패는 크게 냅니다.

확인통과남겨 둘 실패 형태
기대 건수waitForSelector('.quote') 이후 카드 10장그리기 전에 추출하면 0행
필수 필드모든 행에 text와 author카드에는 맞지만 .author를 놓치는 셀렉터
고유 키고유한 것은 인용문이지 author가 아님author로 중복 제거하면 Einstein이 합쳐짐

빈 추출은 남깁니다. 0행 결과가 대기가 중요하다는 증거입니다. 지우면 이후의 완전한 추출이 기본값처럼 보입니다.

Playwright 스크래핑이 깨지는 원인

여기서 중요한 실패는 빈 렌더, 낡은 셀렉터, 속도 제한입니다. 예외를 삼키는 catch 뒤에 행 길이만 로그하면 어느 것도 빨간 스택 트레이스처럼 보이지 않습니다.

빈 렌더. 탐색은 성공했고, 셀렉터는 한 번도 맞지 않았고, []를 저장했습니다. 고칠 것은 대기입니다. 끝나지 않은 같은 DOM에 같은 추출을 다시 돌리지 마세요.

낡은 셀렉터. .quote는 남아 있지만 텍스트가 이동했습니다. 실행은 어제의 10행을 반환합니다. 콘텐츠와 함께 바뀌는 필드를 기다린 뒤, 고유 키를 이전 파일과 비교하세요.

속도 제한. 429는 정지 신호입니다. Retry-After가 있으면 읽고, 백오프하고, 재시도에 한도를 둡니다. 같은 URL을 두드리면 IP가 차단됩니다. 우회는 이 가이드의 범위가 아닙니다.

async function gotoWithRetry(page, url, attempt = 0) {
  const response = await page.goto(url, { waitUntil: "domcontentloaded" });
  const status = response?.status() ?? 0;
  if (status === 429 || status >= 500) {
    if (attempt >= 3) throw new Error(`giving up on ${url}: ${status}`);
    const retryAfter = Number(response?.headers()["retry-after"]);
    const waitMs = Number.isFinite(retryAfter)
      ? retryAfter * 1000
      : 2 ** attempt * 1000;
    await page.waitForTimeout(waitMs);
    return gotoWithRetry(page, url, attempt + 1);
  }
  if (status && status >= 400) throw new Error(`${status} for ${url}`);
  return response;
}

눈에 보이는 브라우저 에이전트가 더 나은 경우

대기가 틀렸거나, 셀렉터가 움직였거나, 사람이 페이지를 보고 넘겨받아야 할 때 눈에 보이는 브라우저 에이전트가 더 낫습니다. CI에서 이미 믿는 고정 Playwright 루프의 대체재가 아닙니다.

보완이지 경쟁이 아닙니다. Playwright는 이미 아는 대기를 코드로 옮깁니다. ego (lite) 0.5.0.32는 카드가 나타나는 모습을 지켜볼 수 있는 Chromium Space에 작업을 두고, 에이전트를 멈추거나 대기가 거짓말할 때 사람이 넘겨받을 수 있게 합니다. 다음 스크래핑에 이미 있는 로그인이 필요하면 실제 프로필을 재사용합니다. 정적 HTML을 더 싸게 만들지도, 차단을 우회하지도 않습니다. 이 버전의 Changelog 날짜는 2026-09-12입니다. 자세한 내용은 ego (lite) 변경 로그.

그 격리는 OpenCode에서 ego (lite)로 확인했습니다. Spaces 개요는 quotes 스크래핑을 단독 실행 Space에 두고, 다른 작업은 별도 Space로 나눴습니다. headed Chromium 창 하나를 공유하지 않았습니다.

quotes 스크래핑이 단독 Space에서 돌아가는 ego (lite) Spaces 개요와 옆의 OpenCode
같은 작업을 격리된 Space로. ego (lite)는 스크래핑을 한 Space에 두고 다른 작업은 다른 Space에 둘 수 있습니다. headed Chromium 창 하나를 공유할 필요가 없습니다.

같은 공개 URL을 그런 Space 하나에서 확인했습니다. Space 4는 quotes.toscrape.com/js에서 에이전트 제어를 유지했고, Take over와 Stop이 보였으며, 고유 카드 10건을 출력했습니다.

quotes.toscrape.com/js의 ego (lite) Space에 Agent is in control, Take over, Stop이 보이는 상태를 옆 OpenCode가 ego-browser로 조작하는 모습
지켜보는 스크래핑. 왼쪽이 OpenCode, 오른쪽이 ego (lite) Space 하나. quote 카드가 보이는 동안에도 에이전트 제어입니다.

헤드리스와 headed의 트레이드오프는 AI 에이전트를 위한 헤드리스 브라우저 vs 실제 브라우저에서 다룹니다. 라이브러리 선택은 스크래핑을 위한 Playwright vs Puppeteer에서 다룹니다. 제품 수준 대비는 ego (lite) vs Playwright.

과제와 한계

Playwright 스크래핑은 HTTP보다 조용히 실패합니다. 브라우저는 멀쩡해 보여도 파일이 틀릴 수 있기 때문입니다.

첫 한계는 비용입니다. Chromium context 하나는 GET 한 번이 아닙니다. 카드가 이미 HTML에 있다면 필요 없는 브라우저 값을 내는 것입니다.

두 번째는 가로채기에 대한 정직함입니다. 튜토리얼은 XHR 캡처를 고급 기술로 다룹니다. 이 데모에서 고급 기술은 페이로드가 인라인이라고 인정하는 것입니다.

세 번째는 페이지 넘김이고, 여기서는 한 줄 한계로 남깁니다. 렌더링된 한 페이지가 검증된 뒤에야 번호 페이지, Load More, 무한 스크롤이 별도 일이 됩니다. 그 루프를 이 대기-추출 페이지에 섞으면 빈 DOM 실패가 가려집니다.

네 번째는 허가입니다. robots 규칙, 약관, 개인정보법은 그대로 적용됩니다. headed 브라우저가 수집할 권리를 만들지는 않습니다.

스크래핑이 비었는데 페이지는 가득 차 보이면, 카드를 기다린 뒤 행 수를 세요. headed 창은 증거가 아닙니다. 이 픽스처에서 가로채기도 JSON XHR을 찾지 못했고, 인용은 이미 var data로 인라인에 있었습니다.

FAQ

Playwright로 웹 스크래핑해도 되나

눈에 보이는 카드가 원시 HTML에 없고 실제 브라우저에서 기다릴 수 있다면 Playwright는 스크래핑에 맞습니다. quotes.toscrape.com/js가 공개 예입니다. GET은 .quote 노드를 반환하지 않고, .quote 대기로 그려진 카드를 읽습니다. 정적 HTML이나 문서화된 API의 기본값으로는 틀립니다.

Playwright 대신 HTTP를 써야 하는 경우

값이 이미 응답 본문에 있으면 HTTP를 씁니다. JavaScript 데모에는 .quote 카드가 없어도 인용이 든 var data는 남아 있습니다. 그 페이로드 파서가 안정적이면 브라우저는 건너뛰세요.

Playwright 스크래핑이 0건인 이유

0건은 대개 카드가 생기기 전에 추출했다는 뜻입니다. 이 데모에서는 그리기 전 commit에서 DOM을 읽으면 인용이 0건이고, waitForSelector('.quote') 이후에는 렌더링된 카드가 나오며, 빈 경우에도 예외는 나지 않습니다.

networkidle을 기다려야 하나

기다리는 것은 데이터를 나타내는 셀렉터이지, 조용한 네트워크가 아닙니다. networkidle은 분석 비콘에서 멈출 수 있습니다. 이 데모에서 통과하는 대기는 .quote이지 networkidle이 아닙니다.

Playwright로 JSON을 가로채는 방법

content-type에 json이 포함된 응답을 듣거나, 페이지가 이미 호출하는 URL을 route하세요. quotes.toscrape.com/js에서는 인용이 인라인이라 잡을 JSON XHR이 없습니다. 가로채기는 선택입니다. DOM 대기는 그래도 동작합니다.

Playwright로 페이지 넘김 목록을 스크래핑하는 방법

먼저 렌더링된 한 페이지를 끝냅니다. 대기, 추출, 건수 검증입니다. 번호 페이지, Load More, 무한 스크롤은 별도 일입니다. 이 페이지는 그 루프를 구현하지 않습니다.

Playwright 스크래핑에서 로그인을 재사용하는 방법

사용이 허용된 로그인 뒤에 스토리지 상태를 저장하고, 이후 context에서 불러옵니다. 파일은 시크릿으로 취급하고 만료를 전제로 하세요.

429가 오면 어떻게 하나

멈추고, Retry-After가 있으면 지키고, 백오프하고, 작은 재시도 한도 뒤에 실패시키세요. 429는 속도 신호이지 셀렉터 문제가 아닙니다.

Playwright 스크래핑에 ego (lite)가 필요한가

아니요. 대기와 셀렉터를 이미 알면 Playwright입니다. 지켜볼 수 있는 Space, 실제 로그인 프로필, 사람 인계가 필요하면 ego (lite)입니다. 2026-09-18 두 경로 모두 quotes.toscrape.com/js에서 고유 카드 10건을 추출했고, Space에서는 Take over와 Stop이 보였습니다.

실제 브라우저 스크래핑은 합법인가

사이트, 데이터, 관할, 파일 사용 방식에 따라 다릅니다. headed 브라우저에서도 robots 규칙과 약관은 적용됩니다. 법률 자문이 아닙니다.

스크래핑은 Playwright인가 Puppeteer인가

둘 다 같은 페이지를 렌더링할 수 있습니다. 라이브러리 선택은 locator, 대기, 언어 바인딩입니다. 이 페이지의 HTTP 대 브라우저 판단이 먼저입니다.

다음 스크래핑이 또 다른 대기 코드가 아니라 지켜볼 수 있는 브라우저가 필요하다면, ego (lite)는 무료로 내려받을 수 있습니다. 가격 스크레이퍼 안내가 그 Space에서 하는 구체적인 작업 하나입니다.