REPO DEEP DIVE · 2026-07-29 · MICROSOFT/PLAYWRIGHT · 크로스브라우저 테스트 자동화 프레임워크

microsoft/playwright 딥다이브
"한 번 쓰고 세 브라우저 다 돌린다"는 약속의 내부

PlaywrightChromium·Firefox·WebKit 세 브라우저 엔진을 단 하나의 API로 조종하는 웹 자동화·테스트 프레임워크다. 브라우저를 열고, 클릭하고, 값을 채우고, 화면에 뭐가 보이는지 "기다렸다가" 검증하는 일을, 사람이 타이밍을 맞춰가며 코딩하지 않아도 되게 만든 것이 핵심이다.

가장 큰 특징은 "클라이언트-서버 분리 아키텍처"다. 사용자가 짜는 테스트 코드(클라이언트)는 실제 브라우저를 직접 조작하지 않는다. 대신 내부 채널(프로토콜)을 통해 "서버" 프로세스에 명령을 보내고, 서버가 각 브라우저의 진짜 저수준 프로토콜(Chromium은 CDP, Firefox·WebKit은 자체 패치)로 번역해 실행한다. 그리고 모든 동작 전에 "이 요소가 지금 눌러도 되는 상태인가"를 자동으로 확인하는 auto-waiting(자동 대기)이 기본으로 켜져 있어, 초보자가 가장 많이 겪는 "요소를 찾았는데 클릭이 안 먹는" 문제의 상당수를 프레임워크가 대신 해결해준다.

(저장소 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)
목차
  1. 프로젝트 한 줄 요약
  2. 왜 지금 주목받는가
  3. 무엇을 해결하는가
  4. 전체 구조 — 클라이언트/서버/브라우저 3단
  5. 핵심 설계·동작 원리
  6. 기술 스택·의존성
  7. 설치·요구사항
  8. 실제 사용 흐름
  9. 함정·오해 방지
  10. 대안 비교
  11. 정리 — 누가 쓰면 좋은가

1프로젝트 한 줄 요약

한 문장으로: "브라우저 세 종류를 한 API로, 자동으로 기다려주면서, 클라이언트-서버로 나눠 조종하는 프레임워크"

Playwright는 "브라우저를 코드로 운전하는 표준 계기판"이다. 로그인 폼에 값을 채우고 버튼을 누르고 결과 화면을 확인하는 흐름을, Chromium(크롬·엣지의 엔진)·Firefox·WebKit(사파리의 엔진) 어느 브라우저에서든 거의 똑같은 코드로 짤 수 있게 해준다. 저장소 이름은 하나지만 실제로는 packages/ 아래 서로 다른 역할을 하는 패키지 20여 개가 협업하는 모노레포(monorepo)다.

이 저장소가 특히 자랑하는 것은 두 가지다. 첫째는 테스트 러너(@playwright/test)까지 자체 제공한다는 점 — 단순 브라우저 조작 라이브러리가 아니라, 병렬로 테스트를 돌리고 리포트를 만들고 실패를 재현하는 트레이스(trace)까지 통째로 제공하는 "완결형 테스트 도구"다. 둘째는 모든 동작 앞에 자동으로 상태를 점검한다는 것 — 버튼이 화면에 보이고, 다른 요소에 가려지지 않고, 애니메이션이 멈춰 안정된 뒤에야 클릭을 실행한다.

한 줄 비유

"통역사 딸린 만능 리모컨"

브라우저 세 개는 각자 다른 언어(저수준 프로토콜)를 쓰는 세 대의 TV와 같다. Playwright의 테스트 코드는 리모컨 버튼(공통 API)만 누르면 되고, 뒤에 있는 "서버"라는 통역사가 그 버튼 입력을 각 TV가 알아듣는 언어로 바꿔 전달한다.

게다가 이 리모컨은 성격이 꼼꼼해서, 채널을 바꾸기 전에 "TV가 켜져 있는지, 화면이 지금 멈춰 있는지"를 스스로 확인하고 나서야 버튼을 누른다 — 이게 auto-waiting이다.

2왜 지금 주목받는가

"눈으로 보는" 자동화에서 "구조로 읽는" 자동화로 · AI 에이전트 브라우저 제어의 표준화

① 코딩 에이전트 시대의 "브라우저 손발"로 재발견됨

