ν™ˆμœΌλ‘œ

Playwright λ„μž…ν•˜κΈ°


사내 ν”„λ‘œμ νŠΈμ— Playwrightλ₯Ό λ„μž…ν–ˆλ‹€. λΉ„λ‘œκ·ΈμΈ μ‚¬μš©μžκ°€ νœ΄λŒ€ν° 번호λ₯Ό λ³€κ²½ν•˜λ €λ©΄ λ¨Όμ € μ΄λ©”μΌλ‘œ 본인 인증을 ν•΄μ•Ό ν•˜λŠ” ν”Œλ‘œμš°κ°€ μžˆλ‹€. 여기에 이메일 λ°œμ†‘ 횟수 μ œν•œ(ν•˜λ£¨ μ΅œλŒ€ 10회) μš”κ΅¬μ‚¬ν•­μ΄ μΆ”κ°€λ˜μ—ˆλ‹€. 즉, 이메일 인증 μš”μ²­ API 호좜 μ‹œ 10회λ₯Ό μ΄ˆκ³Όν•˜λ©΄, μ‚¬μš©μžμ—κ²Œ μ œν•œ λͺ¨λ‹¬μ„ λ„μ›Œ μ•Œλ €μ•Ό ν•œλ‹€.

이메일 인증 μš”μ²­ APIκ°€ ν˜ΈμΆœλ˜λŠ” κ²½μš°λŠ” λ‹€μŒκ³Ό κ°™λ‹€.

ν…ŒμŠ€νŠΈ μ½”λ“œ μž‘μ„±μ˜ ν•„μš”μ„±

μœ„ λͺ¨λ“  μΌ€μ΄μŠ€μ—μ„œ μ‚¬μš©μžκ°€ μ‹€μ œλ‘œ 10회 μ œν•œ λͺ¨λ‹¬μ„ 보게 λ˜λŠ”μ§€ ν™•μΈν•˜λ €λ©΄, 맀번 APIλ₯Ό 10회 ν˜ΈμΆœν•˜κ³ , λ‹€λ₯Έ μΌ€μ΄μŠ€λ₯Ό μœ„ν•΄ λ°±μ—”λ“œ κ°œλ°œμžμ—κ²Œ μ œν•œ ν•΄μ œλ₯Ό μš”μ²­ν•œ λ’€ λ‹€μ‹œ 10회 ν˜ΈμΆœν•˜λŠ”β€¦ 이런 방식은 λ¦¬μ†ŒμŠ€ λ‚­λΉ„κ°€ 크고 λΉ„νš¨μœ¨μ μ΄λ‹€.

κ·Έλž˜μ„œ Playwright ν…ŒμŠ€νŠΈλ₯Ό μž‘μ„±ν•  λ•Œ, API μš”μ²­μ„ Mockingν•˜μ—¬ λ°”λ‘œ 10회 초과 응닡을 λ°˜ν™˜ν•˜λ„λ‘ μ²˜λ¦¬ν•˜κ³ , UI λ™μž‘λ§Œ κ²€μ¦ν•˜λ„λ‘ μ„€κ³„ν–ˆλ‹€.
λ˜ν•œ, @faker-js/faker λ₯Ό μ‚¬μš©ν•˜μ—¬ μ‹€μ œμ— κ°€κΉŒμš΄ ν…ŒμŠ€νŠΈ 데이터λ₯Ό ν™œμš©ν–ˆλ‹€.

λŒ€ν‘œ ν…ŒμŠ€νŠΈ μ½”λ“œ

μ•„λž˜λŠ” λŒ€ν‘œμ μΈ 두 κ°€μ§€ μΌ€μ΄μŠ€μ˜ μ˜ˆμ‹œμ΄λ‹€.

νšŒμ‚¬ λ‚΄λΆ€ API와 데이터λ₯Ό λ³΄ν˜Έν•˜κΈ° μœ„ν•΄ URLκ³Ό 응닡 등을 μΌλ°˜ν™”λœ μ˜ˆμ‹œ κ°’μœΌλ‘œ λŒ€μ²΄ν–ˆλ‹€.

