Playwright로 E2E 테스트 시작하기

Playwright로 E2E 테스트 시작하기

웹 애플리케이션을 개발하다 보면 “이거 진짜 브라우저에서도 잘 되나?” 하는 불안감이 들 때가 있죠. 단위 테스트로 함수 하나하나는 검증했지만, 사용자가 실제로 버튼을 클릭하고 페이지를 이동하고 폼을 제출하는 흐름까지 테스트하려면 별도의 도구가 필요합니다. 이런 종단간(End-to-End) 테스트를 위해 등장한 것이 바로 Playwright입니다.

이 글에서는 Playwright가 무엇인지 살펴보고, 설치부터 테스트 작성과 실행, 그리고 디버깅까지 E2E 테스트의 전체 흐름을 다뤄보겠습니다.

Playwright란?

Playwright는 Microsoft에서 만든 오픈소스 E2E 테스트 프레임워크입니다. Chromium, Firefox, WebKit 세 가지 브라우저 엔진을 모두 지원하며, 하나의 API로 크로스 브라우저 테스트를 작성할 수 있어요.

Playwright가 나오기 전에는 E2E 테스트 하면 Cypress가 대표적이었는데요. Playwright는 Cypress와 비교해서 몇 가지 차별점이 있습니다. 우선 멀티 브라우저를 기본 지원합니다. Cypress는 얼마 전까지 Chromium 계열만 지원했지만 Playwright는 처음부터 Chromium, Firefox, WebKit을 동등하게 지원했습니다. 멀티 탭이나 멀티 도메인 테스트도 자연스럽게 처리할 수 있고요. 자동 대기(auto-waiting) 메커니즘이 내장되어 있어서 요소가 준비될 때까지 알아서 기다려줍니다. sleep(3000) 같은 코드를 넣을 필요가 없는 거죠.

설치하기

Playwright를 새 프로젝트에 설치하는 가장 쉬운 방법은 create 명령어를 사용하는 것입니다.

bun create playwright

이 명령어를 실행하면 몇 가지 질문이 나옵니다. 테스트 파일을 어디에 둘 것인지, GitHub Actions 워크플로우를 추가할 것인지, 브라우저를 바로 설치할 것인지 물어보는데요. 기본값으로 진행해도 충분합니다.

설치가 끝나면 프로젝트에 다음과 같은 파일이 생깁니다.

프로젝트 구조
├── playwright.config.ts   # Playwright 설정 파일
├── tests/
│   └── example.spec.ts    # 예제 테스트
├── tests-examples/
│   └── demo-todo-app.spec.ts  # Todo 앱 예제
└── package.json

playwright.config.ts가 핵심 설정 파일이고, tests/ 디렉토리에 테스트 파일을 작성하게 됩니다.

기존 프로젝트에 Playwright만 추가하고 싶다면 패키지를 직접 설치할 수도 있습니다.

bun add -D @playwright/test

패키지를 설치한 후에는 테스트에 사용할 브라우저도 따로 설치해야 합니다.

bunx playwright install

이 명령어는 Chromium, Firefox, WebKit 브라우저 바이너리를 로컬에 다운로드합니다. CI 환경에서는 --with-deps 옵션을 추가하면 브라우저 실행에 필요한 시스템 의존성까지 함께 설치할 수 있습니다.

설정 파일 살펴보기

playwright.config.ts 파일을 열어보면 테스트 실행에 필요한 여러 옵션을 설정할 수 있습니다.

playwright.config.ts
import { defineConfig, devices } from "@playwright/test";

export default defineConfig({
  testDir: "./tests",
  fullyParallel: true,
  retries: 2,
  reporter: "html",
  use: {
    baseURL: "http://localhost:3000",
    trace: "on-first-retry",
  },
  projects: [
    {
      name: "chromium",
      use: { ...devices["Desktop Chrome"] },
    },
    {
      name: "firefox",
      use: { ...devices["Desktop Firefox"] },
    },
    {
      name: "webkit",
      use: { ...devices["Desktop Safari"] },
    },
  ],
  webServer: {
    command: "bun run dev",
    url: "http://localhost:3000",
    reuseExistingServer: true,
  },
});

