웹 스크래핑과 브라우저 자동화는 현대 웹 개발과 데이터 수집 영역에서 필수적인 기술이 되었습니다. 특히 동적 콘텐츠가 많은 현대 웹사이트를 다룰 때는 강력한 도구가 필요합니다. 이번 글에서는 마이크로소프트에서 개발한 강력한 브라우저 자동화 프레임워크인 Playwright를 소개하고, 이를 활용한 웹 스크래핑 기법을 자세히 살펴보겠습니다.

Playwright 소개: 개념과 주요 용도
Playwright란 무엇인가?
Playwright는 웹 브라우저 자동화 및 end-to-end 테스트를 위한 현대적인 프레임워크입니다. 기존의 Selenium이나 Puppeteer와 같은 도구들의 한계를 극복하기 위해 마이크로소프트에서 개발되었으며, 크로스 브라우저 테스트와 자동화에 중점을 두고 있습니다.
// Playwright의 기본 구조
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('https://example.com');
// 다양한 작업 수행...
await browser.close();
})();
Playwright의 주요 특징
Playwright는 다음과 같은 주요 특징을 가지고 있어 테스트와 스크래핑 작업에 매우 적합합니다:
- 크로스 브라우저 지원: Chromium, Firefox, WebKit(Safari) 등 모든 주요 브라우저 엔진을 단일 API로 제어
- 다양한 플랫폼 지원: Windows, Linux, macOS에서 동일하게 작동
- 자동 대기 메커니즘: 요소의 로딩, 가시성, 활성화 등을 자동으로 처리하여 안정적인 테스트와 스크래핑 구현
- 강력한 셀렉터 엔진: CSS, XPath, 텍스트 기반 선택자 등 다양한 방법으로 요소 검색
- 네트워크 제어: 요청 가로채기, 모킹, 수정 등 네트워크 활동 제어
- 모바일 에뮬레이션: 모바일 디바이스 환경 테스트 지원
⠀
Playwright을 이용한 테스트 자동화
Playwright의 원래 주요 목적은 웹 애플리케이션의 end-to-end 테스트입니다. 테스트 자동화 분야에서 Playwright는 어떤 장점과 기능을 제공하는지 살펴보겠습니다.
기본 테스트 작성 방법
Playwright Test 프레임워크를 사용하면 간결하고 강력한 테스트를 작성할 수 있습니다:
// example.spec.js
import { test, expect } from '@playwright/test';
test('기본 테스트', async ({ page }) => {
// 페이지 이동
await page.goto('https://playwright.dev/');
// 타이틀 검증
const title = page.locator('.navbar__inner .navbar__title');
await expect(title).toHaveText('Playwright');
// 링크 클릭
await page.click('text=Get Started');
// URL 검증
await expect(page).toHaveURL(/.*intro/);
});
테스트 실행 방법
테스트를 실행하는 방법은 매우 간단합니다:
# 모든 테스트 실행
npx playwright test
# 특정 브라우저에서만 실행
npx playwright test --project=chromium
# UI 모드로 실행 (시각적 디버깅)
npx playwright test --ui
효과적인 테스트 전략
Playwright를 이용한 효과적인 테스트 전략은 다음과 같습니다:
- 페이지 객체 모델 사용: 재사용 가능한 컴포넌트로 테스트 구조화
- 자동 대기 활용: 명시적 지연 대신 Playwright의 자동 대기 메커니즘 활용
- 병렬 실행: 여러 브라우저에서 동시에 테스트 실행하여 시간 단축
- 시각적 비교 테스트: 스크린샷을 활용한 시각적 회귀 테스트
- 네트워크 모킹: 테스트 환경 독립성을 위한 API 응답 모킹
⠀
Playwright를 활용한 웹 스크랩 개요
Playwright는 테스트 도구로 시작했지만, 강력한 브라우저 제어 기능 덕분에 웹 스크래핑에도 탁월한 성능을 발휘합니다.
웹 스크래핑에 적합한 이유
Playwright가 웹 스크래핑에 적합한 이유는 다음과 같습니다:
- JavaScript 렌더링 완전 지원: SPA(Single Page Application)와 같은 JavaScript 기반 애플리케이션도 완벽하게 렌더링
- 자동 대기 기능: 요소가 로드되고 상호작용 가능해질 때까지 자동으로 대기
- 강력한 네비게이션 제어: 다중 페이지, 탭, iframe 처리 가능
- 다양한 입력 방식: 키보드, 마우스, 터치 등 다양한 사용자 입력 시뮬레이션
- 인증 세션 관리: 로그인 상태 유지 및 쿠키 관리 용이
⠀
기본 스크래핑 패턴
Playwright를 이용한 기본적인 웹 스크래핑 패턴은 다음과 같습니다:
const { chromium } = require('playwright');
(async () => {
// 브라우저 시작 (헤드리스 모드)
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
try {
// 대상 페이지로 이동
await page.goto('https://example.com');
// 필요한 요소가 로드될 때까지 대기
await page.waitForSelector('.content');
// 데이터 추출
const data = await page.evaluate(() => {
// 브라우저 컨텍스트에서 실행되는 코드
const items = Array.from(document.querySelectorAll('.item'));
return items.map(item => ({
title: item.querySelector('.title')?.textContent,
description: item.querySelector('.description')?.textContent,
url: item.querySelector('a')?.href
}));
});
console.log('수집된 데이터:', data);
// 결과 저장 (예: JSON 파일)
require('fs').writeFileSync('results.json', JSON.stringify(data, null, 2));
} catch (error) {
console.error('스크래핑 중 오류 발생:', error);
} finally {
// 브라우저 종료
await browser.close();
}
})();
고급 스크래핑 기법
더 복잡한 웹사이트를 스크래핑하기 위한 고급 기법들:
- 페이지네이션 처리: 여러 페이지에 걸친 데이터 수집
- 무한 스크롤 처리: 스크롤 이벤트에 의해 로드되는 콘텐츠 수집
- 인증 우회: 로그인이 필요한 페이지 접근
- 차단 회피: IP 차단 및 CAPTCHA 우회 전략
- 병렬 스크래핑: 여러 페이지 동시 처리
⠀
Ruby로 웹 스크랩하기 예제: Hackernews 웹스크랩
Playwright는 JavaScript 외에도 다양한 언어 바인딩을 제공합니다. 자바스크립트 예제는 많이 나와 있고 또 저는 프로젝트에서 루비를 많이 사용하기에 여기서도 Ruby를 사용하여 Hacker News 웹사이트를 스크래핑하는 실용적인 예제를 살펴보겠습니다.

