Sam Baek, The Dev's Corner

๐Ÿงช E2E ํ…Œ์ŠคํŠธ ์™„๋ฒฝ ๊ฐ€์ด๋“œ (Playwright, Cypress)

08 Nov 2025

E2E ํ…Œ์ŠคํŠธ๋ž€ ๋ฌด์—‡์ธ๊ฐ€


์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋งŒ๋“ค์—ˆ๋‹ค๋ฉด,
์‹ค์ œ๋กœ ์ž˜ ๋™์ž‘ํ•˜๋Š”์ง€ ํ™•์ธํ•ด์•ผ ํ•œ๋‹ค.

E2E(End-to-End) ํ…Œ์ŠคํŠธ๋Š”
์‹ค์ œ ์‚ฌ์šฉ์ž๊ฐ€ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ฒ˜๋Ÿผ
์ฒ˜์Œ๋ถ€ํ„ฐ ๋๊นŒ์ง€ ์ „์ฒด ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ํ…Œ์ŠคํŠธํ•˜๋Š” ๋ฐฉ๋ฒ•์ด๋‹ค.

๋งˆ์น˜ ์ž๋™์ฐจ ๊ณต์žฅ์—์„œ
์—”์ง„, ๋ฐ”ํ€ด, ๋ฌธ์„ ๋”ฐ๋กœ ํ…Œ์ŠคํŠธํ•˜๊ณ  (๋‹จ์œ„ ํ…Œ์ŠคํŠธ)
์กฐ๋ฆฝ ํ›„ ์‹ค์ œ๋กœ ๋„๋กœ๋ฅผ ์ฃผํ–‰ํ•ด๋ณด๋Š” ๊ฒƒ (E2E ํ…Œ์ŠคํŠธ)
์ฒ˜๋Ÿผ ๋ง์ด๋‹ค.

์™œ E2E ํ…Œ์ŠคํŠธ๋ฅผ ํ•ด์•ผ ํ• ๊นŒ?


์ด์œ  1: ์‹ค์ œ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ๊ฒ€์ฆ
๊ฐ ๋ถ€๋ถ„์ด ์ž˜ ๋™์ž‘ํ•ด๋„ ์ „์ฒด๊ฐ€ ์•ˆ ๋  ์ˆ˜ ์žˆ๋‹ค.

์ด์œ  2: ํšŒ๊ท€ ๋ฒ„๊ทธ ๋ฐฉ์ง€
์ƒˆ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ ์‹œ ๊ธฐ์กด ๊ธฐ๋Šฅ์ด ๋ง๊ฐ€์ง€๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€.

์ด์œ  3: ์ž๋™ํ™”๋กœ ์‹œ๊ฐ„ ์ ˆ์•ฝ
์ˆ˜๋™ ํ…Œ์ŠคํŠธ๋Š” ์‹œ๊ฐ„์ด ๋งŽ์ด ๊ฑธ๋ฆฌ๊ณ  ์‹ค์ˆ˜ํ•  ์ˆ˜ ์žˆ๋‹ค.

์ด์œ  4: ์ž์‹ ๊ฐ ์žˆ๋Š” ๋ฐฐํฌ
ํ…Œ์ŠคํŠธ๊ฐ€ ํ†ต๊ณผํ•˜๋ฉด ์•ˆ์‹ฌํ•˜๊ณ  ๋ฐฐํฌํ•  ์ˆ˜ ์žˆ๋‹ค.

๊ธฐ๋ณธ ๊ฐœ๋… ์š”์•ฝ


๐Ÿท๏ธ ํ…Œ์ŠคํŠธ ํ”ผ๋ผ๋ฏธ๋“œ


        /\
       /E2E\      ์ ์Œ, ๋А๋ฆผ, ๋น„์šฉ ๋†’์Œ
      /------\
     /ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ\   ์ค‘๊ฐ„
    /----------\
   /  ๋‹จ์œ„ ํ…Œ์ŠคํŠธ  \  ๋งŽ์Œ, ๋น ๋ฆ„, ๋น„์šฉ ๋‚ฎ์Œ
  /--------------\


1. ๋‹จ์œ„ ํ…Œ์ŠคํŠธ (Unit Test)


๊ฐœ๋…: ํ•จ์ˆ˜๋‚˜ ๋ฉ”์„œ๋“œ ๋‹จ์œ„๋กœ ํ…Œ์ŠคํŠธ

