microsoft/playwright · 제작 Microsoft · 라이선스 Apache-2.0 · 코어 버전 1.63.0-next(개발 중) · 언어=TypeScript(서버·클라이언트) + Python/Java/.NET 바인딩(별도 저장소) · Node.js ≥20 · 모노레포 packages/* 20여 개 · 소스 TS/TSX/JS 약 1,650개 파일 · 최신 커밋 0b2088e, 2026-07-29)
Playwright는 "브라우저를 코드로 운전하는 표준 계기판"이다. 로그인 폼에 값을 채우고 버튼을 누르고 결과 화면을 확인하는 흐름을, Chromium(크롬·엣지의 엔진)·Firefox·WebKit(사파리의 엔진) 어느 브라우저에서든 거의 똑같은 코드로 짤 수 있게 해준다. 저장소 이름은 하나지만 실제로는 packages/ 아래 서로 다른 역할을 하는 패키지 20여 개가 협업하는 모노레포(monorepo)다.
이 저장소가 특히 자랑하는 것은 두 가지다. 첫째는 테스트 러너(@playwright/test)까지 자체 제공한다는 점 — 단순 브라우저 조작 라이브러리가 아니라, 병렬로 테스트를 돌리고 리포트를 만들고 실패를 재현하는 트레이스(trace)까지 통째로 제공하는 "완결형 테스트 도구"다. 둘째는 모든 동작 앞에 자동으로 상태를 점검한다는 것 — 버튼이 화면에 보이고, 다른 요소에 가려지지 않고, 애니메이션이 멈춰 안정된 뒤에야 클릭을 실행한다.
브라우저 세 개는 각자 다른 언어(저수준 프로토콜)를 쓰는 세 대의 TV와 같다. Playwright의 테스트 코드는 리모컨 버튼(공통 API)만 누르면 되고, 뒤에 있는 "서버"라는 통역사가 그 버튼 입력을 각 TV가 알아듣는 언어로 바꿔 전달한다.
게다가 이 리모컨은 성격이 꼼꼼해서, 채널을 바꾸기 전에 "TV가 켜져 있는지, 화면이 지금 멈춰 있는지"를 스스로 확인하고 나서야 버튼을 누른다 — 이게 auto-waiting이다.
2023~2024년까지 Playwright는 "Selenium보다 빠르고 안정적인 E2E(End-to-End, 처음부터 끝까지) 테스트 도구"로 자리 잡았다. 그런데 2025~2026년 들어 AI 코딩 에이전트(Claude Code, Copilot 등)가 웹 화면을 직접 조작해야 하는 상황이 폭발적으로 늘면서, Playwright의 접근성 트리(accessibility tree) 기반 요소 인식 방식이 "에이전트가 스크린샷 없이도 화면을 정확히 읽고 클릭할 수 있는 유일한 실전 도구"로 재조명받고 있다. README 자체가 이제 "AI 에이전트를 위한 도구"라는 문구로 시작한다.
이 저장소는 단순 유지보수를 넘어, AI 에이전트 연동 전용 자매 프로젝트를 여럿 낳았다. 브라우저를 MCP(Model Context Protocol) 서버로 노출하는 @playwright/mcp, 토큰을 아끼는 전용 CLI인 @playwright/cli가 그것이다(둘 다 별도 저장소 microsoft/playwright-mcp·microsoft/playwright-cli로 분리돼 있다). 이 저장소 자체에도 packages/playwright/src/agents·mcp 폴더에 테스트를 자동 생성·자가치유(self-heal)하는 에이전트 정의가 포함돼 있다.
Chromium만 지원하는 도구(예: 초기 Puppeteer)와 달리, Playwright는 Firefox·WebKit까지 세 엔진을 같은 팀이 직접 브라우저를 패치해가며 지원한다. "크롬에서는 되는데 사파리에서 깨진다"는 흔한 사고를 CI 단계에서 미리 잡아낼 수 있다는 점이 여전히 강력한 채택 이유다.
자세한 비교는 10번 섹션에서 다루지만, 요지만 먼저: Selenium은 역사가 길고 언어 지원이 넓지만 API가 장황하고 대기 로직을 직접 짜야 하는 경우가 많다. Cypress는 개발 경험이 좋지만 같은 브라우저 탭 안에서 테스트 코드가 도는 구조적 제약 때문에 여러 탭·도메인을 넘나드는 시나리오가 약하다. Puppeteer는 Playwright와 같은 팀(Google Chrome팀 출신 인력)이 만든 조상뻘이지만 Chromium 중심이다. Playwright는 이 셋의 장점을 절충하며 "테스트 러너까지 자체 제공"이라는 완결성으로 차별화했다.
웹 자동화에서 초보자가 가장 먼저 부딪히는 벽은 "방금 로딩된 페이지의 어떤 요소가, 언제 클릭 가능한 상태가 되는가"다. 옛날 도구들은 이걸 사람이 직접 sleep(2000) 같은 하드코딩된 대기로 때웠다. 페이지가 빠르면 시간 낭비, 느리면 실패(flaky, 들쭉날쭉한 테스트)로 이어진다.
Playwright는 이 문제를 "모든 액션 앞에 자동으로, 실패할 때까지 재시도하며 상태를 확인하는" 구조로 해결한다. 클릭 한 줄을 써도 내부에서는 "요소가 붙어있는가(attached) → 보이는가(visible) → 안정됐는가(stable, 두 프레임 연속 같은 위치) → 다른 요소에 가려지지 않았는가(receives events) → 비활성화 상태가 아닌가(enabled)"를 순서대로, 정해진 시간 안에서 반복 확인한다.
두 번째로 해결하는 문제는 브라우저 파편화다. Chromium·Firefox·WebKit은 내부 프로토콜이 완전히 다르다. Playwright는 이 차이를 서버 계층 안에 가두고, 사용자에게는 브라우저 이름만 바꾸면 되는 동일한 API를 내준다.
Playwright의 근본 구조는 "클라이언트가 브라우저를 직접 만지지 않는다"는 것이다. 대신 클라이언트(사용자가 짜는 테스트 코드)와 서버(각 브라우저를 실제로 조종하는 프로세스) 사이에 양방향 프로토콜(채널)이 있고, 둘은 이 채널로만 대화한다. 언어별 바인딩(Python·Java·.NET)이 존재할 수 있는 것도 이 구조 덕분이다 — 각 언어는 "클라이언트" 부분만 자기 언어로 다시 구현하면 되고, 실제 브라우저 제어 로직(서버)은 이 저장소의 TypeScript 코드 하나로 공유된다.
이 분리 덕분에 세 가지가 가능해진다. 첫째, 원격 연결 — browserType.connect()로 다른 머신에서 도는 브라우저 서버에 붙을 수 있다(클라이언트와 서버가 물리적으로 떨어져도 프로토콜만 맞으면 된다). 둘째, 언어 독립성 — Python·Java·.NET 바인딩은 이 채널 프로토콜만 자기 언어로 구현하면 되므로, 브라우저 제어의 어려운 부분(서버)을 다시 짤 필요가 없다. 셋째, 격리 — 서버 프로세스가 죽어도 클라이언트가 그 사실을 채널 단절로 감지해 정리할 수 있다.
crConnection.ts)는 이 프로토콜을 그대로 사용한다.browser_patches/firefox/juggler 폴더에 그 소스가 있다.
클라이언트와 서버가 "같은 언어"로 대화하려면 메시지 형식이 어긋나면 안 된다. Playwright는 이걸 손으로 맞추는 대신, packages/protocol/spec/*.yml(page.yml·frame.yml·browser.yml 등 19개 파일)에 "이 객체는 어떤 명령·이벤트·초기값을 갖는가"를 선언적으로 적어두고, utils/generate_channels.js 스크립트가 이 YAML을 읽어 channels.d.ts(TypeScript 타입 선언)를 클라이언트·서버 양쪽에 자동 생성한다. 실행 시점 유효성 검증은 packages/protocol/src/validator.ts가 담당한다.
두 사람이 전화 통화를 하려면 같은 언어를 배워야 한다. Playwright는 그 언어(문법)를 YAML 사전 한 권으로 정의해두고, 통역 프로그램이 그 사전을 읽어 "말하는 쪽 교재"와 "듣는 쪽 교재"를 동시에 찍어낸다. 그래서 사전(YAML)만 고치면 양쪽 교재(타입)가 항상 같이 바뀐다 — 손으로 두 벌을 관리하다 어긋나는 사고가 구조적으로 안 난다.
| 브라우저 | 통신 방식 | 비고 |
|---|---|---|
| Chromium | CDP를 그대로 사용 (crConnection.ts) | 표준 프로토콜이라 패치 없이 대부분 제어 가능 |
| Firefox | 자체 확장 Juggler (ffConnection.ts) | browser_patches/firefox에 소스 패치 보관, 자체 빌드한 Firefox 배포 |
| WebKit | 패치된 내부 프로토콜 (wkConnection.ts) | browser_patches/webkit의 embedder(임베더 셸)로 감싼 커스텀 빌드 |
세 서버 모듈(server/chromium·server/firefox·server/webkit) 모두 *Page.ts·*Browser.ts·*NetworkManager.ts 같은 대칭적인 파일 구성을 갖는다 — 통신 방식은 다르지만, 그 위에서 "페이지·네트워크·입력을 어떻게 다룰지"는 같은 추상 인터페이스(server/page.ts의 PageDelegate 등)를 구현하는 방식으로 통일해, 상위 계층(dispatchers·frames.ts)은 어느 브라우저인지 신경 쓰지 않는다.
실제로 다운로드되는 브라우저 바이너리의 버전은 사람이 직접 편집하지 않는 packages/playwright-core/browsers.json이 유일한 진실원(source of truth)이다. 이 파일 맨 위에는 "이 파일을 직접 고치지 말고 utils/roll_browser.js를 쓰라"는 주석이 박혀 있다. npx playwright install은 이 JSON의 리비전 번호를 보고 정확히 그 빌드만 내려받는다 — 사용자 시스템에 이미 설치된 크롬을 재사용하지 않는 것이 기본 동작이다.
실제 구현은 두 파일에 나뉘어 있다. 판정 로직 자체(요소가 보이는지·안정됐는지 등)는 브라우저 안에 주입되는 스크립트인 packages/injected/src/injectedScript.ts의 elementState()·checkElementStates()가 담당한다. 이 판정을 "언제까지, 몇 번이나 재시도할지" 감독하는 쪽은 서버의 packages/playwright-core/src/server/frames.ts에 있는 retryWithProgressAndBackoff()다.
// frames.ts — 실제 재시도 스케줄 (밀리초)
const backoffScale = [20, 50, 100, 100, 500];
// timeout이 짧으면 뒤쪽의 큰 대기값을 잘라내 총 시간 안에 맞춘다
while (backoffScale.length && backoffScale[backoffScale.length - 1] > progress.timeout / 5)
backoffScale.pop();
코드가 하는 일을 풀어 쓰면: 클릭 같은 액션을 시도했다가 "아직 준비 안 됨"을 만나면, 20ms → 50ms → 100ms → 100ms → 500ms 간격으로 점점 뜸을 들이며 다시 시도한다. 처음엔 촘촘하게(빠른 페이지는 즉시 성공), 나중엔 널널하게(느린 페이지에 자원을 아끼며) 재확인하는 지수적 백오프(exponential backoff)에 가까운 전략이다. 전체 제한 시간(기본 30초)을 넘기면 그제서야 실패로 보고한다.
actionability 체크 항목은 액션 종류에 따라 다르지만, 클릭 기준으로는 Attached(DOM에 붙어있음) → Visible(보임) → Stable(두 애니메이션 프레임 연속 같은 위치) → Receives Events(다른 요소에 가려 클릭이 가로채이지 않음) → Enabled(비활성화 아님) 순서로 확인한다. fill()류는 여기에 Editable(읽기 전용 아님)이 추가된다.
domUtils.ts의 isElementVisible() 판정 기준은 "빈 바운딩 박스가 아니고 visibility:hidden이 아님"뿐이다. 즉 opacity:0으로 완전히 투명한 요소도 Playwright 기준에서는 "보이는" 상태로 친다. 화면엔 안 보이는데 클릭이 성공하는 상황을 만났다면, 이 정의를 의심해볼 것 — display:none과 opacity:0은 Playwright에게 전혀 다르게 취급된다.
page.locator(...)가 만드는 로케이터는 요소를 즉시 찾아 붙잡는 핸들이 아니라, "이 조건에 맞는 요소를 필요할 때마다 다시 찾아라"는 지연 평가(lazy) 레시피다(client/locator.ts). 실제 요소를 DOM에서 골라내는 저수준 매칭은 packages/injected/src의 셀렉터 엔진들이 담당한다 — CSS(selectorEngine.ts), XPath(xpathSelectorEngine.ts), 접근성 역할 기반의 role=(roleSelectorEngine.ts), 텍스트 매칭 등을 각각 별도 엔진으로 두고 injectedScript.ts가 이들을 조합해 하나의 셀렉터 문자열(예: internal:role=button >> text=제출)을 해석한다.
왜 "지연 평가"가 중요한가: 옛날 방식(예: elementHandle)은 요소를 한 번 붙잡으면 그 참조를 계속 쓰는데, 페이지가 리렌더링되면 그 참조가 죽은 요소(stale element)를 가리키게 된다. 로케이터는 액션을 실행하는 바로 그 순간 다시 DOM을 검색하므로, 리렌더링이 일어나도 "지금 화면에 있는" 요소를 잡는다. auto-waiting의 재시도 루프와도 자연스럽게 맞물리는 이유다.
expect(locator).toBeVisible() 같은 어서션은 한 번 검사하고 끝나지 않는다. frames.ts의 같은 retryWithProgressAndBackoff() 폴링 엔진을 재사용해, 조건이 참이 될 때까지(또는 타임아웃까지) 반복 확인한다. 그래서 "서버 응답이 조금 늦게 와서 텍스트가 나중에 바뀌는" 상황에서도 sleep 없이 안정적으로 통과한다 — auto-waiting과 web-first assertion은 사실 같은 재시도 엔진을 공유하는 한 몸이다.
@playwright/test(packages/playwright/src)는 테스트 파일들을 여러 워커 프로세스(runner/workerHost.ts → worker/workerMain.ts)에 나눠 병렬로 돌린다. 각 워커 안에서는 worker/fixtureRunner.ts가 fixture(픽스처) — 테스트마다 필요한 page·context 같은 준비물 — 를 의존성 그래프로 관리한다. 실제 소스의 Fixture 클래스는 _deps(자신이 의존하는 다른 fixture)와 _usages(자신을 쓰는 쪽)를 서로 추적해, 필요한 fixture만 지연 생성하고 사용이 끝난 순서의 역순으로 정리(teardown)한다.
fixture 시스템은 주방의 재료 준비 순서와 비슷하다. "소스"를 만들려면 "다진 마늘"이 먼저 있어야 하고, "다진 마늘"은 "마늘"이 있어야 한다. 필요한 요리(테스트)에 실제로 쓰이는 재료만 그때그때 준비하고, 다 쓴 재료는 준비한 역순으로 치운다 — 이게 fixture의 의존성 그래프가 하는 일이다.
packages/trace가 테스트 실행 중 스냅샷·네트워크·콘솔 로그를 .zip 트레이스 파일로 기록하면, packages/trace-viewer(브라우저에서 도는 뷰어 앱, uiMode.tsx 포함)가 이를 타임라인으로 되감아 보여준다 — 실패한 단계 직전 DOM 스냅샷까지 그대로 재현된다. packages/recorder와 packages/injected/src/recorder는 사람이 브라우저를 클릭하는 것을 보고 테스트 코드를 자동 생성(codegen)하는 기록기다. UI 모드는 이 모든 걸 워치 모드(파일 변경 감지, runner/fsWatcher.ts)와 묶어 데스크탑 앱처럼 제공한다.
packages/playwright-ct-core와 -ct-react·-ct-vue는 페이지 전체가 아니라 컴포넌트 하나를 Vite로 번들링해 빈 페이지에 마운트(mount.ts)한 뒤, 똑같은 Playwright API로 그 컴포넌트만 상호작용·검증하게 해준다. E2E 테스트보다 훨씬 빠르게 단위 수준 UI를 검증하려는 목적이다.
이 저장소 안에는 packages/playwright/src/agents(테스트 계획·생성·자가치유 에이전트 정의, .agent.md 파일)와 packages/playwright/src/mcp(run-test-mcp-server 숨김 명령으로 뜨는, 테스트 실행 전용 MCP 서버)가 있다. 이건 "이미 짜인 Playwright 테스트를 실행·생성·수리"하는 도구다. 반면 README가 강조하는 범용 브라우저 자동화 MCP 서버(@playwright/mcp)와 토큰 절약형 CLI(@playwright/cli)는 이 저장소가 아니라 microsoft/playwright-mcp·microsoft/playwright-cli라는 별도 저장소다. 자세한 구분은 9번 섹션에서 다시 짚는다.
| 레이어 | 기술 / 버전 | 역할 |
|---|---|---|
| 서버·클라이언트 | TypeScript (Node.js ≥20) | 채널 프로토콜·브라우저 드라이버·CLI 전부 이 언어 |
| 핵심 라이브러리 | playwright-core | 브라우저 제어의 실체. 런타임 dependencies가 비어있다(제로 디펜던시 방침) |
| 테스트 러너 | @playwright/test(=playwright 패키지) | playwright-core에만 의존. 워커·리포터·트레이스 포함 |
| 브라우저 자체 빌드 | Chromium(패치 거의 없음) · 패치된 Firefox·WebKit | browser_patches/에 패치 소스 보관, CI가 주기적으로 리롤(roll) |
| 브라우저 버전 진실원 | packages/playwright-core/browsers.json | "직접 편집 금지, utils/roll_browser.js로만 갱신" 주석이 파일 첫 줄에 박혀 있음 |
| 트레이스·리포트 UI | React 기반 웹앱(trace-viewer·html-reporter) | Vite로 빌드, 트레이스 zip을 브라우저에서 재생 |
| 컴포넌트 테스트 | Vite 플러그인(playwright-ct-core) | React/Vue 컴포넌트를 개별 마운트해 테스트 |
| 라이선스 | Apache License 2.0 | 상업적 사용·수정·재배포 자유, 특허 조항 포함 |
playwright-core의 package.json에는 dependencies 항목 자체가 없다 — 공급망(supply-chain) 보안 사고 위험을 줄이고 설치 크기를 가볍게 하려는 의도적 설계다.새 프로젝트라면 아래 한 줄로 테스트 러너·설정 파일·예제 테스트·브라우저 설치까지 마법사가 다 물어봐준다. "복사만 하면 OK"다.
# 새 프로젝트 초기화 (질문에 답하면 설정·예제까지 자동 생성)
npm init playwright@latest
# 이미 있는 프로젝트에 라이브러리로만 추가하는 경우
npm i -D @playwright/test
# 브라우저 바이너리(Chromium/Firefox/WebKit)는 별도로 받아야 한다
npx playwright install
| 항목 | 요구사항 |
|---|---|
| Node.js | package.json 최소 ≥20, 공식 문서 권장은 22.x / 24.x / 26.x 최신판(둘이 살짝 다름 — 9번 섹션 참고) |
| OS | Windows 11+ / WSL, macOS 14(Sonoma)+, Debian 12·13, Ubuntu 22.04·24.04·26.04(x64/arm64) |
| 브라우저 설치 용량 | 세 엔진을 모두 받으면 수백 MB 단위 — 프로젝트당 한 번, 전역 캐시 폴더에 저장 |
| CI 환경 | 공식 Docker 이미지 제공(mcr.microsoft.com/playwright), GitHub Actions 등 대부분 CI에서 그대로 실행 가능 |
| 언어 바인딩 | Python(pytest-playwright)·Java·.NET은 각자 별도 저장소에서 이 프로젝트의 서버 드라이버를 내려받아 사용 |
npm install playwright만으로는 브라우저 바이너리가 자동으로 안 딸려온다고 착각하기 쉽다. 실제로는 playwright(최상위 패키지)는 설치 후 postinstall로 브라우저를 받으려 시도하지만, playwright-core만 설치했거나 CI 캐시 문제로 건너뛰면 npx playwright install을 따로 실행해야 한다.
가장 대표적인 흐름 하나를 처음부터 끝까지 손으로 따라가 보자 — "로그인 폼에 값을 채우고 제출한 뒤 환영 문구가 뜨는지 확인"하는 시나리오다.
// tests/login.spec.ts — 실제로 짜는 테스트 코드 형태
import { test, expect } from '@playwright/test';
test('로그인하면 환영 문구가 보인다', async ({ page }) => {
await page.goto('https://example.com/login');
await page.locator('#username').fill('demo');
await page.locator('#password').fill('secret');
await page.getByRole('button', { name: '로그인' }).click();
await expect(page.getByText('환영합니다')).toBeVisible();
});
이 코드가 내부에서 흘러가는 순서: ① page.goto가 클라이언트→채널→서버를 거쳐 실제 브라우저에 내비게이션 명령을 보낸다. ② fill()은 로케이터가 그 순간 DOM에서 #username을 다시 찾고, actionability(보임·안정·활성화 등)를 확인한 뒤에야 값을 입력한다. ③ click()도 같은 재시도 루프(retryWithProgressAndBackoff)를 거친다. ④ 마지막 expect(...).toBeVisible()은 "환영합니다" 텍스트가 나타날 때까지, 서버 응답이 늦더라도 정해진 타임아웃 안에서 반복 확인한다. 이 전 과정에서 사용자는 sleep이나 waitFor를 한 줄도 쓰지 않았다.
실행은 npx playwright test 한 줄이면 되고, 기본적으로 여러 워커 프로세스가 테스트 파일을 나눠 병렬로 돈다. 테스트가 실패하면 기본 설정에서 트레이스가 남아, npx playwright show-trace trace.zip로 실패 직전 화면·네트워크·콘솔을 그대로 재생할 수 있다. 코드를 직접 짜기 부담스러우면 npx playwright codegen <url>로 브라우저를 열어 클릭하는 대로 코드를 자동 생성(codegen)할 수도 있다.
npx playwright test --ui로 뜨는 UI 모드는 각 테스트 단계를 타임라인으로 보여주고, 실패한 단계에 마우스를 올리면 그 순간의 DOM 스냅샷이 나타난다. "왜 이 클릭이 실패했지"를 콘솔 로그 없이 눈으로 바로 확인할 수 있어, 초보자가 auto-waiting의 동작을 이해하는 데도 가장 좋은 도구다.
5번 섹션에서 봤듯, Playwright의 visible 판정은 바운딩 박스가 0이 아니고 visibility:hidden이 아니면 통과다. opacity:0이거나 다른 요소 뒤에 완전히 겹쳐 사람 눈엔 안 보여도, 조건만 맞으면 Playwright는 "보인다"고 판단해 액션을 강행할 수 있다. 반대로 "다른 요소에 가려져 클릭이 가로채이는" 경우는 Receives Events라는 별도 체크가 따로 잡아낸다 — 이 둘을 헷갈리면 "왜 이 케이스는 잡고 저 케이스는 안 잡지"에서 혼란스러워진다.
README은 @playwright/mcp(AI 에이전트용 브라우저 제어 MCP 서버)와 @playwright/cli(코딩 에이전트용 CLI)를 크게 소개하지만, 이 둘의 실제 소스는 이 저장소(microsoft/playwright)에 없다. 각각 microsoft/playwright-mcp·microsoft/playwright-cli라는 별도 저장소다. 이 저장소 안에 있는 packages/playwright/src/mcp·agents는 그와 이름만 비슷할 뿐, "이미 작성된 Playwright 테스트를 실행·생성·자가치유"하는 완전히 다른 기능(테스트 러너 내장 MCP)이다. "MCP 관련 버그를 고치려는데 코드가 안 보인다"면 저장소를 잘못 찾아온 것이다.
루트 package.json의 engines.node는 ">=20"이라고만 돼 있지만, 공식 문서(docs/src/intro-js.md)의 "System requirements"에는 "Node.js: latest 22.x, 24.x or 26.x"라고 더 구체적이고 다소 높은 기준이 적혀 있다. "패키지 설정상 20도 되니까"라고 오래된 Node로 밀어붙이다 문서 기준과 어긋나는 이슈를 만들 수 있다 — 실제 CI·지원 대상은 문서 쪽을 따르는 게 안전하다.
browsers.json이 진실원이라는 건 바꿔 말하면, 시스템에 이미 깔린 최신 크롬·엣지를 Playwright가 알아서 쓰지 않는다는 뜻이다. 매번 정해진 리비전을 새로 받는다(용량·네트워크 이슈로 이어질 수 있음). "회사 정책상 지정된 크롬 버전만 써야 한다"면 channel 옵션이나 executablePath로 별도 지정이 필요하다.
위 함정들은 버그가 아니라 일관된 설계 원칙의 결과다. actionability 문서(docs/src/actionability.md)가 "opacity:0도 visible"이라고 명시하고, README가 세 프로젝트의 경계를 명확히 설명한다. 소스와 문서를 한 번씩 대조해보면 대부분 "몰랐던 규칙"이지 "고장난 동작"이 아니라는 걸 알 수 있다.
| 도구 | 구조적 특징 | Playwright와 다른 점 |
|---|---|---|
| Selenium | WebDriver 표준 프로토콜로 각 브라우저의 "드라이버" 바이너리와 통신 | 언어·브라우저 지원이 가장 넓지만 auto-waiting이 기본이 아니고(명시적 wait를 직접 짜야 하는 경우가 많음), API가 더 장황함 |
| Cypress | 테스트 코드가 브라우저 탭 안(같은 JS 실행 컨텍스트)에서 돎 | 디버깅 경험은 뛰어나지만 구조상 여러 탭·오리진(도메인)을 넘나드는 시나리오, 진짜 멀티 브라우저(WebKit 등) 지원이 약함 |
| Puppeteer | Playwright와 뿌리가 같은 팀(구글 크롬팀 출신)이 만든 CDP 전용 라이브러리 | Chromium/Chrome 중심 — Firefox·WebKit 지원이 실험적이거나 약함. 내장 테스트 러너·트레이스 뷰어도 없음 |
| Playwright | 클라이언트-서버 채널 + 세 브라우저 자체 드라이버 | 세 엔진 동등 지원 + auto-waiting 기본 + 테스트 러너·트레이스·codegen까지 한 생태계로 통합 |
요약하면 "넓은 호환성이 최우선"이면 Selenium, "프론트엔드 개발자 경험과 컴포넌트 단위 테스트"가 우선이면 Cypress, "Chromium만 있으면 되는 가벼운 스크래핑·자동화"면 Puppeteer가 각자의 자리가 있다. Playwright는 "브라우저 파편화를 신경 안 쓰면서, 테스트 인프라(병렬 실행·트레이스·리포트)까지 한 번에 갖추고 싶다"는 요구에 가장 잘 맞는다.
Playwright는 "Chromium 하나로는 부족하고, 타이밍 버그로 CI가 자꾸 흔들리는 게 지긋지긋한" 팀에게 가장 큰 이득을 준다. 실제로 배울 거리도 명확하다 — YAML 하나로 클라이언트·서버 타입을 동시에 생성하는 스키마 우선 설계, "폴링 + 지수적 백오프"로 타이밍 문제를 구조적으로 없앤 재시도 엔진, 브라우저별로 다른 저수준 프로토콜을 공통 인터페이스 뒤에 감추는 어댑터 패턴은 이 저장소가 아니어도 어디서나 쓸 수 있는 설계 감각이다.
이미 웹 자동화를 다뤄본 사람이라면 5번 섹션의 frames.ts·injectedScript.ts부터, 이제 막 시작하는 초보자라면 npm init playwright@latest로 만들어지는 예제 테스트와 --ui 모드부터 열어보길 권한다. 릴리스가 잦고 @playwright/mcp·@playwright/cli 같은 자매 프로젝트가 계속 늘고 있으므로, 이 문서는 아키텍처 원리 위주로 이해하고 세부 API는 공식 문서로 확인하는 것이 안전하다.