// app/e2e/이메일_인증_초과.spec.ts

import { test, expect } from "@playwright/test";
import { faker } from "@faker-js/faker";

test("μΈμ¦ν•˜κΈ° λ²„νŠΌ 클릭 μ‹œ μš”μ²­ 10회λ₯Ό μ΄ˆκ³Όν•˜λ©΄ λͺ¨λ‹¬ ν‘œμ‹œ", async ({
  page,
}) => {
  // 이메일 쑴재 확인 API mocking
  await page.route(/\/api\/check/, (route) =>
    route.fulfill({
      status: 200,
      body: JSON.stringify({ isUser: true }),
    })
  );

  // 인증 μ½”λ“œ λ°œμ†‘ API mocking (10회 초과 응닡)
  // HTTP 429: Too Many Requests (ν•˜λ£¨ μ΅œλŒ€ μš”μ²­ 횟수 초과)
  await page.route(/\/api\/send/, (route) =>
    route.fulfill({
      status: 429,
      body: JSON.stringify({ code: "LIMIT_EXCEEDED" }),
    })
  );

  await page.goto("/email-verification");
  await page.locator('input[name="email"]').fill(faker.internet.email());

  await page.getByRole("button", { name: "μΈμ¦ν•˜κΈ°" }).click();

  // μ œν•œ λͺ¨λ‹¬ ν‘œμ‹œ 확인
  await expect(
    page.getByText("ν•˜λ£¨ μ΅œλŒ€ 10회 μš”μ²­μ΄ κ°€λŠ₯ν•©λ‹ˆλ‹€")
  ).toBeVisible();
});

test("μž¬μ „μ†‘ λ²„νŠΌ 클릭 μ‹œ μš”μ²­ 10회λ₯Ό μ΄ˆκ³Όν•˜λ©΄ λͺ¨λ‹¬ ν‘œμ‹œ", async ({ page }) => {
  // 이메일 쑴재 확인 API mocking
  await page.route(/\/api\/check/, (route) =>
    route.fulfill({
      status: 200,
      body: JSON.stringify({ isUser: true }),
    })
  );

  // 인증 μ½”λ“œ λ°œμ†‘ API mocking (첫 μš”μ²­μ€ μ„±κ³΅ν•΄μ„œ 인증 μ½”λ“œ μž…λ ₯ UI κ°€ 보여야 ν•œλ‹€)
  await page.route(/\/api\/send/, (route) =>
    route.fulfill({
      status: 200,
      body: JSON.stringify({ msg: "인증 μ½”λ“œ λ°œμ†‘ 성곡" }),
    })
  );

  await page.goto("/email-verification");
  await page.locator('input[name="email"]').fill(faker.internet.email());
  await page.getByRole("button", { name: "μΈμ¦ν•˜κΈ°" }).click();

  // μž¬μ „μ†‘ μ‹œ 10회 초과 응닡 mocking
  // κΈ°μ‘΄ Mock 제거 ν›„ μƒˆ Mock 적용 β†’ 동일 URL에 λ‹€λ₯Έ μ‹œλ‚˜λ¦¬μ˜€ ν…ŒμŠ€νŠΈ κ°€λŠ₯
  page.unroute(/\/api\/send/);
  page.route(/\/api\/send/, (route) =>
    route.fulfill({
      status: 429,
      body: JSON.stringify({ code: "LIMIT_EXCEEDED" }),
    })
  );

  await page.getByText("μž¬μ „μ†‘").click();

  // μ œν•œ λͺ¨λ‹¬ ν‘œμ‹œ 확인
  await expect(
    page.getByText("ν•˜λ£¨ μ΅œλŒ€ 10회 μš”μ²­μ΄ κ°€λŠ₯ν•©λ‹ˆλ‹€")
  ).toBeVisible();

  // 인증 μ½”λ“œ μž…λ ₯ 타이머 μ΄ˆκΈ°ν™” 확인
  expect(await page.locator('[data-test-id="timer"]').textContent()).toBe(
    "00:00"
  );
});