์˜ˆ์‹œ:
๋”ํ•˜๊ธฐ ํ•จ์ˆ˜๊ฐ€ 2 + 3 = 5๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š”์ง€ ํ™•์ธ

ํŠน์ง•:

  • ๋น ๋ฆ„ (๋ฐ€๋ฆฌ์ดˆ ๋‹จ์œ„)
  • ๋งŽ์ด ์ž‘์„ฑ (์ˆ˜๋ฐฑ~์ˆ˜์ฒœ ๊ฐœ)
  • ๋ฒ„๊ทธ ์œ„์น˜ ๋ช…ํ™•


2. ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ (Integration Test)


๊ฐœ๋…: ์—ฌ๋Ÿฌ ๋ชจ๋“ˆ์ด ํ•จ๊ป˜ ๋™์ž‘ํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธ

์˜ˆ์‹œ:
ํšŒ์›๊ฐ€์ž… API๊ฐ€ DB์— ์ €์žฅ๋˜๋Š”์ง€ ํ™•์ธ

ํŠน์ง•:

  • ์ค‘๊ฐ„ ์†๋„ (์ดˆ ๋‹จ์œ„)
  • ์ค‘๊ฐ„ ๊ฐœ์ˆ˜ (์ˆ˜์‹ญ~์ˆ˜๋ฐฑ ๊ฐœ)
  • ๋ชจ๋“ˆ ๊ฐ„ ์—ฐ๋™ ํ™•์ธ


3. E2E ํ…Œ์ŠคํŠธ (End-to-End Test)


๊ฐœ๋…: ์‹ค์ œ ์‚ฌ์šฉ์ž์ฒ˜๋Ÿผ ์ „์ฒด ์‹œ๋‚˜๋ฆฌ์˜ค ํ…Œ์ŠคํŠธ

์˜ˆ์‹œ:
๋กœ๊ทธ์ธ โ†’ ์ƒํ’ˆ ๊ฒ€์ƒ‰ โ†’ ์žฅ๋ฐ”๊ตฌ๋‹ˆ โ†’ ๊ฒฐ์ œ ์ „์ฒด ํ๋ฆ„

ํŠน์ง•:

  • ๋А๋ฆผ (๋ถ„ ๋‹จ์œ„)
  • ์ ๊ฒŒ ์ž‘์„ฑ (์ˆ˜์‹ญ ๊ฐœ)
  • ์‹ค์ œ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ๊ฒ€์ฆ


๐Ÿท๏ธ E2E ํ…Œ์ŠคํŠธ ๋„๊ตฌ ๋น„๊ต


๋„๊ตฌ ์–ธ์–ด ๋ธŒ๋ผ์šฐ์ € ์†๋„ ํŠน์ง•
Playwright JS/TS Chrome, Firefox, Safari ๋น ๋ฆ„ ์ตœ์‹  ๊ธฐ์ˆ , ์ž๋™ ๋Œ€๊ธฐ
Cypress JS/TS Chrome, Edge ๋น ๋ฆ„ ๊ฐœ๋ฐœ์ž ์นœํ™”์ , ๋””๋ฒ„๊น… ์ข‹์Œ
Selenium ๋‹ค์–‘ ๋ชจ๋“  ๋ธŒ๋ผ์šฐ์ € ๋А๋ฆผ ์˜ค๋ž˜๋จ, ์•ˆ์ •์ 
Puppeteer JS Chrome๋งŒ ๋น ๋ฆ„ ํ—ค๋“œ๋ฆฌ์Šค ํŠนํ™”


๐Ÿท๏ธ Playwright ํ•ต์‹ฌ ๊ฐœ๋…


1. Browser Context


๊ฐœ๋…: ๋…๋ฆฝ๋œ ๋ธŒ๋ผ์šฐ์ € ์„ธ์…˜ (์ฟ ํ‚ค, ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€ ๊ฒฉ๋ฆฌ)

์‹œํฌ๋ฆฟ ๋ชจ๋“œ ๋น„์œ :
๊ฐ Context๋Š” ๋…๋ฆฝ๋œ ์‹œํฌ๋ฆฟ ์ฐฝ์ฒ˜๋Ÿผ ๋™์ž‘

2. Page