여기서 눈여겨볼 부분이 몇 가지 있습니다. fullyParalleltrue로 설정하면 테스트 파일뿐만 아니라 파일 안의 개별 테스트도 병렬로 실행됩니다. projects 배열에 여러 브라우저를 지정하면 하나의 테스트를 여러 브라우저에서 돌릴 수 있어요. webServer 옵션을 설정해두면 테스트 실행 전에 개발 서버를 자동으로 시작해줍니다.

trace: "on-first-retry"는 테스트가 실패해서 재시도할 때 트레이스를 기록하겠다는 뜻인데요. 이 트레이스는 나중에 디버깅할 때 정말 유용합니다.

첫 번째 테스트 작성하기

이제 실제로 테스트를 작성해볼까요? Playwright 테스트는 JestVitest를 써본 적이 있다면 꽤 익숙하게 느껴질 거예요.

tests/homepage.spec.ts
import { test, expect } from "@playwright/test";

test("홈페이지에 제목이 표시된다", async ({ page }) => {
  await page.goto("/");
  await expect(page).toHaveTitle(/My App/);
});

test 함수로 테스트를 정의하고, expect로 결과를 검증합니다. 여기서 page는 Playwright가 자동으로 제공하는 브라우저 페이지 객체예요. 테스트마다 새로운 브라우저 컨텍스트가 생성되기 때문에 테스트 간 상태가 격리됩니다.

조금 더 실용적인 예제를 작성해보겠습니다. 사용자가 로그인하는 흐름을 테스트한다고 가정해볼게요.

tests/login.spec.ts
import { test, expect } from "@playwright/test";

test("사용자가 이메일과 비밀번호로 로그인할 수 있다", async ({ page }) => {
  // 로그인 페이지로 이동
  await page.goto("/login");

  // 이메일과 비밀번호 입력
  await page.getByLabel("이메일").fill("user@example.com");
  await page.getByLabel("비밀번호").fill("password123");

  // 로그인 버튼 클릭
  await page.getByRole("button", { name: "로그인" }).click();

  // 대시보드로 이동했는지 확인
  await expect(page).toHaveURL("/dashboard");
  await expect(page.getByText("환영합니다")).toBeVisible();
});

코드를 읽어보면 거의 자연어에 가깝죠? “라벨이 ‘이메일’인 입력 필드를 찾아서 값을 채우고, ‘로그인’이라는 버튼을 클릭한다.” 이런 식으로 사용자의 실제 행동을 그대로 코드로 옮길 수 있습니다.

요소 찾기: Locator

Playwright에서 페이지의 요소를 찾는 방법을 Locator라고 부릅니다. CSS 셀렉터나 XPath도 사용할 수 있지만, Playwright는 사용자가 페이지를 인식하는 방식과 비슷한 시맨틱 Locator를 권장합니다.

가장 많이 쓰는 Locator는 getByRole입니다. HTML 요소의 역할(role)을 기준으로 찾기 때문에 접근성도 자연스럽게 챙기게 돼요.

// 버튼 찾기
page.getByRole("button", { name: "제출" });

// 링크 찾기
page.getByRole("link", { name: "회원가입" });

// 체크박스 찾기
page.getByRole("checkbox", { name: "약관 동의" });

// 텍스트 입력 필드 찾기
page.getByRole("textbox", { name: "검색" });

getByLabel<label> 태그와 연결된 폼 요소를 찾을 때 쓰고, getByPlaceholder는 placeholder 텍스트로 찾습니다. getByText는 화면에 보이는 텍스트로 요소를 찾을 때 유용해요.

// label로 찾기
page.getByLabel("사용자 이름");

// placeholder로 찾기
page.getByPlaceholder("이메일을 입력하세요");

// 화면 텍스트로 찾기
page.getByText("검색 결과가 없습니다");

getByTestIddata-testid 속성으로 요소를 찾습니다. 시맨틱 Locator로 특정하기 어려운 요소에 사용하면 됩니다.