2023~2024년까지 Playwright는 "Selenium보다 빠르고 안정적인 E2E(End-to-End, 처음부터 끝까지) 테스트 도구"로 자리 잡았다. 그런데 2025~2026년 들어 AI 코딩 에이전트(Claude Code, Copilot 등)가 웹 화면을 직접 조작해야 하는 상황이 폭발적으로 늘면서, Playwright의 접근성 트리(accessibility tree) 기반 요소 인식 방식이 "에이전트가 스크린샷 없이도 화면을 정확히 읽고 클릭할 수 있는 유일한 실전 도구"로 재조명받고 있다. README 자체가 이제 "AI 에이전트를 위한 도구"라는 문구로 시작한다.

② 별도 생태계로 분화 — MCP·CLI·에이전트 서브패키지

이 저장소는 단순 유지보수를 넘어, 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 단계에서 미리 잡아낼 수 있다는 점이 여전히 강력한 채택 이유다.

경쟁 지형
Selenium·Cypress·Puppeteer 사이에서

자세한 비교는 10번 섹션에서 다루지만, 요지만 먼저: Selenium은 역사가 길고 언어 지원이 넓지만 API가 장황하고 대기 로직을 직접 짜야 하는 경우가 많다. Cypress는 개발 경험이 좋지만 같은 브라우저 탭 안에서 테스트 코드가 도는 구조적 제약 때문에 여러 탭·도메인을 넘나드는 시나리오가 약하다. Puppeteer는 Playwright와 같은 팀(Google Chrome팀 출신 인력)이 만든 조상뻘이지만 Chromium 중심이다. Playwright는 이 셋의 장점을 절충하며 "테스트 러너까지 자체 제공"이라는 완결성으로 차별화했다.

3무엇을 해결하는가

"타이밍 맞추기 지옥"과 "브라우저마다 다른 코드" 두 가지 고질병

웹 자동화에서 초보자가 가장 먼저 부딪히는 벽은 "방금 로딩된 페이지의 어떤 요소가, 언제 클릭 가능한 상태가 되는가"다. 옛날 도구들은 이걸 사람이 직접 sleep(2000) 같은 하드코딩된 대기로 때웠다. 페이지가 빠르면 시간 낭비, 느리면 실패(flaky, 들쭉날쭉한 테스트)로 이어진다.

Playwright는 이 문제를 "모든 액션 앞에 자동으로, 실패할 때까지 재시도하며 상태를 확인하는" 구조로 해결한다. 클릭 한 줄을 써도 내부에서는 "요소가 붙어있는가(attached) → 보이는가(visible) → 안정됐는가(stable, 두 프레임 연속 같은 위치) → 다른 요소에 가려지지 않았는가(receives events) → 비활성화 상태가 아닌가(enabled)"를 순서대로, 정해진 시간 안에서 반복 확인한다.

두 번째로 해결하는 문제는 브라우저 파편화다. Chromium·Firefox·WebKit은 내부 프로토콜이 완전히 다르다. Playwright는 이 차이를 서버 계층 안에 가두고, 사용자에게는 브라우저 이름만 바꾸면 되는 동일한 API를 내준다.

용어
플레이키(flaky) 테스트
같은 코드, 같은 환경인데도 실행할 때마다 성공했다 실패했다 하는 테스트. 대부분 "화면이 준비되기 전에 조작을 시도했다"는 타이밍 문제에서 비롯된다. auto-waiting은 이 원인의 상당 부분을 구조적으로 없앤다.

4전체 구조 — 클라이언트/서버/브라우저 3단

테스트 코드(Client) → 프로토콜 채널 → 드라이버 서버(Server) → 실제 브라우저 프로세스

Playwright의 근본 구조는 "클라이언트가 브라우저를 직접 만지지 않는다"는 것이다. 대신 클라이언트(사용자가 짜는 테스트 코드)와 서버(각 브라우저를 실제로 조종하는 프로세스) 사이에 양방향 프로토콜(채널)이 있고, 둘은 이 채널로만 대화한다. 언어별 바인딩(Python·Java·.NET)이 존재할 수 있는 것도 이 구조 덕분이다 — 각 언어는 "클라이언트" 부분만 자기 언어로 다시 구현하면 되고, 실제 브라우저 제어 로직(서버)은 이 저장소의 TypeScript 코드 하나로 공유된다.

