Playwright는 진짜 브라우저(크로미움·파이어폭스·웹킷)를 코드로 조종하는 E2E 테스트 도구입니다. E2E(End-to-End)는 이름 그대로 끝에서 끝까지 — 사용자가 접속해서 검색하고 결과를 보는 흐름 전체를 실제 브라우저에서 재현하는 테스트고요. 이 글에서 설치부터 첫 시나리오 통과까지 한 번에 갑니다.
시리즈로 따라오셨다면 무대가 커지는 순간이에요 — 지금까지 jsdom이라는 가짜 브라우저에서 놀았다면, 이제 진짜를 엽니다. 검색으로 처음 오셨어도 문제없습니다. Playwright 설치부터 시작하고, 예제의 데모 앱 자리엔 여러분의 아무 Vite/React 앱을 놓아도 흐름은 같아요.
실습 코드: 이 편의 상태가
step-11태그에 담겨 있습니다(Playwright 설치·설정과 첫 시나리오까지 — 픽스처나 axe 같은 뒤 편 코드는 아직 없어요). StackBlitz에서 바로 열기도 됩니다.
이번 편에서 배울 것#
- Playwright 설치와 설정(webServer 포함)
- 첫 E2E 시나리오 작성
- 단위·컴포넌트 테스트와의 역할 분담

잠깐, 검색은 이미 테스트했잖아요?#
맞아요. 상호작용 편에서 타이핑도 하고 결과도 검증했습니다. 그런데 그 무대였던 jsdom은 DOM 흉내만 내는 가짜 브라우저예요 — CSS를 그리지 않고, 진짜 네트워크 스택도 없고, 주소창도 없습니다. 그래서 이런 것들은 잡지 못해요.
- 빌드·번들 설정이 깨져서 앱 자체가 안 뜨는 문제
- 라우팅, 리다이렉트, 새로고침 같은 주소창이 얽힌 흐름
- “요소는 DOM에 있는데 CSS 때문에 화면에선 안 보이는” 문제
연극에 비유하면 지금까지는 배우 개인 연습(단위)과 장면 연습(컴포넌트)이었고, E2E는 본 무대 총연습입니다. 조명·음향·무대장치(빌드·서버·브라우저)까지 전부 켜고, 처음부터 끝까지 실제로 한 번 돌려보는 거죠.