Playwrightμ—μ„œ API Mocking ν•˜κΈ°

μœ„ μ˜ˆμ‹œμ²˜λŸΌ Playwrightμ—μ„œλŠ” page.route()λ₯Ό μ΄μš©ν•΄ νŠΉμ • μš”μ²­μ„ κ°€λ‘œμ±„κ³ , μ›ν•˜λŠ” 응닡(Mock)을 λ°˜ν™˜ν•  수 μžˆλ‹€.
이λ₯Ό 톡해 μ‹€μ œ μ„œλ²„ 호좜 없이도 λ‹€μ–‘ν•œ μ‹œλ‚˜λ¦¬μ˜€λ₯Ό ν…ŒμŠ€νŠΈν•  수 μžˆλ‹€.

// route.fulfill(): μ‹€μ œ μ„œλ²„ 호좜 없이 μ§€μ •ν•œ Mock 응닡을 μ¦‰μ‹œ λ°˜ν™˜
await page.route(/\/api\/send/, async (route) => {
  await route.fulfill({
    status: 429,
    body: JSON.stringify({ code: "LIMIT_EXCEEDED" }),
  });
});

μœ„ μ½”λ“œλŠ” /api/code ν˜ΈμΆœμ‹œ μ„œλ²„κ°€ 10회 초과 응닡을 λ°˜ν™˜ν•˜λŠ” κ²ƒμ²˜λŸΌ λ™μž‘ν•˜κ²Œ ν•œλ‹€.
PlaywrightλŠ” μ •κ·œ ν‘œν˜„μ‹μ„ μ§€μ›ν•˜λ―€λ‘œ, νŠΉμ • νŒ¨ν„΄μ— λ§žλŠ” URL을 ν•œ λ²ˆμ— κ°€λ‘œμ±Œ 수 μžˆλ‹€.

route.fulfill()은 ν•΄λ‹Ή μš”μ²­μ„ μ¦‰μ‹œ Mock μ‘λ‹΅μœΌλ‘œ μ²˜λ¦¬ν•˜λ©°, μ‹€μ œ μ„œλ²„λ‘œ μš”μ²­μ„ 보내지 μ•ŠλŠ”λ‹€. 반면 route.fetch()λ₯Ό μ‚¬μš©ν•˜λ©΄ μ‹€μ œ μ„œλ²„λ‘œ μš”μ²­μ„ 보내고, 응닡을 λ³€ν˜•ν•΄μ„œ 전달할 수 μžˆλ‹€.

이번 ν…ŒμŠ€νŠΈμ—μ„œλŠ” λ‹€μŒ 이유둜 route.fulfill()을 μ‚¬μš©ν–ˆλ‹€:

  1. μ‹€μ œ μ„œλ²„ 호좜 λΆˆν•„μš”
  1. ν…ŒμŠ€νŠΈ 격리
  1. νš¨μœ¨μ„±

즉, 이번 e2e ν…ŒμŠ€νŠΈλŠ” UI λ™μž‘ 검증에 μ§‘μ€‘ν•˜κ³ , μ„œλ²„μ™€μ˜ μƒν˜Έμž‘μš©μ€ μ™„μ „νžˆ Mock으둜 λŒ€μ²΄ν–ˆκΈ° λ•Œλ¬Έμ— route.fulfill()이 κ°€μž₯ μ ν•©ν–ˆλ‹€.

μ°Έκ³ : route.fetch()