๊ฐœ๋…: ๋ธŒ๋ผ์šฐ์ € ํƒญ

3. Locator


๊ฐœ๋…: ์š”์†Œ๋ฅผ ์ฐพ๋Š” ๋ฐฉ๋ฒ•

// CSS Selector
page.locator('.submit-button')

// Text๋กœ ์ฐพ๊ธฐ
page.locator('text=๋กœ๊ทธ์ธ')

// Role๋กœ ์ฐพ๊ธฐ
page.getByRole('button', { name: '์ œ์ถœ' })


4. Auto-waiting


๊ฐœ๋…: ์ž๋™์œผ๋กœ ์š”์†Œ๊ฐ€ ๋‚˜ํƒ€๋‚  ๋•Œ๊นŒ์ง€ ๋Œ€๊ธฐ

// โŒ Selenium (๋ช…์‹œ์  ๋Œ€๊ธฐ ํ•„์š”)
await driver.wait(until.elementLocated(By.id('button')), 5000);

// โœ… Playwright (์ž๋™ ๋Œ€๊ธฐ)
await page.click('#button'); // ์ž๋™์œผ๋กœ ๋Œ€๊ธฐ


์‹ค์ „ ์˜ˆ์‹œ


๐Ÿท๏ธ Playwright ์„ค์น˜ ๋ฐ ์„ค์ •


1. ์„ค์น˜


npm init playwright@latest

# ์„ค์น˜ ์˜ต์…˜
โœ” Do you want to use TypeScript or JavaScript? ยท TypeScript
โœ” Where to put your end-to-end tests? ยท tests
โœ” Add a GitHub Actions workflow? ยท Yes


2. ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ


project/
โ”œโ”€โ”€ tests/
โ”‚   โ”œโ”€โ”€ login.spec.ts
โ”‚   โ”œโ”€โ”€ checkout.spec.ts
โ”‚   โ””โ”€โ”€ search.spec.ts
โ”œโ”€โ”€ playwright.config.ts
โ””โ”€โ”€ package.json


3. ์„ค์ • ํŒŒ์ผ (playwright.config.ts)


import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  
  // ํƒ€์ž„์•„์›ƒ
  timeout: 30 * 1000,
  
  // ์žฌ์‹œ๋„
  retries: process.env.CI ? 2 : 0,
  
  // ๋ณ‘๋ ฌ ์‹คํ–‰
  workers: process.env.CI ? 1 : undefined,
  
  // ๋ฆฌํฌํ„ฐ
  reporter: 'html',
  
  use: {
    // ๊ธฐ๋ณธ URL
    baseURL: 'http://localhost:3000',
    
    // ์Šคํฌ๋ฆฐ์ƒท
    screenshot: 'only-on-failure',
    
    // ๋น„๋””์˜ค
    video: 'retain-on-failure',
    
    // ํŠธ๋ ˆ์ด์Šค
    trace: 'on-first-retry',
  },
  
  // ํ”„๋กœ์ ํŠธ (๋ธŒ๋ผ์šฐ์ €๋ณ„)
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
    {
      name: 'Mobile Chrome',
      use: { ...devices['Pixel 5'] },
    },
  ],
  
  // ๊ฐœ๋ฐœ ์„œ๋ฒ„ ์ž๋™ ์‹œ์ž‘
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
});


๐Ÿท๏ธ ๋กœ๊ทธ์ธ ํ…Œ์ŠคํŠธ


import { test, expect } from '@playwright/test';