page.getByTestId("submit-button");

자주 쓰는 동작과 검증

요소를 찾았으면 여러 동작을 해볼 수 있어요.

// 클릭
await page.getByRole("button", { name: "저장" }).click();

// 텍스트 입력
await page.getByLabel("이름").fill("홍길동");

// 체크박스 토글
await page.getByRole("checkbox", { name: "알림 수신" }).check();

// 드롭다운 선택
await page.getByLabel("국가").selectOption("KR");

// 키보드 입력
await page.keyboard.press("Enter");

검증은 expect와 함께 여러 매처(matcher)를 사용합니다. Playwright의 매처는 자동으로 재시도하면서 조건이 만족될 때까지 기다려주기 때문에 비동기 UI 변화를 안정적으로 테스트할 수 있습니다.

// 요소가 보이는지
await expect(page.getByText("저장 완료")).toBeVisible();

// 요소가 없는지
await expect(page.getByText("에러")).not.toBeVisible();

// URL 확인
await expect(page).toHaveURL("/success");

// 페이지 제목 확인
await expect(page).toHaveTitle("대시보드");

// 입력값 확인
await expect(page.getByLabel("이름")).toHaveValue("홍길동");

// 요소 개수 확인
await expect(page.getByRole("listitem")).toHaveCount(5);

테스트 실행하기

테스트를 실행하는 가장 기본적인 명령어는 다음과 같습니다.

bunx playwright test

기본적으로 모든 테스트가 headless 모드(브라우저 창 없이)로 실행됩니다. 설정 파일의 projects에 지정한 모든 브라우저에서 테스트가 돌아가요.

특정 파일이나 특정 테스트만 실행하고 싶을 때는 이렇게 합니다.

# 특정 파일만 실행
bunx playwright test tests/login.spec.ts

# 테스트 이름으로 필터링
bunx playwright test -g "로그인"

# 특정 프로젝트(브라우저)만 실행
bunx playwright test --project=chromium

브라우저 창을 직접 보면서 테스트하고 싶다면 --headed 옵션을 추가합니다.

bunx playwright test --headed

테스트 실행이 끝나면 결과 리포트를 확인할 수 있어요.

bunx playwright show-report

이 명령어는 HTML 리포트를 브라우저에서 열어줍니다. 각 테스트의 성공/실패 여부, 실행 시간, 스크린샷, 트레이스 등을 한눈에 볼 수 있습니다.

테스트 구조화하기

테스트가 많아지면 describe로 관련 테스트를 묶고, beforeEach로 공통 설정을 분리하면 관리하기 편해집니다.

tests/cart.spec.ts
import { test, expect } from "@playwright/test";

test.describe("장바구니", () => {
  test.beforeEach(async ({ page }) => {
    // 각 테스트 전에 장바구니 페이지로 이동
    await page.goto("/cart");
  });

  test("빈 장바구니 메시지가 표시된다", async ({ page }) => {
    await expect(page.getByText("장바구니가 비어있습니다")).toBeVisible();
  });

  test("상품을 추가하면 목록에 나타난다", async ({ page }) => {
    await page.goto("/products/1");
    await page.getByRole("button", { name: "장바구니 담기" }).click();
    await page.goto("/cart");
    await expect(page.getByRole("listitem")).toHaveCount(1);
  });
});

디버깅하기

테스트가 실패하면 원인을 찾아야 하는데, Playwright는 디버깅 도구가 정말 훌륭합니다.

가장 먼저 시도해볼 것은 UI 모드입니다.

bunx playwright test --ui

UI 모드를 열면 테스트 목록이 왼쪽에 나타나고, 각 테스트를 클릭하면 단계별로 브라우저 상태를 확인할 수 있어요. 마치 동영상을 재생하듯이 각 동작 시점의 DOM 스냅샷을 타임라인으로 탐색할 수 있습니다. 특정 지점에서 멈추고 브라우저의 DevTools를 열어 직접 확인하는 것도 가능합니다.

