네트워크 제어와 모킹

네트워크 제어와 모킹

Playwright는 브라우저가 주고받는 HTTP·HTTPS 네트워크 트래픽을 감시하고 수정할 수 있는 API를 제공해요. 페이지가 하는 요청(XHR, fetch 포함)은 모두 추적하고 바꾸고 처리할 수 있답니다. 요청을 막는 것부터 응답을 바꾸는 것까지, 네트워크를 내 마음대로 다루는 방법을 살펴볼게요.

출처: Playwright 공식 문서 — Network

본문

API 모킹

API 모킹에 대한 자세한 내용은 별도 가이드에서 다루는데, 크게 세 가지가 가능해요.

  • API 요청을 모킹해서 실제 API를 호출하지 않기
  • API 요청을 수행하고 응답을 수정하기
  • HAR 파일로 네트워크 요청 모킹하기

네트워크 모킹

네트워크 모킹에 특별한 설정은 필요 없어요. 브라우저 컨텍스트의 네트워크를 모킹하는 커스텀 [Route]만 정의하면 돼요.

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

test.beforeEach(async ({ context }) => {
  // Block any css requests for each test in this file.
  await context.route(/.css$/, route => route.abort());
});

test('loads page without css', async ({ page }) => {
  await page.goto('https://playwright.dev');
  // ... test goes here
});

단일 페이지에서만 모킹하려면 [method: Page.route]를 쓸 수 있어요.

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

test('loads page without images', async ({ page }) => {
  // Block png and jpeg images.
  await page.route(/(png|jpeg)$/, route => route.abort());

  await page.goto('https://playwright.dev');
  // ... test goes here
});

HTTP 인증

HTTP 인증을 수행하려면 컨텍스트에 자격 증명을 지정해요.

import { defineConfig } from '@playwright/test';
export default defineConfig({
  use: {
    httpCredentials: {
      username: 'bill',
      password: 'pa55w0rd',
    }
  }
});
const context = await browser.newContext({
  httpCredentials: {
    username: 'bill',
    password: 'pa55w0rd',
  },
});
const page = await context.newPage();
await page.goto('https://example.com');

HTTP 프록시

페이지가 HTTP(S) 프록시나 SOCKSv5를 통해 로드되게 설정할 수 있어요. 프록시는 브라우저 전체에 전역으로, 또는 컨텍스트별로 각각 설정할 수 있어요. HTTP(S) 프록시에는 사용자 이름·비밀번호를 지정할 수 있고, 프록시를 우회할 호스트도 지정할 수 있어요. 전역 프록시 예시를 볼게요.

import { defineConfig } from '@playwright/test';
export default defineConfig({
  use: {
    proxy: {
      server: 'http://myproxy.com:3128',
      username: 'usr',
      password: 'pwd'
    }
  }
});

컨텍스트별로도 지정할 수 있어요.

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

test('should use custom proxy on a new context', async ({ browser }) => {
  const context = await browser.newContext({
    proxy: {
      server: 'http://myproxy.com:3128',
    }
  });
  const page = await context.newPage();

  await context.close();
});

네트워크 이벤트

모든 [Request]와 [Response]를 감시할 수 있어요.

// Subscribe to 'request' and 'response' events.
page.on('request', request => console.log('>>', request.method(), request.url()));
page.on('response', response => console.log('<<', response.status(), response.url()));

await page.goto('https://example.com');

버튼 클릭 후 네트워크 응답을 기다리려면 [method: Page.waitForResponse]를 써요.

// Use a glob URL pattern. Note no await.
const responsePromise = page.waitForResponse('**/api/fetch_data');
await page.getByText('Update').click();
const response = await responsePromise;

변형으로는 RegExp나 predicate를 이용할 수 있어요.

// Use a RegExp. Note no await.
const responsePromise = page.waitForResponse(/\.jpeg$/);
await page.getByText('Update').click();
const response = await responsePromise;

// Use a predicate taking a Response object. Note no await.
const responsePromise = page.waitForResponse(response => response.url().includes(token));
await page.getByText('Update').click();
const response = await responsePromise;

다만 waitForResponse에서는 await 없이 프로미스를 만들고, 액션 후에 다시 await한다는 점에 주의하세요. 그래야 레이스가 일어나지 않아요.

요청 처리하기 (handle requests)

요청을 가로채 원하는 응답으로 대체할 수 있어요.