설치와 설정#
npm i -D @playwright/test
npx playwright install chromium둘째 줄이 좀 특이하죠 — npm 패키지와 별개로 브라우저 실행 파일 자체를 내려받는 명령입니다. Playwright는 여러분 컴퓨터의 크롬을 빌리는 게 아니라, 버전이 고정된 자기 전용 브라우저를 씁니다. “내 크롬에선 됐는데"조차 없애려는 거예요.
설정에서 webServer를 지정하면, 테스트 시작 시 dev 서버를 알아서 띄워줍니다. (repo playwright.config.ts)
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI, // test.only가 CI에 흘러들면 실패시킨다
retries: process.env.CI ? 2 : 0, // 리트라이는 CI에서만 — 나중에 자세히
use: {
baseURL: 'http://localhost:5173', // goto('/')의 기준 주소
trace: 'on-first-retry', // 재시도부터 실행 과정을 녹화
},
webServer: {
command: 'npm run dev', // 테스트 시작 시 dev 서버 자동 실행
url: 'http://localhost:5173', // 이 주소가 응답하면 준비 완료로 판단
reuseExistingServer: !process.env.CI, // 로컬에선 이미 떠 있는 서버 재사용
},
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
})낯선 항목 셋만 짚을게요. fullyParallel은 테스트 파일들을 동시에 돌려 시간을 아끼는 옵션, trace: 'on-first-retry'는 실패해서 재시도할 때 실행 과정 전체를 녹화해두는 옵션입니다(이 녹화가 얼마나 고마운지는 디버깅을 다루는 편에서 절감하게 돼요). projects는 어떤 브라우저로 돌릴지의 목록 — 지금은 크로미움 하나로 시작합니다.
첫 시나리오#
repo의 실제 E2E입니다. (e2e/dashboard.spec.ts)
import { test, expect } from '@playwright/test'
test('대시보드에서 사용자를 검색한다', async ({ page }) => {
await page.goto('/')
await expect(page.getByRole('heading', { name: '대시보드' })).toBeVisible()
await expect(page.getByText('Alice Kim')).toBeVisible() // 목록 로딩까지 확인
// 사용자처럼: 검색창을 찾아 타이핑
await page.getByRole('searchbox', { name: '검색' }).fill('carol')
await expect(page.getByText('Carol Park')).toBeVisible()
await expect(page.getByText('Alice Kim')).toHaveCount(0) // 0개 = 화면에서 사라짐
})읽는 법을 짚어볼게요. 먼저 async ({ page }) => — 이 page는 Playwright가 테스트마다 새로 건네주는 깨끗한 브라우저 탭입니다(받아쓰기만 하면 되고, 정리도 알아서 해줘요). page.goto('/')의 /는 설정의 baseURL 기준이라 곧 localhost:5173입니다. toBeVisible()은 “화면에 보인다”, toHaveCount(0)은 “0개다 = 사라졌다"는 웹 우선 단언이에요 — 컴포넌트 테스트의 toBeInTheDocument/queryBy 짝과 역할이 같죠. 참고로 이 E2E가 백엔드 없이 도는 비결도 MSW 편의 브라우저 목입니다 — dev 서버 속에서 같은 핸들러가 /api/users를 응답하고 있어요.
npx playwright test
Running 1 test using 1 worker
✓ 1 [chromium] › e2e/dashboard.spec.ts:3:1 › 대시보드에서 사용자를 검색한다 (398ms)
1 passed (1.6s)실행하면 dev 서버가 먼저 뜨고(webServer 덕), 진짜 브라우저가 열렸다 닫히며 1.6초 만에 초록불이 켜집니다. 그런데 브라우저 창은 안 보였을 거예요 — 기본이 headless(창 없는) 모드거든요. 눈으로 직접 보고 싶다면 npx playwright test --headed로 실행해보세요. 커서가 저절로 움직이며 검색하는 장면은 처음 보면 꽤 신기합니다.
여기서도 getByRole — 컴포넌트 테스트에서 익힌 쿼리 철학이 E2E까지 그대로 이어집니다. (왜 getByLabel('검색')이 아닌지는 다음 편에서 재미있는 사연과 함께.)
일부러 부숴봅시다#
통과만 보면 심심하니, 경보기도 점검해보죠. 기대 텍스트에 오타를 내봅니다 — Carol Park을 Carol Prak으로.
✘ 1 [chromium] › e2e/dashboard.spec.ts:3:1 › 대시보드에서 사용자를 검색한다 (5.0s)
Error: expect(locator).toBeVisible() failed
Locator: getByText('Carol Prak')
Expected: visible
Error: element(s) not found
Call log:
- Expect "toBeVisible" with timeout 5000ms
- waiting for getByText('Carol Prak')
> 10 | await expect(page.getByText('Carol Prak')).toBeVisible()
| ^실패 메시지의 정보량을 봐주세요. 어떤 로케이터가(getByText('Carol Prak')), 어느 파일 몇 번째 줄에서(화살표가 정확히 그 지점을 찍어줍니다), 뭘 하다가(Call log — 요소가 나타나기를 5초 동안 기다렸어요) 실패했는지 전부 담겨 있습니다. 게다가 test-results/ 폴더에는 실패 순간의 페이지 상태가 증거 파일로 남고요. “expected true, got false” 한 줄 던지고 끝나는 도구들과는 온도가 다르죠.
그리고 Call log의 **“5초 기다림”**이 눈에 걸렸다면 좋은 감각입니다 — Playwright는 단언이 안 맞아도 바로 포기하지 않고, 요소가 나타날 때까지 기다려줍니다(auto-wait). 이 기다림이 왜 E2E의 생명줄인지가 바로 다음 편 주제예요.
E2E는 적게, 굵게#
E2E는 느리고 비쌉니다. 방금 그 테스트 하나가 서버를 띄우고 브라우저를 열고 페이지를 그리는 동안, 단위 테스트는 수백 개가 끝나요. 그러니 같은 걸 층마다 반복하면 낭비입니다.
역할 분담은 이렇게 — 빈 검색어·대소문자·공백 같은 경우의 수는 이미 단위 테스트가, 로딩·에러·포커스 같은 화면의 반응은 컴포넌트 테스트가 잡았습니다. E2E는 그 위에서 “이 조각들이 실제로 연결돼 굴러가는가“라는 대표 흐름 몇 개만 굵게 확인하면 됩니다. 검색 대시보드라면 “접속 → 검색 → 결과"면 충분해요.

한 장 요약#
- Playwright = 진짜 브라우저를 코드로 조종하는 E2E 도구 — 설치는
npm i -D @playwright/test+npx playwright install chromium - 설정의 webServer가 테스트 시작 시 dev 서버를 자동으로 띄움 —
baseURL덕에 테스트는goto('/')만 - 첫 시나리오 문형:
goto→getByRole로 찾고 →fill/click→toBeVisible/toHaveCount로 단언 - 쿼리 철학은 컴포넌트 테스트와 동일 — role과 이름으로 찾는 습관이 그대로 이어집니다
- 실패하면 로케이터·코드 위치·기다림 기록(Call log)이 통째로 남습니다 — 증거와 함께 실패하는 도구
- E2E는 적게, 굵게 — 경우의 수는 아래층(단위·컴포넌트)이 이미 맡았습니다
진짜 브라우저의 세계로#
첫 E2E가 돌았습니다. 그런데 E2E의 최대 적, flaky(같은 코드인데 돌릴 때마다 통과와 실패를 오가는 변덕 테스트)가 기다리고 있어요. 다음 편에서 로케이터로 대응합니다.
Playwright는 실패하면 스크린샷·비디오·트레이스를 남겨줘요. “내 컴퓨터에선 됐는데"의 시대는 끝났습니다. 증거가 남거든요.
레벨업: 진짜 브라우저에서 사용자 흐름 전체를 검증할 수 있습니다.
다음 편: 로케이터와 auto-wait — flaky 테스트 줄이기
낯선 용어가 있었다면 — 용어집에 전부 한 줄씩 정리돼 있어요.
