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()을 사용했다:
- 실제 서버 호출 불필요
- UI 검증과 10회 초과 시나리오 테스트가 목적이므로, 서버 요청 없이 Mock 응답만으로 충분하다.
- 테스트 격리
- 백엔드 상태나 데이터와 무관하게 항상 동일한 조건에서 테스트 가능
- 효율성
- 매번 실제 서버 호출 없이도 다양한 시나리오(성공, 제한 초과)를 즉시 테스트할 수 있다.
즉, 이번 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 관련 이슈가 발생하지 않아, 기능의 안정성을 높이는 효과를 직접 확인할 수 있었다.