test.describe('๋กœ๊ทธ์ธ ๊ธฐ๋Šฅ', () => {
  
  test.beforeEach(async ({ page }) => {
    // ๊ฐ ํ…Œ์ŠคํŠธ ์ „์— ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ์ด๋™
    await page.goto('/login');
  });
  
  test('์„ฑ๊ณต์ ์ธ ๋กœ๊ทธ์ธ', async ({ page }) => {
    // 1. ์ž…๋ ฅ ํ•„๋“œ ์ฐพ์•„์„œ ์ž…๋ ฅ
    await page.fill('input[name="username"]', 'testuser');
    await page.fill('input[name="password"]', 'password123');
    
    // 2. ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ ํด๋ฆญ
    await page.click('button:has-text("๋กœ๊ทธ์ธ")');
    
    // 3. ํ™ˆ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ ํ™•์ธ
    await expect(page).toHaveURL('/home');
    
    // 4. ํ™˜์˜ ๋ฉ”์‹œ์ง€ ํ™•์ธ
    await expect(page.locator('text=ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค')).toBeVisible();
  });
  
  test('์ž˜๋ชป๋œ ๋น„๋ฐ€๋ฒˆํ˜ธ', async ({ page }) => {
    await page.fill('input[name="username"]', 'testuser');
    await page.fill('input[name="password"]', 'wrongpassword');
    await page.click('button:has-text("๋กœ๊ทธ์ธ")');
    
    // ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ํ™•์ธ
    await expect(page.locator('.error-message'))
      .toHaveText('์•„์ด๋”” ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค');
  });
  
  test('๋นˆ ํ•„๋“œ ๊ฒ€์ฆ', async ({ page }) => {
    await page.click('button:has-text("๋กœ๊ทธ์ธ")');
    
    // HTML5 validation ํ™•์ธ
    const usernameInput = page.locator('input[name="username"]');
    await expect(usernameInput).toHaveAttribute('required', '');
  });
});


๐Ÿท๏ธ ์‡ผํ•‘๋ชฐ E2E ํ…Œ์ŠคํŠธ


import { test, expect } from '@playwright/test';

test('์ƒํ’ˆ ๊ตฌ๋งค ์ „์ฒด ํ๋ฆ„', async ({ page }) => {
  
  // 1. ํ™ˆํŽ˜์ด์ง€ ๋ฐฉ๋ฌธ
  await page.goto('/');
  await expect(page).toHaveTitle(/์‡ผํ•‘๋ชฐ/);
  
  // 2. ์ƒํ’ˆ ๊ฒ€์ƒ‰
  await page.fill('input[name="search"]', '๋…ธํŠธ๋ถ');
  await page.press('input[name="search"]', 'Enter');
  
  // ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ํ™•์ธ
  await expect(page.locator('.product-list .product')).toHaveCount(10, {
    timeout: 5000
  });
  
  // 3. ์ฒซ ๋ฒˆ์งธ ์ƒํ’ˆ ํด๋ฆญ
  await page.click('.product-list .product:first-child');
  
  // ์ƒํ’ˆ ์ƒ์„ธ ํŽ˜์ด์ง€ ํ™•์ธ
  await expect(page.locator('h1.product-title')).toBeVisible();
  
  // 4. ์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ์ถ”๊ฐ€
  await page.click('button:has-text("์žฅ๋ฐ”๊ตฌ๋‹ˆ ๋‹ด๊ธฐ")');
  
  // ์„ฑ๊ณต ํ† ์ŠคํŠธ ๋ฉ”์‹œ์ง€ ํ™•์ธ
  await expect(page.locator('.toast.success'))
    .toHaveText('์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค');
  
  // 5. ์žฅ๋ฐ”๊ตฌ๋‹ˆ๋กœ ์ด๋™
  await page.click('a[href="/cart"]');
  await expect(page).toHaveURL('/cart');
  
  // ์žฅ๋ฐ”๊ตฌ๋‹ˆ ์•„์ดํ…œ ํ™•์ธ
  await expect(page.locator('.cart-item')).toHaveCount(1);
  
  // 6. ์ˆ˜๋Ÿ‰ ๋ณ€๊ฒฝ
  await page.click('.quantity-increase');
  await expect(page.locator('.quantity-display')).toHaveText('2');
  
  // ์ด ๊ฐ€๊ฒฉ ์—…๋ฐ์ดํŠธ ํ™•์ธ
  const totalPrice = await page.locator('.total-price').textContent();
  expect(totalPrice).toContain('2,000,000'); // ์˜ˆ์‹œ
  
  // 7. ๊ฒฐ์ œ ํŽ˜์ด์ง€๋กœ ์ด๋™
  await page.click('button:has-text("์ฃผ๋ฌธํ•˜๊ธฐ")');
  await expect(page).toHaveURL('/checkout');
  
  // 8. ๋ฐฐ์†ก ์ •๋ณด ์ž…๋ ฅ
  await page.fill('input[name="name"]', 'ํ™๊ธธ๋™');
  await page.fill('input[name="phone"]', '010-1234-5678');
  await page.fill('input[name="address"]', '์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ');
  
  // 9. ๊ฒฐ์ œ ์ˆ˜๋‹จ ์„ ํƒ
  await page.check('input[value="card"]');
  
  // 10. ์ฃผ๋ฌธ ์™„๋ฃŒ
  await page.click('button:has-text("๊ฒฐ์ œํ•˜๊ธฐ")');
  
  // ์ฃผ๋ฌธ ์™„๋ฃŒ ํŽ˜์ด์ง€ ํ™•์ธ
  await expect(page).toHaveURL(/\/order\/\d+/);
  await expect(page.locator('.order-complete'))
    .toHaveText('์ฃผ๋ฌธ์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค');
});