// route.fetch() μ‚¬μš© μ˜ˆμ‹œ
// μ‹€μ œ μ„œλ²„ μš”μ²­μ„ 보내고 응닡을 일뢀 μˆ˜μ •ν•΄μ„œ λ°˜ν™˜ κ°€λŠ₯
await page.route("**/api/data", async (route) => {
  // 1. μ›λž˜ μš”μ²­μ„ μ„œλ²„μ— κ·ΈλŒ€λ‘œ 보내고 응닡 λ°›κΈ°
  const response = await route.fetch();
  const data = await response.json();

  // 2. 응닡 데이터 일뢀 μˆ˜μ •
  const modifiedData = { ...data, modified: true };

  // 3. μˆ˜μ •ν•œ 데이터λ₯Ό ν΄λΌμ΄μ–ΈνŠΈμ— λ°˜ν™˜
  await route.fulfill({
    body: JSON.stringify(modifiedData),
  });
});

κΈ°μ‘΄ Mock을 μ œκ±°ν•˜κ³  μƒˆ Mock 적용: page.unroute()

λ™μΌν•œ URL에 λŒ€ν•΄ λ‹€λ₯Έ μ‹œλ‚˜λ¦¬μ˜€λ₯Ό ν…ŒμŠ€νŠΈν•  λ•ŒλŠ”, κΈ°μ‘΄ Mock을 μ œκ±°ν•˜κ³  μƒˆ Mock을 μ μš©ν•  수 μžˆλ‹€.

// 기쑴 Mock 제거
page.unroute(/\/api\/send/);

// μƒˆλ‘œμš΄ Mock 적용
page.route(/\/api\/send/, async (route) => {
  await route.fulfill({
    status: 200,
    body: JSON.stringify({ msg: "인증 μ½”λ“œ λ°œμ†‘ 성곡" }),
  });
});

page.unroute()λ₯Ό μ‚¬μš©ν•˜λ©΄ 이전 Mock이 더 이상 μ μš©λ˜μ§€ μ•ŠμœΌλ―€λ‘œ, μž¬μ „μ†‘ λ²„νŠΌ 클릭 μ‹œ λ‹€λ₯Έ μ‹œλ‚˜λ¦¬μ˜€λ₯Ό μ‰½κ²Œ ν…ŒμŠ€νŠΈν•  수 μžˆλ‹€.

μ΄λ ‡κ²Œ Mockκ³Ό unrouteλ₯Ό μ‘°ν•©ν•˜λ©΄, 단일 ν…ŒμŠ€νŠΈ λ‚΄μ—μ„œ λ‹€μ–‘ν•œ API 응닡 μΌ€μ΄μŠ€λ₯Ό 검증할 수 μžˆλ‹€. (μ΄ˆκΈ°μ—λŠ” μš”μ²­μ΄ μ„±κ³΅ν•˜κ³ , μ΄ν›„μ—λŠ” μ‹€νŒ¨ν•˜λŠ” μΌ€μ΄μŠ€ 검증 λ“±)

마치며

Playwrightλ₯Ό ν™œμš©ν•œ Mock 기반 e2e ν…ŒμŠ€νŠΈ 덕뢄에, λ°±μ—”λ“œμ— μ˜μ‘΄ν•˜μ§€ μ•Šκ³  ν”„λ‘ νŠΈμ—μ„œ λ…λ¦½μ μœΌλ‘œ UI λ™μž‘μ„ 검증할 수 μžˆμ—ˆλ‹€. 이둜 인해 개발 생산성이 크게 ν–₯μƒλ˜μ—ˆκ³ , νŒ€μ›λ“€μ—κ²Œλ„ e2e ν…ŒμŠ€νŠΈ λ„μž…μ„ μ†Œκ°œν•˜λŠ” 쒋은 κΈ°νšŒκ°€ λ˜μ—ˆλ‹€.

특히 QA μ§„ν–‰ κ³Όμ •μ—μ„œ κΈ°λŠ₯μ΄λ‚˜ UI κ΄€λ ¨ μ΄μŠˆκ°€ λ°œμƒν•˜μ§€ μ•Šμ•„, κΈ°λŠ₯의 μ•ˆμ •μ„±μ„ λ†’μ΄λŠ” 효과λ₯Ό 직접 확인할 수 μžˆμ—ˆλ‹€.