Ruby용 Playwright 클라이언트 설정
Ruby에서 Playwright를 사용하기 위해 먼저 필요한 설정을 해봅시다:
# Gemfile
source 'https://rubygems.org'
gem 'playwright-ruby-client'
gem 'json'
Playwright 드라이버 설치:
npm init -y
npm install playwright
npx playwright install
HackerNews 스크래핑 코드
이제 Ruby를 사용하여 HackerNews의 주요 기사를 스크래핑하는 코드를 작성해보겠습니다:
# hackernews_scraper.rb
require 'playwright'
require 'json'
class HackerNewsScraper
def initialize(max_retries: 3)
@max_retries = max_retries
end
def scrape
retries = 0
while retries < @max_retries
begin
puts "스크래핑 시작 (시도 #{retries + 1}/#{@max_retries})..."
articles = scrape_hackernews
save_articles(articles)
return articles
rescue => e
puts "오류 발생: #{e.message}"
puts e.backtrace
retries += 1
if retries >= @max_retries
puts "최대 재시도 횟수를 초과했습니다."
raise e
end
sleep_time = retries * 3
puts "#{sleep_time}초 후 재시도합니다..."
sleep(sleep_time)
end
end
end
private
def scrape_hackernews
articles = []
Playwright.create(playwright_cli_executable_path: './node_modules/.bin/playwright') do |playwright|
playwright.chromium.launch(headless: false) do |browser|
puts '브라우저를 시작합니다...'
# 새 컨텍스트 생성
context = browser.new_context(
viewport: { width: 1280, height: 800 },
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
)
page = context.new_page
# 페이지 설정
page.set_default_timeout(30000) # 30초 타임아웃
# HackerNews로 이동
puts 'HackerNews로 이동합니다...'
page.goto('https://news.ycombinator.com/', waitUntil: 'networkidle')
# 스크린샷 촬영
timestamp = Time.now.strftime('%Y%m%d_%H%M%S')
page.screenshot(path: "hackernews_#{timestamp}.png")
puts '뉴스 기사를 수집합니다...'
# 뉴스 항목 선택자
items = page.locator('tr.athing')
count = items.count
puts "#{count}개의 항목을 발견했습니다."
# 각 항목 처리
(0...count).each do |i|
item = items.nth(i)
# 필요한 요소 추출
id = item.get_attribute('id')
title_element = item.locator('td.title > span.titleline > a')
title = title_element.text_content
url = title_element.get_attribute('href')
site_element = item.locator('span.sitestr')
site = site_element.count > 0 ? site_element.text_content : ''
# 다음 행 찾기
subtext = page.locator("tr:has(#score_#{id})").last
# 점수 추출
score_element = subtext.locator('.score')
score = score_element.count > 0 ? score_element.text_content : 'No score'
# 댓글 수 추출
comment_element = subtext.locator("a[href=\"item?id=#{id}\"]").last
comments = comment_element.count > 0 ? comment_element.text_content : '0 comments'
# 작성자 추출
author_element = subtext.locator('.hnuser')
author = author_element.count > 0 ? author_element.text_content : 'Unknown'
articles << {
id: id,
title: title,
url: url,
site: site,
score: score,
comments: comments,
author: author
}
end
puts "#{articles.length}개의 기사를 수집했습니다."
end
end
articles
end
def save_articles(articles)
timestamp = Time.now.strftime('%Y%m%d_%H%M%S')
filename = "hackernews_#{timestamp}.json"
File.write(filename, JSON.pretty_generate(articles))
puts "결과를 #{filename}에 저장했습니다."
filename
end
end
scraper = HackerNewsScraper.new
begin
scraper.scrape
puts "스크래핑이 성공적으로 완료되었습니다."
rescue => e
puts "스크래핑 실패: #{e.message}"
end
위 스크립트를 실행하면 아래와 같이 스크랩한 결과가 JSON 파일에 저장됩니다.
데이터 분석 추가
이제 스크래핑한 데이터를 간단히 분석하는 코드를 추가해봅시다:
# analyzer.rb
require 'json'
class HackerNewsAnalyzer
def initialize(filename)
@filename = filename
@articles = JSON.parse(File.read(filename), symbolize_names: true)
puts "#{@articles.length}개의 기사를 분석합니다..."
end
def analyze
# 점수 추출 및 숫자로 변환
scores = @articles.map do |article|
score_text = article[:score]
score_match = score_text.match(/(\d+)/)
score_match ? score_match[1].to_i : 0
end
# 기본 통계 계산
total_score = scores.sum
avg_score = total_score.to_f / scores.length
max_score = scores.max
min_score = scores.select { |s| s > 0 }.min
# 도메인별 글 개수
domain_counts = Hash.new(0)
@articles.each do |article|
domain = article[:site].to_s.empty? ? '(no domain)' : article[:site]
domain_counts[domain] += 1
end
# 상위 도메인 추출
top_domains = domain_counts.sort_by { |_, count| -count }.take(5)
# 결과 출력
puts "\n===== 분석 결과 ====="
puts "총 기사 수: #{@articles.length}"
puts "평균 점수: #{avg_score.round(2)} 포인트"
puts "최고 점수: #{max_score} 포인트"
puts "최저 점수: #{min_score} 포인트"
puts "\n상위 5개 도메인:"
top_domains.each_with_index do |(domain, count), index|
puts "#{index + 1}. #{domain}: #{count}개 기사"
end
# 최고 점수 기사 찾기
top_article = @articles.max_by do |article|
score_match = article[:score].match(/(\d+)/)
score_match ? score_match[1].to_i : 0
end
puts "\n최고 점수 기사:"
puts "제목: #{top_article[:title]}"
puts "URL: #{top_article[:url]}"
puts "점수: #{top_article[:score]}"
puts "작성자: #{top_article[:author]}"
end
end
# 최신 파일 찾기
files = Dir['hackernews_*.json'].sort
if files.empty?
puts "분석할 데이터 파일이 없습니다."
exit(1)
end
latest_file = files.last
puts "최신 파일 #{latest_file}을 분석합니다."
analyzer = HackerNewsAnalyzer.new(latest_file)
analyzer.analyze
분석 결과는 다음과 같습니다(이 글을 쓰는 시점의 분석 결과입니다)
$ ruby analyzer.rb
최신 파일 hackernews_20250411_162217.json을 분석합니다.
30개의 기사를 분석합니다...
===== 분석 결과 =====
총 기사 수: 30
평균 점수: 111.73 포인트
최고 점수: 578 포인트
최저 점수: 6 포인트
상위 5개 도메인:
1. developer.mozilla.org: 1개 기사
2. lesswrong.com: 1개 기사
3. sciencedaily.com: 1개 기사
4. garfieldminusgarfield.net: 1개 기사
5. centminmod.com: 1개 기사
최고 점수 기사:
제목: Garfield Minus Garfield
URL: https://garfieldminusgarfield.net
점수: 578 points
작성자: mike1o1
Ruby의 다른 스크래핑 라이브러리와 비교
참고로, Playwright 외에도 Ruby에는 다양한 웹 스크래핑 라이브러리가 있습니다:
라이브러리 | 주요 특징 | Playwright와 비교했을 때 |
---|---|---|
Nokogiri | HTML/XML 파서, CSS/XPath 선택자 | JavaScript 미지원, 정적 페이지만 가능 |
Mechanize | 폼 작성, 버튼 클릭 지원 | 제한적 JavaScript 지원, 실제 브라우저 미사용 |
Watir | Selenium 기반 브라우저 제어 | 더 오래된 기술, 더 느림 |
Kimurai | 고급 스크래핑 프레임워크 | 종합 프레임워크이나 성능 면에서 불리 |
Ferrum | Chrome DevTools 프로토콜 기반 | Chrome만 지원 |
Playwright는 모든 주요 브라우저를 지원하고, 자동 대기 메커니즘이 뛰어나며, 성능이 우수하다는 장점이 있습니다. 특히 현대적인 JavaScript 기반 웹사이트를 다룰 때 가장 강력한 옵션입니다.
Playwright MCP
최근 마이크로소프트는 AI와 LLM(대규모 언어 모델)이 웹 브라우저와 상호작용할 수 있게 해주는 새로운 도구인 Playwright MCP(Model Context Protocol)를 출시했습니다.
Playwright MCP란?
Playwright MCP는 LLM이 웹 페이지와 상호작용할 수 있도록 하는 서버입니다. 스크린샷이나 시각적 모델을 사용하지 않고 구조화된 접근성 스냅샷(accessibility snapshot)을 통해 웹 페이지와 상호작용합니다.
주요 특징:
- 빠르고 가벼운 동작: 픽셀 기반 입력이 아닌 접근성 트리 사용
- LLM 친화적: 비전 모델 불필요, 구조화된 데이터로 작동
- 결정적 도구 적용: 스크린샷 기반 접근에서 흔한 모호성 제거
⠀
MCP 사용 방법
VS Code에서 Playwright MCP를 설정하는 방법:
# VS Code에서
code --add-mcp '{"name":"playwright","command":"npx","args":["@playwright/mcp@latest"]}'
MCP 서버 구성 예시:
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": [
"@playwright/mcp@latest"
]
}
}
}
MCP 주요 명령어
Playwright MCP는 다양한 브라우저 조작 명령을 제공합니다:
// URL로 이동
browser_navigate({ url: "https://example.com" });
// 요소 클릭하기
browser_click({
element: "Login 버튼",
ref: "button-login-123"
});
// 텍스트 입력하기
browser_type({
element: "이메일 입력란",
ref: "input-email-456",
text: "[email protected]",
submit: true
});
// 스냅샷 캡처
const snapshot = await browser_snapshot();
MCP와 일반 Playwright의 차이점
항목 | 일반 Playwright | Playwright MCP |
---|---|---|
대상 사용자 | 개발자와 QA 엔지니어 | AI 에이전트와 LLM |
사용 방식 | 직접 코드 작성 | 구조화된 명령 인터페이스 |
페이지 분석 | CSS/XPath 선택자 | 접근성 트리 또는 스크린샷 |
주요 용도 | 웹 테스트, 스크래핑 | AI 기반 웹 자동화 |
마무리
Playwright는 현대적인 웹 테스트 자동화와 스크래핑을 위한 강력한 도구입니다. 모든 주요 브라우저를 지원하며, 뛰어난 안정성과 성능으로 복잡한 웹 환경에서도 원활하게 작동합니다.
Playwright의 주요 장점 요약
- 크로스 브라우저 호환성: 모든 주요 브라우저에서 일관된 동작
- 자동 대기 메커니즘: 안정적인 테스트와 스크래핑을 위한 자동화된 대기
- 강력한 API: 복잡한 사용자 상호작용 시뮬레이션 가능
- 언어 지원: JavaScript, TypeScript, Python, Java, .NET, Ruby 등 지원
- 병렬 실행: 빠른 테스트와 스크래핑을 위한 병렬 처리
- 최신 웹 기술 지원: Shadow DOM, Web Components 등 최신 웹 기술 지원⠀
활용 시나리오
Playwright는 다음과 같은 다양한 시나리오에서 활용할 수 있습니다:
- 웹 애플리케이션 테스트 자동화: end-to-end 테스트 구현
- 데이터 수집 및 모니터링: 웹 사이트의 데이터를 주기적으로 수집
- 콘텐츠 마이그레이션: 한 플랫폼에서 다른 플랫폼으로 콘텐츠 이전
- 경쟁사 분석: 경쟁사 웹사이트의 가격, 제품, 기능 등을 모니터링
- AI와의 통합: MCP를 통한 AI 에이전트의 웹 자동화
앞으로의 발전 방향
Playwright는 계속해서 발전하고 있으며, 다음과 같은 방향으로 나아가고 있습니다:
- 보다 강력한 AI 통합: LLM과의 긴밀한 통합으로 더 지능적인 브라우저 자동화
- 성능 개선: 더 빠르고 안정적인 테스트 실행
- 새로운 브라우저 기능 지원: 최신 웹 표준과 기능에 대한 지속적인 지원
- 생산성 도구 확장: 코드 생성, 디버깅, 분석 도구의 개선
⠀
Playwright는 기존의 Selenium, Puppeteer 등의 도구를 뛰어넘는 기능과 성능을 제공하며, 웹 테스트 자동화와 스크래핑의 새로운 표준으로 자리잡고 있습니다. 특히 JavaScript 기반의 복잡한 웹 애플리케이션을 다룰 때 강력한 선택지가 될 것입니다.
처음엔 Playwright에 대한 소개를 하는 글을 쓰려고 했는데, 쓰다보니 어째 루비 웹스크랩에 더 많은 비중을 둔 게 아닌가 싶네요.
이 글을 통해 Playwright의 기본 개념부터 실제 사용 사례까지 살펴보았습니다. 웹 스크래핑과 테스트 자동화가 필요한 프로젝트에 Playwright를 도입하여 더 효율적이고 안정적인 솔루션을 구축해 보세요!