๐Ÿท๏ธ API ์‘๋‹ต ๋Œ€๊ธฐ ๋ฐ ๋ชจํ‚น


API ์‘๋‹ต ๋Œ€๊ธฐ


test('๊ฒ€์ƒ‰ ํ›„ ๊ฒฐ๊ณผ ํ™•์ธ', async ({ page }) => {
  await page.goto('/');
  
  // API ์‘๋‹ต ๋Œ€๊ธฐ
  const responsePromise = page.waitForResponse(
    response => response.url().includes('/api/search') && response.status() === 200
  );
  
  await page.fill('input[name="search"]', '๋…ธํŠธ๋ถ');
  await page.press('input[name="search"]', 'Enter');
  
  const response = await responsePromise;
  const data = await response.json();
  
  // API ์‘๋‹ต ๊ฒ€์ฆ
  expect(data.results).toHaveLength(10);
  
  // UI ์—…๋ฐ์ดํŠธ ํ™•์ธ
  await expect(page.locator('.product')).toHaveCount(10);
});


API ๋ชจํ‚น


test('API ๋ชจํ‚น์œผ๋กœ ๋น ๋ฅธ ํ…Œ์ŠคํŠธ', async ({ page }) => {
  // API ์‘๋‹ต ๋ชจํ‚น
  await page.route('**/api/products', route => {
    route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({
        products: [
          { id: 1, name: 'ํ…Œ์ŠคํŠธ ์ƒํ’ˆ 1', price: 10000 },
          { id: 2, name: 'ํ…Œ์ŠคํŠธ ์ƒํ’ˆ 2', price: 20000 },
        ]
      })
    });
  });
  
  await page.goto('/products');
  
  // ๋ชจํ‚น๋œ ๋ฐ์ดํ„ฐ๋กœ ๋ Œ๋”๋ง ํ™•์ธ
  await expect(page.locator('.product')).toHaveCount(2);
  await expect(page.locator('.product:first-child .name'))
    .toHaveText('ํ…Œ์ŠคํŠธ ์ƒํ’ˆ 1');
});


๐Ÿท๏ธ Page Object Model (POM)


๊ฐœ๋…: ํŽ˜์ด์ง€๋ณ„๋กœ ํด๋ž˜์Šค๋ฅผ ๋งŒ๋“ค์–ด ์žฌ์‚ฌ์šฉ์„ฑ ํ–ฅ์ƒ

LoginPage ํด๋ž˜์Šค


// pages/LoginPage.ts
export class LoginPage {
  constructor(private page: Page) {}
  
  // Locators
  private usernameInput = () => this.page.locator('input[name="username"]');
  private passwordInput = () => this.page.locator('input[name="password"]');
  private loginButton = () => this.page.locator('button:has-text("๋กœ๊ทธ์ธ")');
  private errorMessage = () => this.page.locator('.error-message');
  
  // Actions
  async goto() {
    await this.page.goto('/login');
  }
  
  async login(username: string, password: string) {
    await this.usernameInput().fill(username);
    await this.passwordInput().fill(password);
    await this.loginButton().click();
  }
  
  async getErrorMessage() {
    return await this.errorMessage().textContent();
  }
  
  // Assertions
  async expectErrorMessage(message: string) {
    await expect(this.errorMessage()).toHaveText(message);
  }
}


ํ…Œ์ŠคํŠธ์—์„œ ์‚ฌ์šฉ


import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';

test('๋กœ๊ทธ์ธ ์‹คํŒจ', async ({ page }) => {
  const loginPage = new LoginPage(page);
  
  await loginPage.goto();
  await loginPage.login('testuser', 'wrongpassword');
  await loginPage.expectErrorMessage('์•„์ด๋”” ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค');
});