┌────────────────────────────────────────────────────────────────┐ │ PLAYWRIGHT 3단 구조 (실제 소스 기준) │ └────────────────────────────────────────────────────────────────┘ [1] CLIENT (packages/playwright-core/src/client) 사용자 코드가 부르는 page.click(), locator.fill() 등 · channelOwner.ts — 서버의 객체(Page·Frame…)를 로컬 프록시로 표현 · connection.ts — 채널 메시지 송수신 허브 │ Channel (JSON 메시지, packages/protocol 스키마로 검증) ▼ [2] SERVER (packages/playwright-core/src/server) ├ dispatchers/ → 클라이언트 요청을 실제 서버 객체 메서드로 연결 ├ frames.ts → retryWithProgressAndBackoff — 액션 재시도·폴링 엔진 ├ chromium/ → crConnection.ts가 CDP(devtools 프로토콜)로 직접 대화 ├ firefox/ → ffConnection.ts가 "Juggler"(자체 확장 프로토콜)로 대화 └ webkit/ → wkConnection.ts가 패치된 WebKit 내부 프로토콜로 대화 │ 각 브라우저의 네이티브 원격 제어 프로토콜 ▼ [3] 실제 브라우저 프로세스 (Chrome/Chromium, Firefox, WebKit 바이너리) browsers.json에 고정된 리비전으로 npx playwright install 시 다운로드

클라이언트-서버가 "왜" 분리돼 있나

이 분리 덕분에 세 가지가 가능해진다. 첫째, 원격 연결browserType.connect()로 다른 머신에서 도는 브라우저 서버에 붙을 수 있다(클라이언트와 서버가 물리적으로 떨어져도 프로토콜만 맞으면 된다). 둘째, 언어 독립성 — Python·Java·.NET 바인딩은 이 채널 프로토콜만 자기 언어로 구현하면 되므로, 브라우저 제어의 어려운 부분(서버)을 다시 짤 필요가 없다. 셋째, 격리 — 서버 프로세스가 죽어도 클라이언트가 그 사실을 채널 단절로 감지해 정리할 수 있다.

용어
CDP (Chrome DevTools Protocol)
크롬 개발자도구가 브라우저 내부와 대화할 때 쓰는 공식 원격 제어 프로토콜. Playwright의 Chromium 서버 코드(crConnection.ts)는 이 프로토콜을 그대로 사용한다.
용어
Juggler
Firefox에는 CDP 같은 표준 원격 제어 프로토콜이 없어서, Playwright 팀이 Firefox 자체에 패치를 넣어 만든 전용 원격 제어 확장 이름. browser_patches/firefox/juggler 폴더에 그 소스가 있다.

5핵심 설계·동작 원리

채널 프로토콜 생성 · 브라우저별 드라이버 · auto-waiting 엔진 · 로케이터/셀렉터 · 테스트 러너 · 트레이스

① 프로토콜(채널) — YAML 스펙 하나가 클라이언트·서버 타입을 동시에 낳는다

클라이언트와 서버가 "같은 언어"로 대화하려면 메시지 형식이 어긋나면 안 된다. 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)만 고치면 양쪽 교재(타입)가 항상 같이 바뀐다 — 손으로 두 벌을 관리하다 어긋나는 사고가 구조적으로 안 난다.

② 브라우저별 드라이버 — 세 엔진, 세 가지 통역 방식

브라우저통신 방식비고
ChromiumCDP를 그대로 사용 (crConnection.ts)표준 프로토콜이라 패치 없이 대부분 제어 가능
Firefox자체 확장 Juggler (ffConnection.ts)browser_patches/firefox에 소스 패치 보관, 자체 빌드한 Firefox 배포
WebKit패치된 내부 프로토콜 (wkConnection.ts)browser_patches/webkitembedder(임베더 셸)로 감싼 커스텀 빌드

세 서버 모듈(server/chromium·server/firefox·server/webkit) 모두 *Page.ts·*Browser.ts·*NetworkManager.ts 같은 대칭적인 파일 구성을 갖는다 — 통신 방식은 다르지만, 그 위에서 "페이지·네트워크·입력을 어떻게 다룰지"는 같은 추상 인터페이스(server/page.tsPageDelegate 등)를 구현하는 방식으로 통일해, 상위 계층(dispatchers·frames.ts)은 어느 브라우저인지 신경 쓰지 않는다.

실제로 다운로드되는 브라우저 바이너리의 버전은 사람이 직접 편집하지 않는 packages/playwright-core/browsers.json이 유일한 진실원(source of truth)이다. 이 파일 맨 위에는 "이 파일을 직접 고치지 말고 utils/roll_browser.js를 쓰라"는 주석이 박혀 있다. npx playwright install은 이 JSON의 리비전 번호를 보고 정확히 그 빌드만 내려받는다 — 사용자 시스템에 이미 설치된 크롬을 재사용하지 않는 것이 기본 동작이다.

