리소스 누수 방지: Playwright 테스트에서 AbortSignal 사용 방법
Source: Dev.to
위 링크에 있는 전체 글 내용을 제공해 주시면, 해당 내용을 한국어로 번역해 드리겠습니다. (코드 블록, URL 및 기술 용어는 그대로 유지됩니다.)
문제
test('should fetch user profile', async () => {
const response = await fetch('https://api.example.com/users/123');
const user = await response.json();
expect(user.name).toBe('Alice');
});
- 테스트가 타임아웃될 경우(느린 API, 서버 부하 등), Playwright는 테스트를 중단하지만
fetch호출은 **완료되거나 자체 타임아웃(보통 30 초 이상)**에 도달할 때까지 계속 실행됩니다.
규모가 큰 경우 왜 중요한가
- 연결 풀 고갈 – 워커가 소켓을 다 사용하게 됩니다.
- 서버 과부하 – 실제로 기다리는 사람이 없는 요청까지 스테이징 환경에서 처리됩니다.
- 오해를 일으키는 로그 – 고아가 된 요청이 서버 로그에 오류로 나타납니다.
- 연쇄적인 실패 – 하나의 서비스가 고갈되면 전체 테스트 스위트가 중단될 수 있습니다.
afterEach가 도움이 안 되는 이유
afterEach가 실행될 때는 이미 진행 중인 요청에 대한 참조가 사라져 있습니다. 타임아웃된 테스트 함수 안에 프로미스가 잡혀 있기 때문에, 접근하지 못하는 요청을 취소할 수 없습니다.
해결책: 모든 비동기 작업에 취소 토큰(AbortSignal)을 미리 전달하고, 테스트가 끝날 때 토큰을 트리거합니다.
AbortController & AbortSignal
AbortController (브라우저와 Node.js에서 표준으로 제공됨) 은 비동기 작업을 취소하는 데 사용할 수 있는 시그널을 생성합니다:
const controller = new AbortController();
const signal = controller.signal;
// Pass the signal to fetch
await fetch('/api/data', { signal });
// Cancel later
controller.abort(); // fetch rejects with an AbortError
- Pass the signal to fetch → fetch에 시그널을 전달합니다
- Cancel later → 나중에 취소
- fetch rejects with an AbortError → fetch가 AbortError와 함께 거부됩니다
@playwright-labs/fixture-abort 패키지
설치
npm install @playwright-labs/fixture-abort
패키지가 제공하는 내용
| 픽스처 | 설명 |
|---|---|
abortController | 각 테스트마다 새로운 AbortController 인스턴스. |
signal | 연관된 AbortSignal. |
useAbortController(options?) | 컨트롤러를 반환합니다; onAbort 콜백을 제공할 수 있습니다. |
useSignalWithTimeout(ms) | ms 밀리초 후 자동으로 중단되는 신호를 반환합니다. |
Note:
@playwright/test대신 패키지에서test와expect를 가져와야 픽스처가 자동으로 적용됩니다.
import { test, expect } from '@playwright-labs/fixture-abort';
사용 예시
1️⃣ 자동 취소가 포함된 간단한 fetch
import { test, expect } from '@playwright-labs/fixture-abort';
test('should fetch user profile', async ({ signal }) => {
const response = await fetch('https://api.example.com/users/123', { signal });
const user = await response.json();
expect(user.name).toBe('Alice');
});
테스트가 시간 초과되면 signal이 발생하고 요청이 즉시 취소됩니다.
2️⃣ 조건이 만족될 때까지 폴링
import { test, expect } from '@playwright-labs/fixture-abort';
test('should wait for order processing', async ({ signal }) => {
const orderId = await createOrder();
while (!signal.aborted) {
const response = await fetch(`/api/orders/${orderId}`, { signal });
const order = await response.json();
if (order.status === 'completed') {
expect(order.total).toBeGreaterThan(0);
return;
}
// 다음 폴링 전 2 초 대기
await new Promise(resolve => setTimeout(resolve, 2000));
}
});
while (!signal.aborted) 가드가 테스트가 중단될 때 루프가 깔끔하게 종료되도록 보장합니다.
3️⃣ 병렬 요청 – 하나의 signal, 여러 호출
import { test, expect } from '@playwright-labs/fixture-abort';
test('should fetch dashboard data', async ({ signal }) => {
const [users, orders, metrics] = await Promise.all([
fetch('/api/users', { signal }),
fetch('/api/orders', { signal }),
fetch('/api/metrics', { signal })
]);
expect(users.ok).toBe(true);
expect(orders.ok).toBe(true);
expect(metrics.ok).toBe(true);
});
테스트가 중단되면 세 요청이 모두 동시에 취소됩니다.
4️⃣ 테스트 로직에 기반한 수동 abort
import { test, expect } from '@playwright-labs/fixture-abort';
test('should stop on first error', async ({ signal, abortController }) => {
const items = await getItemsToProcess();
for (const item of items) {
if (signal.aborted) break;
const response = await fetch(`/api/process/${item.id}`, {
method: 'POST',
signal
});
if (!response.ok) {
abortController.abort(); // 남은 작업 취소
break;
}
}
});
테스트 본문 내부에서 전체 테스트를 중단할 수 있습니다.
5️⃣ useAbortController – abort 시 콜백 등록
import { test, expect } from '@playwright-labs/fixture-abort';
test('should handle abort with cleanup', async ({ useAbortController, signal }) => {
const controller = useAbortController({
onAbort: () => console.log('Operation cancelled, cleaning up'),
abortTest: true // 선택 사항 – 테스트 자체를 중단
});
const response = await fetch('/api/long-operation', { signal });
const data = await response.json();
expect(data).toBeDefined();
});
onAbort 훅은 signal이 트리거될 때 자동으로 실행됩니다.
6️⃣ useSignalWithTimeout – 고정 기간 후 자동 abort
import { test, expect } from '@playwright-labs/fixture-abort';
test('should auto‑abort after 10 seconds', async ({ useSignalWithTimeout }) => {
const signal = useSignalWithTimeout(10_000); // 10 s
const response = await fetch('/api/slow-endpoint', { signal });
const data = await response.json();
expect(data).toBeDefined();
});
signal은 지정된 타임아웃 후 자동으로 abort되어, 테스트 자체가 시간 초과되지 않더라도 장기 요청으로부터 보호합니다.
TL;DR
- 문제: Playwright 테스트에서 오래 실행되는 비동기 작업이 테스트 타임아웃 후에도 계속 실행되어 리소스가 누수됩니다.
- 해결책: 모든 비동기 작업에
AbortSignal을 전달하고 테스트가 끝날 때 이를 중단(abort)합니다. - 도구:
@playwright-labs/fixture-abort는 준비된 픽스처(signal,abortController,useAbortController,useSignalWithTimeout)를 제공합니다. - 결과: 고아 HTTP 요청이 없고, 연결 풀 고갈도 없으며, CI 실행이 더 깔끔해집니다.
행복한 테스트 되세요! 🚀
# Abort Fixtures for Playwright
```ts
import { test, expect } from '@playwright-labs/fixture-abort';
test('should complete within 5 seconds', async ({ useSignalWithTimeout }) => {
const timeoutSignal = useSignalWithTimeout(5000);
const response = await fetch('/api/slow-endpoint', {
signal: timeoutSignal,
});
expect(response.ok).toBe(true);
});
많은 최신 라이브러리가 AbortSignal을 지원합니다. 해당 신호를 지원하는 어디에든 전달할 수 있습니다:
import { test, expect } from '@playwright-labs/fixture-abort';
test('should query database', async ({ signal }) => {
// 많은 DB 클라이언트가 abort signal을 받습니다
const result = await db.query('SELECT * FROM users', {
signal,
});
expect(result.rows.length).toBeGreaterThan(0);
});
다음과 함께 사용할 수 있습니다:
- Axios (
signal옵션을 통해) - Node.js
fetch구현 - 다양한 데이터베이스 드라이버
- gRPC 클라이언트
- …그 외 여러 경우
이 패키지는 abort 상태를 테스트하기 위한 커스텀 expect 매처도 제공합니다:
import { test, expect } from '@playwright-labs/fixture-abort';
test('should verify abort state', async ({ signal, abortController }) => {
expect(signal).toBeActive();
abortController.abort('test reason');
expect(signal).toBeAborted();
expect(signal).toBeAbortedWithReason('test reason');
expect(abortController).toHaveAbortedSignal();
});
test('should verify timeout signal aborts', async ({ useSignalWithTimeout }) => {
const timeoutSignal = useSignalWithTimeout(100);
await expect(timeoutSignal).toAbortWithin(150);
});
How It Works
- 각 테스트 실행 전 – Playwright 픽스처 시스템을 통해 새로운
AbortController가 생성됩니다. - 컨트롤러와 그
signal은 각각abortController와signal픽스처로 노출됩니다. - 테스트가 타임아웃될 때, 컨트롤러가 자동으로 abort됩니다.
- 신호를 듣고 있던 모든 작업은
AbortError를 받아 중단됩니다. - 각 테스트가 끝난 뒤, 컨트롤러가 정리됩니다.
이는 각 테스트마다 독립된 취소 범위가 제공된다는 의미이며, 한 테스트가 타임아웃돼도 다른 테스트에 영향을 주지 않습니다.
Best Practices
- 픽스처가 이미
AbortController를 제공하고 있다면 직접AbortController를 만들지 마세요. 픽스처가 제공하는 컨트롤러는 테스트 라이프사이클에 연결되어 있어 타임아웃 시 자동으로 abort됩니다. - 항상 신호를 비동기 작업에 전달하세요 (
fetch, DB 쿼리 등). 신호 없이 호출하면 좀비 요청 문제가 발생할 수 있습니다. AbortError를 조용히 잡아먹지 마세요. 신호가 발생하면 작업은AbortError로 거부됩니다. 오류를 숨기지 말고 Playwright가 타임아웃을 보고하도록 두세요.
Installation
npm install @playwright-labs/fixture-abort
전체 소스 코드와 문서:
이 패키지는 @playwright-labs 모노레포의 일부입니다. @playwright/test 대신 @playwright-labs/fixture-abort에서 test와 expect를 임포트하면 abort 픽스처를 모든 테스트에서 바로 사용할 수 있습니다.
한 번 사용해 보세요—스테이징 서버가 고마워할 겁니다!