await page.route('**/api/fetch_data', route => route.fulfill({
  status: 200,
  body: testData,
}));
await page.goto('https://example.com');

이렇게 네트워크 요청을 처리해 API 엔드포인트를 모킹할 수 있어요. 브라우저 컨텍스트 전체에 [method: BrowserContext.route]를 걸면 팝업 창과 열린 링크에도 적용돼요.

await browserContext.route('**/api/login', route => route.fulfill({
  status: 200,
  body: 'accept',
}));
await page.goto('https://example.com');

요청 수정하기

요청을 수정해서 계속 보낼 수 있어요. 아래 예시는 나가는 요청에서 HTTP 헤더를 제거해요.

// Delete header
await page.route('**/*', async route => {
  const headers = route.request().headers();
  delete headers['x-secret'];
  await route.continue({ headers });
});

// Continue requests as POST.
await page.route('**/*', route => route.continue({ method: 'POST' }));

요청 중단하기

[method: Page.route]와 [method: Route.abort]로 요청을 중단할 수 있어요.

await page.route('**/*.{png,jpg,jpeg}', route => route.abort());

// Abort based on the request type
await page.route('**/*', route => {
  return route.request().resourceType() === 'image' ? route.abort() : route.continue();
});

응답 수정하기

응답을 수정하려면 [APIRequestContext]로 원본 응답을 가져온 뒤 [method: Route.fulfill]에 넘기면 돼요. 옵션으로 개별 필드를 덮어쓸 수 있어요.

await page.route('**/title.html', async route => {
  // Fetch original response.
  const response = await route.fetch();
  // Add a prefix to the title.
  let body = await response.text();
  body = body.replace('<title>', '<title>My prefix:');
  await route.fulfill({
    // Pass all fields from the response.
    response,
    // Override response body.
    body,
    // Force content type to be html.
    headers: {
      ...response.headers(),
      'content-type': 'text/html'
    }
  });
});

Glob URL 패턴

Playwright는 [method: Page.route]나 [method: Page.waitForResponse] 같은 네트워크 가로채기 메서드의 URL 매칭에 단순화된 glob 패턴을 사용해요. 기본 와일드카드 규칙을 정리하면,

  1. * 한 개는 /를 제외한 모든 문자와 매칭
  2. ** 두 개는 /를 포함한 모든 문자와 매칭
  3. ?는 오직 물음표 ? 하나와만 매칭 (?*를 쓸 것)
  4. 중괄호 {}는 쉼표로 구분된 옵션 목록과 매칭
  5. 백슬래시 \는 특수 문자를 이스케이프 (백슬래시 자신은 \\로)

예시:

  • https://example.com/*.jshttps://example.com/file.js는 매칭, https://example.com/path/file.js는 안 됨
  • https://example.com/?page=1https://example.com/?page=1은 매칭, https://example.com은 안 됨
  • **/*.js → 둘 다 매칭
  • **/*.{png,jpg,jpeg} → 모든 이미지 요청 매칭

중요한 점: glob 패턴은 URL 전체와 매칭해야 해요(일부가 아님). 더 복잡한 매칭이 필요하면 glob 대신 [RegExp]를 고려하세요.

WebSockets

Playwright는 WebSockets 검사·모킹·수정을 기본 지원해요. WebSocket이 만들어질 때마다 [event: Page.webSocket] 이벤트가 발생해요.

page.on('websocket', ws => {
  console.log(`WebSocket opened: ${ws.url()}>`);
  ws.on('framesent', event => console.log(event.payload));
  ws.on('framereceived', event => console.log(event.payload));
  ws.on('close', () => console.log('WebSocket closed'));
});

네트워크 이벤트 누락과 Service Workers

기본 제공하는 [method: BrowserContext.route]·[method: Page.route]로 네트워크 이벤트가 안 잡히는 것처럼 보인다면, Service Worker를 'block'으로 설정해 끌 수 있어요.

// in config use: serviceWorkers: 'block'

또한 MSW(Mock Service Worker) 같은 모킹 도구를 쓰면 자체 Service Worker가 네트워크 요청을 가로채서 Playwright의 route에 안 보일 수 있어요. 네트워크 테스트와 모킹을 함께 원한다면 내장 route를 쓰는 걸 고려해 보세요. Service Worker를 단순히 테스트 도구로만이 아니라, Service Worker가 보내는 요청을 라우팅·수신까지 다루고 싶다면 별도 가이드를 참고하세요.

더 알아보기