③ auto-waiting과 actionability — "지금 눌러도 되는가"를 재는 5~6가지 체크

실제 구현은 두 파일에 나뉘어 있다. 판정 로직 자체(요소가 보이는지·안정됐는지 등)는 브라우저 안에 주입되는 스크립트packages/injected/src/injectedScript.tselementState()·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(읽기 전용 아님)이 추가된다.

실제 코드가 알려주는 반전
"보인다(visible)"의 기준은 생각보다 느슨하다

domUtils.tsisElementVisible() 판정 기준은 "빈 바운딩 박스가 아니고 visibility:hidden이 아님"뿐이다. 즉 opacity:0으로 완전히 투명한 요소도 Playwright 기준에서는 "보이는" 상태로 친다. 화면엔 안 보이는데 클릭이 성공하는 상황을 만났다면, 이 정의를 의심해볼 것 — display:noneopacity:0은 Playwright에게 전혀 다르게 취급된다.

④ 로케이터(Locator)와 셀렉터 엔진 — "요소를 어떻게 찾을지"의 두 계층

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의 재시도 루프와도 자연스럽게 맞물리는 이유다.

⑤ 웹 우선 어서션(web-first assertion) — 검증도 재시도한다

expect(locator).toBeVisible() 같은 어서션은 한 번 검사하고 끝나지 않는다. frames.ts의 같은 retryWithProgressAndBackoff() 폴링 엔진을 재사용해, 조건이 참이 될 때까지(또는 타임아웃까지) 반복 확인한다. 그래서 "서버 응답이 조금 늦게 와서 텍스트가 나중에 바뀌는" 상황에서도 sleep 없이 안정적으로 통과한다 — auto-waiting과 web-first assertion은 사실 같은 재시도 엔진을 공유하는 한 몸이다.

⑥ 테스트 러너 — worker 프로세스 병렬화와 fixture 의존성 그래프

@playwright/test(packages/playwright/src)는 테스트 파일들을 여러 워커 프로세스(runner/workerHost.tsworker/workerMain.ts)에 나눠 병렬로 돌린다. 각 워커 안에서는 worker/fixtureRunner.tsfixture(픽스처) — 테스트마다 필요한 page·context 같은 준비물 — 를 의존성 그래프로 관리한다. 실제 소스의 Fixture 클래스는 _deps(자신이 의존하는 다른 fixture)와 _usages(자신을 쓰는 쪽)를 서로 추적해, 필요한 fixture만 지연 생성하고 사용이 끝난 순서의 역순으로 정리(teardown)한다.

비유

fixture 시스템은 주방의 재료 준비 순서와 비슷하다. "소스"를 만들려면 "다진 마늘"이 먼저 있어야 하고, "다진 마늘"은 "마늘"이 있어야 한다. 필요한 요리(테스트)에 실제로 쓰이는 재료만 그때그때 준비하고, 다 쓴 재료는 준비한 역순으로 치운다 — 이게 fixture의 의존성 그래프가 하는 일이다.

⑦ 트레이스 뷰어·codegen·UI 모드 — "실패를 다시 재생"하는 도구들

packages/trace가 테스트 실행 중 스냅샷·네트워크·콘솔 로그를 .zip 트레이스 파일로 기록하면, packages/trace-viewer(브라우저에서 도는 뷰어 앱, uiMode.tsx 포함)가 이를 타임라인으로 되감아 보여준다 — 실패한 단계 직전 DOM 스냅샷까지 그대로 재현된다. packages/recorderpackages/injected/src/recorder는 사람이 브라우저를 클릭하는 것을 보고 테스트 코드를 자동 생성(codegen)하는 기록기다. UI 모드는 이 모든 걸 워치 모드(파일 변경 감지, runner/fsWatcher.ts)와 묶어 데스크탑 앱처럼 제공한다.

⑧ 컴포넌트 테스트 — 브라우저 없이 리액트/뷰 컴포넌트만 마운트

packages/playwright-ct-core-ct-react·-ct-vue는 페이지 전체가 아니라 컴포넌트 하나를 Vite로 번들링해 빈 페이지에 마운트(mount.ts)한 뒤, 똑같은 Playwright API로 그 컴포넌트만 상호작용·검증하게 해준다. E2E 테스트보다 훨씬 빠르게 단위 수준 UI를 검증하려는 목적이다.

