플레이라이트, Next.js, CI | E2E 테스트를 로컬에서 깔고 깃허브 액션에 붙이는 순서
2026년 E2E 테스트 선택 기준부터 Playwright 설치, playwright.config.ts webServer 설정, 첫 테스트 작성, 깃허브 액션 브라우저 캐시 연결까지 Next.js 기준으로 정리한다. 불안정한 테스트가 나오는 waitForTimeout 남용·전역 상태 공유·외부 API 의존 패턴도 포함.
E2E 테스트를 처음 붙이는 팀이 2026년에 고르는 도구는 대부분 Playwright입니다. Cypress가 한때 표준처럼 쓰였지만, 멀티 탭·iframe·네트워크 조건 설정에서 Playwright가 분명히 낫습니다. 저는 Next.js 사이트에 Playwright를 붙이고 깃허브 액션에 연결하는 과정에서 막히는 자리를 한 번씩 만났고, 그 케이스만 정리합니다.
Cypress를 이미 쓰고 있다면 교체 이유가 없습니다. 새로 E2E 테스트를 시작한다면 Playwright를 먼저 보라는 게 제 입장입니다. 테스트 러너 비교보다는 실제 설치와 CI 연결 흐름을 중심으로 씁니다.
Playwright와 Cypress, 어디서 차이가 나나
세세한 API 비교보다 실제 프로젝트에서 선택 기준이 되는 항목만 추립니다.
항목
Playwright
Cypress
지원 브라우저
Chrome, Firefox, WebKit (Safari)
Chrome, Firefox, Edge (WebKit 실험적)
멀티 탭 / 멀티 윈도우
기본 지원
제한적 (우회 필요)
iframe
직접 접근 가능
동일 도메인만 쉬움
네트워크 가로채기
route()로 요청/응답 수정
cy.intercept()로 가능
병렬 실행
기본 제공 (shard 옵션)
유료 Cloud가 있어야 풀 병렬
실행 속도 (100테스트 기준)
빠름 (병렬 기본)
느림 (병렬 제한)
디버깅 UI
VS Code 확장, Trace Viewer
Cypress App (직관적)
학습 곡선
중간
낮음 (API가 직관적)
처음 접하는 팀에는 Cypress가 학습 곡선이 낮습니다. 그런데 Safari 호환 테스트나 병렬 실행 비용이 문제가 되는 시점에 Playwright로 넘어오는 경우가 많습니다. 처음부터 Playwright를 쓰면 그 비용을 아낄 수 있습니다.
Playwright는 Chrome, Firefox, WebKit 세 브라우저를 한 번에 테스트한다
Next.js 프로젝트에 Playwright 설치하기
설치는 단순합니다. 세팅 명령 하나에 브라우저 바이너리 다운로드까지 포함됩니다.
Playwright 설치와 초기 설정
# npm
npm init playwright@latest
# 또는 pnpm
pnpm create playwright
# 선택 항목
# ✔ Where to put your end-to-end tests? › tests
# ✔ Add a GitHub Actions workflow? › Yes
# ✔ Install Playwright browsers? › Yes
init 명령이 playwright.config.ts와 예제 테스트 파일, 깃허브 액션 워크플로 파일을 만들어 줍니다. 브라우저 바이너리는 ~/.cache/ms-playwright에 저장됩니다. CI에서는 이 경로를 캐시에 올려두면 재설치 시간을 아낍니다.
playwright.config.ts — Next.js 로컬 서버 연동
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
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'] } },
],
// 테스트 실행 전 Next.js 개발 서버 자동 시작
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
})
webServer 설정이 핵심입니다. 로컬에서는 이미 켜진 서버를 재사용하고(reuseExistingServer: true), CI에서는 새로 시작합니다. 빌드된 결과로 테스트하려면 command: 'npm run build && npm run start'로 바꿉니다.
첫 E2E 테스트 작성과 실행
Playwright의 로케이터 API는 ARIA 역할과 텍스트를 기준으로 요소를 찾습니다. CSS 셀렉터보다 더 사람 읽기 쉽고, 마크업이 바뀌어도 테스트가 덜 깨집니다.
기본 E2E 테스트 예시 (tests/home.spec.ts)
import { test, expect } from '@playwright/test'
test('홈 페이지 제목 확인', async ({ page }) => {
await page.goto('/')
await expect(page).toHaveTitle(/tech.ambitstock/)
})
test('검색 기능 동작 확인', async ({ page }) => {
await page.goto('/')
// 검색창 찾기
const searchInput = page.getByRole('searchbox')
await searchInput.fill('Next.js')
await searchInput.press('Enter')
// 결과 페이지 확인
await expect(page).toHaveURL(/search/)
await expect(page.getByText('Next.js')).toBeVisible()
})
test('포스트 상세 페이지 OG 메타 확인', async ({ page }) => {
await page.goto('/node-type-stripping-erasable-syntax-tsx-2026/')
// OG title 메타 태그 확인
const ogTitle = page.locator('meta[property="og:title"]')
await expect(ogTitle).toHaveAttribute('content', /타입 스트리핑/)
})
실행은 npx playwright test입니다. 특정 파일만 돌리려면 npx playwright test home.spec.ts입니다. UI 모드로 보려면 npx playwright test --ui입니다. 실패한 테스트의 trace를 보려면 npx playwright show-report입니다.
--ui 플래그로 열리는 UI 모드에서 테스트를 단계별로 따라갈 수 있다
깃허브 액션에 Playwright 연결하기
init이 만들어 준 워크플로 파일을 그대로 쓰면 됩니다. 브라우저 캐시만 추가하면 실행 시간을 줄일 수 있습니다.
Playwright 테스트가 로컬에서는 통과하고 CI에서 간헐적으로 실패하는 경우가 있습니다. 대부분 세 가지 패턴입니다.
⚠️ page.waitForTimeout() 남용 | await page.waitForTimeout(1000)으로 기다리는 패턴은 CI 서버 속도에 따라 불안정합니다. 대신 await page.waitForSelector('...')나 await expect(element).toBeVisible()처럼 조건 기반으로 기다립니다. Playwright의 자동 재시도가 30초 타임아웃 안에서 알아서 기다립니다.
⚠️ 전역 상태 공유 | 테스트마다 독립적인 상태를 써야 합니다. 로그인 상태를 테스트 간에 공유하면 실행 순서에 따라 결과가 달라집니다. Playwright의 storageState를 쓰면 로그인 상태를 파일로 저장해서 각 테스트가 깔끔하게 씁니다.
⚠️ 외부 API 의존 | 실제 외부 API를 호출하는 테스트는 네트워크 상태에 따라 불안정합니다. page.route()로 API 응답을 가로채서 목업 데이터를 돌려주는 방식이 안정적입니다. E2E 테스트에서 외부 API를 실제로 호출해야 하는 건 일부 통합 테스트에서만입니다.
CI에서 통과한 Playwright 테스트 결과와 HTML 리포트
자주 묻는 것들
Playwright로 인증이 필요한 페이지를 테스트하려면?
globalSetup에서 로그인 후 storageState를 파일로 저장합니다. 각 테스트는 use: { storageState: 'auth.json' }으로 저장된 세션을 불러와서 씁니다. 테스트마다 로그인을 반복하지 않아도 됩니다.
Next.js App Router와 Playwright가 잘 맞나?
잘 맞습니다. webServer에 npm run dev나 npm run start를 연결하면 됩니다. Server Components, Server Actions 모두 브라우저가 렌더한 결과를 테스트하는 거라서 Playwright 입장에서는 다르지 않습니다.
테스트 파일을 어디 두는 게 좋나?
tests/ 폴더에 모아두는 게 일반적입니다. 컴포넌트 단위 테스트와 분리해야 관리가 쉽습니다. playwright.config.ts의 testDir으로 경로를 지정합니다.
Playwright 컴포넌트 테스트(CT)가 뭔가?
Playwright는 브라우저 E2E 테스트 외에 컴포넌트 단위 테스트도 지원합니다(@playwright/experimental-ct-react). 아직 실험적 단계이고, 컴포넌트 테스트는 Vitest + Testing Library 조합이 더 성숙합니다. E2E는 Playwright, 컴포넌트는 Vitest로 분리하는 게 현재 권장 패턴입니다.
Cypress에서 Playwright로 마이그레이션할 때 얼마나 걸리나?
API 구조가 달라서 테스트 파일을 그대로 옮길 수는 없습니다. 테스트 수와 커스텀 커맨드 복잡도에 따라 다르지만, 중간 규모 프로젝트(50~100개 테스트) 기준으로 2~5일 정도 예상합니다. 공식 마이그레이션 가이드가 있습니다: playwright.dev/docs/migrating-from-cypress