๐Ÿท๏ธ ์ธ์ฆ ์ƒํƒœ ์žฌ์‚ฌ์šฉ


๊ฐœ๋…: ๋งค๋ฒˆ ๋กœ๊ทธ์ธํ•˜์ง€ ์•Š๊ณ  ์ธ์ฆ ์ƒํƒœ ์ €์žฅ

์ธ์ฆ ์ƒํƒœ ์ €์žฅ


// auth.setup.ts
import { test as setup } from '@playwright/test';

const authFile = 'playwright/.auth/user.json';

setup('authenticate', async ({ page }) => {
  await page.goto('/login');
  await page.fill('input[name="username"]', 'testuser');
  await page.fill('input[name="password"]', 'password123');
  await page.click('button:has-text("๋กœ๊ทธ์ธ")');
  
  // ๋กœ๊ทธ์ธ ์™„๋ฃŒ ๋Œ€๊ธฐ
  await page.waitForURL('/home');
  
  // ์ธ์ฆ ์ƒํƒœ ์ €์žฅ
  await page.context().storageState({ path: authFile });
});


์ €์žฅ๋œ ์ธ์ฆ ์ƒํƒœ ์‚ฌ์šฉ


// playwright.config.ts
export default defineConfig({
  projects: [
    {
      name: 'setup',
      testMatch: /.*\.setup\.ts/
    },
    {
      name: 'chromium',
      use: {
        ...devices['Desktop Chrome'],
        storageState: 'playwright/.auth/user.json',
      },
      dependencies: ['setup'],
    },
  ],
});


// ํ…Œ์ŠคํŠธ๋Š” ์ด๋ฏธ ๋กœ๊ทธ์ธ๋œ ์ƒํƒœ๋กœ ์‹œ์ž‘
test('ํ”„๋กœํ•„ ์ˆ˜์ •', async ({ page }) => {
  await page.goto('/profile'); // ์ด๋ฏธ ๋กœ๊ทธ์ธ๋จ
  // ...
});


Best Practices


๐Ÿท๏ธ 1. ํ…Œ์ŠคํŠธ๋Š” ๋…๋ฆฝ์ ์ด์–ด์•ผ ํ•จ


// โŒ Bad: ํ…Œ์ŠคํŠธ ๊ฐ„ ์˜์กด์„ฑ
test('์‚ฌ์šฉ์ž ์ƒ์„ฑ', async ({ page }) => {
  // user๋ฅผ ์ „์—ญ ๋ณ€์ˆ˜์— ์ €์žฅ
});

test('์‚ฌ์šฉ์ž ์ˆ˜์ •', async ({ page }) => {
  // ์ด์ „ ํ…Œ์ŠคํŠธ์˜ user ์‚ฌ์šฉ
});

// โœ… Good: ๊ฐ ํ…Œ์ŠคํŠธ๊ฐ€ ๋…๋ฆฝ์ 
test('์‚ฌ์šฉ์ž ์ˆ˜์ •', async ({ page }) => {
  // ํ…Œ์ŠคํŠธ ๋‚ด์—์„œ ์‚ฌ์šฉ์ž ์ƒ์„ฑ
  const user = await createTestUser();
  // ์ˆ˜์ • ํ…Œ์ŠคํŠธ
  // ์ •๋ฆฌ
  await deleteTestUser(user.id);
});


๐Ÿท๏ธ 2. ์•ˆ์ •์ ์ธ Locator ์‚ฌ์šฉ


// โŒ Bad: ๊นจ์ง€๊ธฐ ์‰ฌ์šด Locator
page.locator('div > div:nth-child(3) > button')
page.locator('button').nth(5)

// โœ… Good: ์˜๋ฏธ ์žˆ๋Š” Locator
page.getByRole('button', { name: '์ œ์ถœ' })
page.getByTestId('submit-button')
page.getByLabel('์‚ฌ์šฉ์ž ์ด๋ฆ„')


๐Ÿท๏ธ 3. ๋ช…์‹œ์ ์ธ ๋Œ€๊ธฐ ํ”ผํ•˜๊ธฐ


// โŒ Bad: ๊ณ ์ •๋œ ๋Œ€๊ธฐ ์‹œ๊ฐ„
await page.click('button');
await page.waitForTimeout(3000); // ํ•ญ์ƒ 3์ดˆ ๋Œ€๊ธฐ

