네트워크 제어와 모킹
네트워크 제어와 모킹
Playwright는 브라우저가 주고받는 HTTP·HTTPS 네트워크 트래픽을 감시하고 수정할 수 있는 API를 제공해요. 페이지가 하는 요청(XHR, fetch 포함)은 모두 추적하고 바꾸고 처리할 수 있답니다. 요청을 막는 것부터 응답을 바꾸는 것까지, 네트워크를 내 마음대로 다루는 방법을 살펴볼게요.
본문
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 패턴을 사용해요. 기본 와일드카드 규칙을 정리하면,
*한 개는/를 제외한 모든 문자와 매칭**두 개는/를 포함한 모든 문자와 매칭?는 오직 물음표?하나와만 매칭 (?는*를 쓸 것)- 중괄호
{}는 쉼표로 구분된 옵션 목록과 매칭 - 백슬래시
\는 특수 문자를 이스케이프 (백슬래시 자신은\\로)
예시:
https://example.com/*.js→https://example.com/file.js는 매칭,https://example.com/path/file.js는 안 됨https://example.com/?page=1→https://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가 보내는 요청을 라우팅·수신까지 다루고 싶다면 별도 가이드를 참고하세요.