⑨ AI 에이전트 관련 — 저장소 안의 것과 밖의 것을 구분해야 한다

이 저장소 안에는 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번 섹션에서 다시 짚는다.

6기술 스택·의존성

TypeScript 기반 · playwright-core는 런타임 의존성 0개 · 브라우저 버전은 JSON 파일이 진실원
레이어기술 / 버전역할
서버·클라이언트TypeScript (Node.js ≥20)채널 프로토콜·브라우저 드라이버·CLI 전부 이 언어
핵심 라이브러리playwright-core브라우저 제어의 실체. 런타임 dependencies가 비어있다(제로 디펜던시 방침)
테스트 러너@playwright/test(=playwright 패키지)playwright-core에만 의존. 워커·리포터·트레이스 포함
브라우저 자체 빌드Chromium(패치 거의 없음) · 패치된 Firefox·WebKitbrowser_patches/에 패치 소스 보관, CI가 주기적으로 리롤(roll)
브라우저 버전 진실원packages/playwright-core/browsers.json"직접 편집 금지, utils/roll_browser.js로만 갱신" 주석이 파일 첫 줄에 박혀 있음
트레이스·리포트 UIReact 기반 웹앱(trace-viewer·html-reporter)Vite로 빌드, 트레이스 zip을 브라우저에서 재생
컴포넌트 테스트Vite 플러그인(playwright-ct-core)React/Vue 컴포넌트를 개별 마운트해 테스트
라이선스Apache License 2.0상업적 사용·수정·재배포 자유, 특허 조항 포함
용어
제로 디펜던시(zero-dependency)
패키지가 실행에 필요한 외부 라이브러리(third-party 패키지)를 거의/전혀 갖지 않는 것. playwright-corepackage.json에는 dependencies 항목 자체가 없다 — 공급망(supply-chain) 보안 사고 위험을 줄이고 설치 크기를 가볍게 하려는 의도적 설계다.

7설치·요구사항

Node.js ≥20(문서 권장은 22/24/26) · 브라우저는 별도 다운로드 · OS별 지원 버전 명시
가장 흔한 시작 방법

새 프로젝트라면 아래 한 줄로 테스트 러너·설정 파일·예제 테스트·브라우저 설치까지 마법사가 다 물어봐준다. "복사만 하면 OK"다.

# 새 프로젝트 초기화 (질문에 답하면 설정·예제까지 자동 생성)
npm init playwright@latest

# 이미 있는 프로젝트에 라이브러리로만 추가하는 경우
npm i -D @playwright/test
# 브라우저 바이너리(Chromium/Firefox/WebKit)는 별도로 받아야 한다
npx playwright install
항목요구사항
Node.jspackage.json 최소 ≥20, 공식 문서 권장은 22.x / 24.x / 26.x 최신판(둘이 살짝 다름 — 9번 섹션 참고)
OSWindows 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을 따로 실행해야 한다.

8실제 사용 흐름

테스트 작성 → 실행 → 실패 시 트레이스로 되감기, 그 사이 로케이터·어서션이 어떻게 흐르는지

가장 대표적인 흐름 하나를 처음부터 끝까지 손으로 따라가 보자 — "로그인 폼에 값을 채우고 제출한 뒤 환영 문구가 뜨는지 확인"하는 시나리오다.

// 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)할 수도 있다.

실전 팁

디버깅은 UI 모드로

npx playwright test --ui로 뜨는 UI 모드는 각 테스트 단계를 타임라인으로 보여주고, 실패한 단계에 마우스를 올리면 그 순간의 DOM 스냅샷이 나타난다. "왜 이 클릭이 실패했지"를 콘솔 로그 없이 눈으로 바로 확인할 수 있어, 초보자가 auto-waiting의 동작을 이해하는 데도 가장 좋은 도구다.

9함정·오해 방지

"보임"의 정의 · 세 개의 다른 저장소를 하나로 착각 · 문서와 코드의 미세한 어긋남

함정 ① — "화면에 안 보이는데 클릭이 성공했다"

5번 섹션에서 봤듯, Playwright의 visible 판정은 바운딩 박스가 0이 아니고 visibility:hidden이 아니면 통과다. opacity:0이거나 다른 요소 뒤에 완전히 겹쳐 사람 눈엔 안 보여도, 조건만 맞으면 Playwright는 "보인다"고 판단해 액션을 강행할 수 있다. 반대로 "다른 요소에 가려져 클릭이 가로채이는" 경우는 Receives Events라는 별도 체크가 따로 잡아낸다 — 이 둘을 헷갈리면 "왜 이 케이스는 잡고 저 케이스는 안 잡지"에서 혼란스러워진다.