// โœ… Good: ์ž๋™ ๋Œ€๊ธฐ ๋˜๋Š” ์กฐ๊ฑด ๋Œ€๊ธฐ
await page.click('button');
await page.waitForLoadState('networkidle');
await page.waitForSelector('.result');


๐Ÿท๏ธ 4. ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ๊ฒฉ๋ฆฌ


// โœ… Good: ๊ณ ์œ ํ•œ ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ
test('์‚ฌ์šฉ์ž ๋“ฑ๋ก', async ({ page }) => {
  const uniqueEmail = `test${Date.now()}@example.com`;
  
  await page.goto('/register');
  await page.fill('input[name="email"]', uniqueEmail);
  // ...
});


๐Ÿท๏ธ 5. ์Šคํฌ๋ฆฐ์ƒท๊ณผ ๋น„๋””์˜ค ํ™œ์šฉ


test('์ค‘์š”ํ•œ ๊ธฐ๋Šฅ', async ({ page }) => {
  await page.goto('/dashboard');
  
  // ํŠน์ • ์‹œ์  ์Šคํฌ๋ฆฐ์ƒท
  await page.screenshot({ path: 'dashboard.png' });
  
  // ํŠน์ • ์š”์†Œ๋งŒ ์Šคํฌ๋ฆฐ์ƒท
  await page.locator('.chart').screenshot({ path: 'chart.png' });
  
  // ์ „์ฒด ํŽ˜์ด์ง€ ์Šคํฌ๋ฆฐ์ƒท
  await page.screenshot({ path: 'full-page.png', fullPage: true });
});


์‹ค์ „ ์ฒดํฌ๋ฆฌ์ŠคํŠธ


โœ… ํ…Œ์ŠคํŠธ ์ž‘์„ฑ


  • ํ•ต์‹ฌ ์‚ฌ์šฉ์ž ํ๋ฆ„ ์ปค๋ฒ„
  • ํ–‰๋ณต ๊ฒฝ๋กœ + ์—๋Ÿฌ ์ผ€์ด์Šค
  • ๋ชจ๋ฐ”์ผ ๋ฐ˜์‘ํ˜• ํ…Œ์ŠคํŠธ
  • ํฌ๋กœ์Šค ๋ธŒ๋ผ์šฐ์ € ํ…Œ์ŠคํŠธ


โœ… ํ…Œ์ŠคํŠธ ์•ˆ์ •์„ฑ


  • ์•ˆ์ •์ ์ธ Locator ์‚ฌ์šฉ
  • ์ž๋™ ๋Œ€๊ธฐ ํ™œ์šฉ
  • ํ…Œ์ŠคํŠธ ๋…๋ฆฝ์„ฑ ๋ณด์žฅ
  • ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ๊ฒฉ๋ฆฌ


โœ… ์„ฑ๋Šฅ


  • ๋ณ‘๋ ฌ ์‹คํ–‰ ์„ค์ •
  • ์ธ์ฆ ์ƒํƒœ ์žฌ์‚ฌ์šฉ
  • ๋ถˆํ•„์š”ํ•œ ๋Œ€๊ธฐ ์ œ๊ฑฐ
  • API ๋ชจํ‚น ํ™œ์šฉ


โœ… ๋””๋ฒ„๊น…


  • ์‹คํŒจ ์‹œ ์Šคํฌ๋ฆฐ์ƒท
  • ์‹คํŒจ ์‹œ ๋น„๋””์˜ค ๋…นํ™”
  • Trace ํŒŒ์ผ ์ƒ์„ฑ
  • ๋ช…ํ™•ํ•œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€


โœ… CI/CD ํ†ตํ•ฉ


  • GitHub Actions ์„ค์ •
  • ์ž๋™ ์žฌ์‹œ๋„ ์„ค์ •
  • ๋ณ‘๋ ฌ ์‹คํ–‰ ์ตœ์ ํ™”
  • ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ ๋ฆฌํฌํŠธ


์š”์•ฝ


E2E ํ…Œ์ŠคํŠธ๋Š” ์‹ค์ œ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ๊ฒ€์ฆํ•˜๋Š” ํ•„์ˆ˜ ๋„๊ตฌ๋‹ค.

