---
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<Auths>({
  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(<Button onClick={() => { clicked = true; }}>Click</Button>);
  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]]
