AbortController 사용 패턴
AbortController와 AbortSignal는 Phasis의 완전한 WHATWG 구현입니다. 표준 합성 도우미(AbortSignal.any, AbortSignal.timeout)도 제공됩니다. 이 페이지에서는 실제로 자주 사용하는 패턴과 PHP 경계를 넘어 취소가 전파되는 방식을 다룹니다.
fetch 취소
const ctrl = new AbortController();
const p = fetch(url, { signal: ctrl.signal });
ctrl.abort(); // p rejects with AbortError DOMException
기본적으로 이유는 AbortError이지만, 직접 지정할 수도 있습니다.
ctrl.abort(new Error('user cancelled'));
fetch는 전달한 값과 함께 거부됩니다.
시간 초과
const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
AbortSignal.timeout(ms)는 ms 후 자체적으로 중단되는 신호를 반환합니다. 타이머는 setTimeout / queueMicrotask를 구동하는 것과 동일한 엔진 이벤트 루프를 통해 실행됩니다(API: 이벤트 루프 참조). fetch 전송 계층은 전송 중 신호를 확인합니다. 기본 CurlTransport의 경우 CURLOPT_PROGRESSFUNCTION를 통해 확인하므로 기한에 도달하면 요청이 신속하게 중단됩니다.
이 루프는 기본적으로 가상 시간으로 동작합니다. 잠들지 않고 다음 대기 중인 기한으로 시계를 진행하므로 PHP, fetch(url, { signal: AbortSignal.timeout(5000) })은 기한 전에 완료되거나 다음 루프 틱에서 중단됩니다. PHP이 기다리는 일은 없습니다. 실시간 속도로 동작하려면 호스트가 벽시계에 맞춰 tickEventLoop()을 구동해야 합니다.
경쟁 상태: 먼저 취소된 작업이 우선
const user = new AbortController();
const signal = AbortSignal.any([user.signal, AbortSignal.timeout(10_000)]);
const res = await fetch(url, { signal });
// res rejects if EITHER the user clicks cancel OR 10s elapses
계층 간 전파
신호를 인식하는 루프로 자체 비동기 작업을 감싸세요:
async function fetchPaged(url, signal) {
const out = [];
for (let page = 1; ; page++) {
if (signal.aborted) throw signal.reason;
const r = await fetch(`${url}?page=${page}`, { signal });
const data = await r.json();
if (data.length === 0) break;
out.push(...data);
}
return out;
}
fetch는 신호를 직접 상속합니다. 수동 if (signal.aborted)은 요청 사이의 공백을 처리해 페이지 루프가 계속 실행되는 일을 막습니다.
중단 이벤트 수신
ctrl.signal.addEventListener('abort', () => {
releaseDatabaseLock();
cancelDownstreamWork();
});
리스너가 실행되기 전에 signal.reason이 설정됩니다.
사용자 지정 fetch 전송 계층의 신호
setFetchTransport()을 통해 전송 계층을 바꾸면 신호는 호출 가능한 함수에 JsObject로 전달됩니다. 다음과 같이 폴링해 신호를 처리하세요:
$engine->setFetchTransport(function (array $req, $signal) {
$ch = curl_init($req['url']);
// ...
if ($signal !== null) {
curl_setopt($ch, CURLOPT_PROGRESSFUNCTION,
fn () => $signal->get('aborted')->value ? 1 : 0);
curl_setopt($ch, CURLOPT_NOPROGRESS, false);
}
$body = curl_exec($ch);
if (curl_errno($ch) === CURLE_ABORTED_BY_CALLBACK) {
throw new \Phasis\BuiltIn\Fetch\TransportException(
'aborted', 'aborted',
);
}
// ...
});
kind: 'aborted'와 함께 TransportException을 던지면 JS 측의 fetch이 AbortError DOMException로 거부됩니다. 사양의 중단 루프가 실행된 경우와 같습니다.
네이티브 취소를 지원하는 백엔드(Guzzle의 on_progress, Symfony의 ResponseInterface::cancel)에는 동일한 폴링 후 예외 발생 방식을 연결하세요.
아직 작동하지 않는 패턴
signal.addEventListener('abort', () => fetch(cleanupUrl))— 리스너는 동기적으로 실행됩니다. 정리용 fetch는 시작되지만 중단 이벤트가 전달되는 동안 완료되지 않습니다. 주변의await을 통해 예약하세요.
setTimeout(() => ctrl.abort(), N)은 작동합니다(타이머 계열이 제공되며 WPT 테스트를 통과합니다). 하지만 기한이 지나면 중단 신호를 보내려는 경우에는 AbortSignal.timeout(N)이 더 짧고 의도가 잘 드러납니다.
관련 항목
- Fetch 전송 계층 — 사용자 지정 전송 계층을 위한 전체 요청/응답/신호 인터페이스.
- 웹 API(WPT) — AbortController/AbortSignal의 WPT 통과율.