디버그 모드도 유용합니다.

bunx playwright test --debug

디버그 모드는 브라우저 창과 함께 Playwright Inspector가 열리는데, 각 단계를 하나씩 실행하면서 어디서 문제가 생기는지 확인할 수 있습니다. Inspector에서 Locator를 입력하면 해당 요소가 페이지에서 바로 하이라이트되기 때문에 “왜 이 요소를 못 찾지?” 같은 문제를 빠르게 해결할 수 있어요.

코드 안에서 특정 지점에 멈추고 싶다면 page.pause()를 사용합니다.

test("디버깅 예제", async ({ page }) => {
  await page.goto("/");
  await page.pause(); // 여기서 멈춤, Inspector가 열림
  await page.getByRole("button", { name: "시작" }).click();
});

또한 앞서 설정 파일에서 봤던 트레이스 기능도 디버깅에 큰 도움이 됩니다. 테스트가 실패하면 트레이스 파일이 생성되는데, 이 파일에는 각 동작 시점의 스크린샷, DOM 스냅샷, 네트워크 요청, 콘솔 로그가 모두 담겨 있습니다. 트레이스를 보려면 다음 명령어를 실행하면 됩니다.

bunx playwright show-trace trace.zip

CI에서 테스트가 실패했을 때 로컬에서 재현하기 어려운 경우가 종종 있는데, 이때 트레이스가 정말 빛을 발합니다.

코드 생성기 활용하기

테스트 코드를 처음부터 다 손으로 작성하는 건 번거롭죠. Playwright의 코드 생성기(codegen)를 사용하면 브라우저에서 직접 조작한 내용이 자동으로 테스트 코드로 변환됩니다.

bunx playwright codegen http://localhost:3000

이 명령어를 실행하면 브라우저 창과 함께 코드 생성 창이 열립니다. 브라우저에서 원하는 대로 클릭하고 입력하면 오른쪽에 테스트 코드가 실시간으로 만들어져요. 생성된 코드를 복사해서 테스트 파일에 붙여넣고 필요한 부분만 다듬으면 됩니다.

CI에서 실행하기

Playwright 테스트를 GitHub Actions에서 자동으로 실행하려면 다음과 같이 워크플로우를 구성합니다.

.github/workflows/e2e.yml
name: E2E Tests
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: oven-sh/setup-bun@v2
      - run: bun install
      - run: bunx playwright install --with-deps
      - run: bunx playwright test
      - uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: playwright-report
          path: playwright-report/

CI에서 테스트가 실패하면 리포트가 아티팩트로 업로드되기 때문에 로컬에서 다운로드해서 어떤 테스트가 왜 실패했는지 확인할 수 있습니다. 설정 파일에서 retries를 CI 환경에서만 늘려놓으면 네트워크 지연 같은 일시적인 문제로 인한 불안정한 실패(flaky failure)를 줄일 수 있어요.

마치며

이 글에서는 Playwright의 설치부터 테스트 작성, 실행, 디버깅까지 E2E 테스트의 기본적인 흐름을 살펴봤습니다. 자동 대기, 시맨틱 Locator, 트레이스, UI 모드 같은 기능 덕분에 안정적이고 읽기 좋은 E2E 테스트를 작성할 수 있다는 것을 느끼셨을 거예요.

JestVitest로 단위 테스트를 챙기고, Playwright로 사용자 관점의 E2E 테스트까지 더하면 애플리케이션에 대한 신뢰도가 확실히 달라집니다. 참고로 Playwright는 테스트뿐 아니라 AI 에이전트의 브라우저 자동화에도 활용되고 있는데, 이에 대해서는 Playwright MCP로 AI 에이전트에게 브라우저 자동화 맡기기에서 자세히 다루고 있습니다.

더 자세한 내용은 Playwright 공식 문서를 참고하세요.

This work is licensed under CC BY 4.0 CC BY

개발자를 위한 뉴스레터

달레가 정리한 AI 개발 트렌드와 직접 만든 콘텐츠를 전해드립니다.

Discord