함정 ② — "Playwright MCP를 여기서 고치면 되겠지"라는 착각

README은 @playwright/mcp(AI 에이전트용 브라우저 제어 MCP 서버)와 @playwright/cli(코딩 에이전트용 CLI)를 크게 소개하지만, 이 둘의 실제 소스는 이 저장소(microsoft/playwright)에 없다. 각각 microsoft/playwright-mcp·microsoft/playwright-cli라는 별도 저장소다. 이 저장소 안에 있는 packages/playwright/src/mcp·agents는 그와 이름만 비슷할 뿐, "이미 작성된 Playwright 테스트를 실행·생성·자가치유"하는 완전히 다른 기능(테스트 러너 내장 MCP)이다. "MCP 관련 버그를 고치려는데 코드가 안 보인다"면 저장소를 잘못 찾아온 것이다.

함정 ③ — Node.js 버전, package.json과 문서가 미묘하게 다르다

루트 package.jsonengines.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가 세 프로젝트의 경계를 명확히 설명한다. 소스와 문서를 한 번씩 대조해보면 대부분 "몰랐던 규칙"이지 "고장난 동작"이 아니라는 걸 알 수 있다.

10대안 비교

Selenium · Cypress · Puppeteer와 무엇이 다른가
도구구조적 특징Playwright와 다른 점
SeleniumWebDriver 표준 프로토콜로 각 브라우저의 "드라이버" 바이너리와 통신언어·브라우저 지원이 가장 넓지만 auto-waiting이 기본이 아니고(명시적 wait를 직접 짜야 하는 경우가 많음), API가 더 장황함
Cypress테스트 코드가 브라우저 탭 안(같은 JS 실행 컨텍스트)에서 돎디버깅 경험은 뛰어나지만 구조상 여러 탭·오리진(도메인)을 넘나드는 시나리오, 진짜 멀티 브라우저(WebKit 등) 지원이 약함
PuppeteerPlaywright와 뿌리가 같은 팀(구글 크롬팀 출신)이 만든 CDP 전용 라이브러리Chromium/Chrome 중심 — Firefox·WebKit 지원이 실험적이거나 약함. 내장 테스트 러너·트레이스 뷰어도 없음
Playwright클라이언트-서버 채널 + 세 브라우저 자체 드라이버세 엔진 동등 지원 + auto-waiting 기본 + 테스트 러너·트레이스·codegen까지 한 생태계로 통합

요약하면 "넓은 호환성이 최우선"이면 Selenium, "프론트엔드 개발자 경험과 컴포넌트 단위 테스트"가 우선이면 Cypress, "Chromium만 있으면 되는 가벼운 스크래핑·자동화"면 Puppeteer가 각자의 자리가 있다. Playwright는 "브라우저 파편화를 신경 안 쓰면서, 테스트 인프라(병렬 실행·트레이스·리포트)까지 한 번에 갖추고 싶다"는 요구에 가장 잘 맞는다.

11정리 — 누가 쓰면 좋은가

세 브라우저 대응이 필요한 팀 · 안정적인 E2E CI를 원하는 팀 · 브라우저 자동화를 배우는 초보자

Playwright는 "Chromium 하나로는 부족하고, 타이밍 버그로 CI가 자꾸 흔들리는 게 지긋지긋한" 팀에게 가장 큰 이득을 준다. 실제로 배울 거리도 명확하다 — YAML 하나로 클라이언트·서버 타입을 동시에 생성하는 스키마 우선 설계, "폴링 + 지수적 백오프"로 타이밍 문제를 구조적으로 없앤 재시도 엔진, 브라우저별로 다른 저수준 프로토콜을 공통 인터페이스 뒤에 감추는 어댑터 패턴은 이 저장소가 아니어도 어디서나 쓸 수 있는 설계 감각이다.

이미 웹 자동화를 다뤄본 사람이라면 5번 섹션의 frames.ts·injectedScript.ts부터, 이제 막 시작하는 초보자라면 npm init playwright@latest로 만들어지는 예제 테스트와 --ui 모드부터 열어보길 권한다. 릴리스가 잦고 @playwright/mcp·@playwright/cli 같은 자매 프로젝트가 계속 늘고 있으므로, 이 문서는 아키텍처 원리 위주로 이해하고 세부 API는 공식 문서로 확인하는 것이 안전하다.