--- id: testing-playwright-advanced title: Playwright 심화 — Trace / Auth / Parallel category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [testing, playwright, e2e, vibe-coding] tech_stack: { language: "TS / Playwright", applicable_to: ["Frontend"] } applied_in: [] aliases: [Playwright, trace viewer, storage state, fixtures, page object, auto-wait] --- # Playwright 심화 > Modern E2E. **Auto-wait + trace viewer + parallel + auth state reuse**. Cypress 보다 빠르고 multi-browser. CI 핵심: trace + screenshot + storage state. ## 📖 핵심 개념 - Auto-wait: 자동으로 element 대기. - Trace: 단계별 screenshot + network + console. - Storage state: 한 번 로그인 → JSON 저장 → 재사용. - Fixtures: test 별 setup/teardown. ## 💻 코드 패턴 ### 기본 ```ts import { test, expect } from '@playwright/test'; test('login', async ({ page }) => { await page.goto('/login'); await page.getByLabel('Email').fill('a@b.com'); await page.getByLabel('Password').fill('pw'); await page.getByRole('button', { name: 'Sign in' }).click(); await expect(page).toHaveURL('/dashboard'); await expect(page.getByText('Welcome')).toBeVisible(); }); ``` ### Locator (modern, role-based) ```ts page.getByRole('button', { name: 'Save' }); page.getByLabel('Email'); page.getByPlaceholder('Search…'); page.getByText('Welcome'); page.getByTestId('user-card'); // data-testid ``` → 위에서 아래 우선. testId 는 마지막. ### Storage state (auth reuse) ```ts // global-setup.ts import { chromium } from '@playwright/test'; async function globalSetup() { const browser = await chromium.launch(); const page = await browser.newPage(); await page.goto('http://localhost:3000/login'); await page.getByLabel('Email').fill('a@b.com'); await page.getByLabel('Password').fill('pw'); await page.getByRole('button', { name: 'Sign in' }).click(); await page.waitForURL('/dashboard'); await page.context().storageState({ path: 'state.json' }); await browser.close(); } export default globalSetup; ``` ```ts // playwright.config.ts export default defineConfig({ globalSetup: './global-setup', use: { storageState: 'state.json' }, }); ``` → 모든 test 가 이미 로그인 상태로 시작. ### 다양한 user (worker storage) ```ts // fixtures.ts type Auths = { adminPage: Page; userPage: Page }; export const test = base.extend({ adminPage: async ({ browser }, use) => { const ctx = await browser.newContext({ storageState: 'admin-state.json' }); await use(await ctx.newPage()); await ctx.close(); }, userPage: async ({ browser }, use) => { const ctx = await browser.newContext({ storageState: 'user-state.json' }); await use(await ctx.newPage()); await ctx.close(); }, }); test('admin sees panel', async ({ adminPage }) => { await adminPage.goto('/admin'); await expect(adminPage.getByText('Admin Panel')).toBeVisible(); }); ``` ### Trace ```ts // playwright.config.ts use: { trace: 'on-first-retry', screenshot: 'only-on-failure', video: 'retain-on-failure' } ``` ```bash # 실패 후 trace 보기 npx playwright show-trace trace.zip ``` ### Network mocking ```ts test('handles api error', async ({ page }) => { await page.route('**/api/users', (route) => route.fulfill({ status: 500, body: JSON.stringify({ error: 'server error' }), })); await page.goto('/users'); await expect(page.getByText('Failed to load')).toBeVisible(); }); ``` ### API testing ```ts test('create user via API', async ({ request }) => { const r = await request.post('/api/users', { data: { email: 'a@b.com' }, }); expect(r.status()).toBe(201); const body = await r.json(); expect(body.id).toBeTruthy(); }); ``` ### Component testing (CT) ```ts import { test, expect } from '@playwright/experimental-ct-react'; import { Button } from './Button'; test('Button click', async ({ mount }) => { let clicked = false; const btn = await mount(); await btn.click(); expect(clicked).toBe(true); }); ``` ### Visual regression ```ts await expect(page).toHaveScreenshot('home.png', { maxDiffPixelRatio: 0.01, }); ``` → 첫 run 시 저장, 이후 비교. ### Parallel + sharding ```bash npx playwright test --workers=4 npx playwright test --shard=1/4 # CI 분산 ``` ### Retry on flake ```ts // config retries: process.env.CI ? 2 : 0, ``` ### Page object (재사용) ```ts class LoginPage { constructor(private page: Page) {} async goto() { await this.page.goto('/login'); } async login(email: string, pw: string) { await this.page.getByLabel('Email').fill(email); await this.page.getByLabel('Password').fill(pw); await this.page.getByRole('button', { name: 'Sign in' }).click(); } } test('login', async ({ page }) => { const p = new LoginPage(page); await p.goto(); await p.login('a@b.com', 'pw'); }); ``` ## 🤔 의사결정 기준 | 상황 | 추천 | |---|---| | Modern E2E web | Playwright | | Multi-browser (Chrome / FF / Safari) | Playwright | | Cypress 마이그 | Playwright (속도 + 안정성) | | Component test | Playwright CT 또는 Vitest + RTL | | Mobile E2E | Maestro / Detox (Playwright X) | | API + UI | Playwright (둘 다) | ## ❌ 안티패턴 - **`waitForTimeout(2000)`**: flake. auto-wait + expect. - **CSS selector 의존 (`.btn-primary`)**: 변경 시 깨짐. role / label. - **Real DB / external API**: flake. mock / seed. - **Trace off prod**: 실패 분석 못 함. - **Storage state 안 씀**: 매 test 로그인 — 느림. - **Test 간 상태 의존**: serial. 독립 setup. - **Sleep 으로 animation 기다림**: visible() 또는 Promise.race. ## 🤖 LLM 활용 힌트 - getByRole > getByLabel > getByTestId. - Storage state + fixtures = 빠름. - Trace on-first-retry + screenshot + video retain. ## 🔗 관련 문서 - [[Mobile_E2E_Testing]] - [[Frontend_A11y_Testing]] - [[Testing_Mocking_Boundaries]]