๐Ÿ’Ž ํ•ต์‹ฌ ํฌ์ธํŠธ:

  1. ํ…Œ์ŠคํŠธ ํ”ผ๋ผ๋ฏธ๋“œ: ๋‹จ์œ„ > ํ†ตํ•ฉ > E2E ์ˆœ์œผ๋กœ ๋งŽ์ด ์ž‘์„ฑ
  2. Playwright: ์ตœ์‹  ๊ธฐ์ˆ , ๋น ๋ฅด๊ณ  ์•ˆ์ •์ 
  3. ๋…๋ฆฝ์„ฑ: ๊ฐ ํ…Œ์ŠคํŠธ๋Š” ๋…๋ฆฝ์ ์œผ๋กœ ์‹คํ–‰ ๊ฐ€๋Šฅ
  4. ์•ˆ์ •์„ฑ: ์˜๋ฏธ ์žˆ๋Š” Locator, ์ž๋™ ๋Œ€๊ธฐ
  5. ์žฌ์‚ฌ์šฉ: Page Object Model ํ™œ์šฉ
  6. ํšจ์œจ์„ฑ: ์ธ์ฆ ์ƒํƒœ ์žฌ์‚ฌ์šฉ, API ๋ชจํ‚น


๐ŸŽฏ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ ์ˆœ์„œ:

  1. ํ•ต์‹ฌ ์‚ฌ์šฉ์ž ํ๋ฆ„ ํŒŒ์•…
  2. ํ–‰๋ณต ๊ฒฝ๋กœ ์‹œ๋‚˜๋ฆฌ์˜ค ์ž‘์„ฑ
  3. ์—๋Ÿฌ ์ผ€์ด์Šค ์ถ”๊ฐ€
  4. ์—ฃ์ง€ ์ผ€์ด์Šค ์ปค๋ฒ„


๐Ÿ“Œ Best Practices:

  • ๋ช…ํ™•ํ•œ ํ…Œ์ŠคํŠธ๋ช…: โ€œ์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์ธํ•˜๊ณ  ์ƒํ’ˆ์„ ๊ตฌ๋งคํ•œ๋‹คโ€
  • AAA ํŒจํ„ด: Arrange (์ค€๋น„) โ†’ Act (์‹คํ–‰) โ†’ Assert (๊ฒ€์ฆ)
  • ๋‹จ์ผ ์ฑ…์ž„: ํ•˜๋‚˜์˜ ํ…Œ์ŠคํŠธ๋Š” ํ•˜๋‚˜์˜ ์‹œ๋‚˜๋ฆฌ์˜ค๋งŒ
  • ๋…๋ฆฝ์„ฑ: ํ…Œ์ŠคํŠธ ์ˆœ์„œ์— ์˜์กดํ•˜์ง€ ์•Š์Œ
  • ์žฌํ˜„ ๊ฐ€๋Šฅ: ์–ธ์ œ ์‹คํ–‰ํ•ด๋„ ๊ฐ™์€ ๊ฒฐ๊ณผ


๐Ÿš€ Playwright vs Cypress:

ํ•ญ๋ชฉ Playwright Cypress
๋ธŒ๋ผ์šฐ์ € Chrome, Firefox, Safari Chrome, Edge
์†๋„ ๋น ๋ฆ„ ๋น ๋ฆ„
API ๋ชจํ‚น ๋‚ด์žฅ ๋‚ด์žฅ
๋””๋ฒ„๊น… Trace Viewer Time Travel
๋Ÿฌ๋‹์ปค๋ธŒ ๋‚ฎ์Œ ๋‚ฎ์Œ


E2E ํ…Œ์ŠคํŠธ๋Š” ์ฒ˜์Œ์—๋Š” ์ž‘์„ฑ ์‹œ๊ฐ„์ด ๊ฑธ๋ฆฌ์ง€๋งŒ,
ํ•œ ๋ฒˆ ์ž‘์„ฑํ•˜๋ฉด ์ˆ˜๋ฐฑ ๋ฒˆ ์žฌ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.
์ž๋™ํ™”๋œ ํ…Œ์ŠคํŠธ๋Š” ๋ฒ„๊ทธ๋ฅผ ์กฐ๊ธฐ์— ๋ฐœ๊ฒฌํ•˜๊ณ ,
์ž์‹ ๊ฐ ์žˆ๊ฒŒ ๋ฐฐํฌํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค€๋‹ค.