Contents
see List요청이 끝났다고 서버 작업도 끝난 것은 아니다
Node.js API는 클라이언트가 브라우저를 닫거나 화면을 전환해 연결을 끊어도, 이미 시작한 외부 API 호출·파일 처리·대기 작업을 계속 수행하는 경우가 많습니다. 단순한 조회라면 큰 문제가 없어 보일 수 있지만, 트래픽이 쌓이면 끊긴 요청을 위한 연결과 CPU 시간이 남아 응답 지연, 외부 API 비용, 커넥션 풀 고갈로 이어집니다. 특히 검색, 결제 전 단계 검증, 대용량 데이터 변환처럼 여러 비동기 작업을 묶는 API는 취소 신호를 작업 전체에 전달하는 구조가 필요합니다.
이때 표준 Web API인 AbortController와 AbortSignal을 사용하면 타임아웃과 클라이언트 연결 종료를 하나의 취소 신호로 합칠 수 있습니다. Node.js의 fetch, timers/promises, 일부 데이터베이스·SDK API는 signal 옵션을 받습니다. 지원하지 않는 라이브러리도 signal 이벤트를 구독해 자신이 시작한 타이머, 스트림, 작업 큐를 정리하도록 만들 수 있습니다.
취소 정책을 먼저 정한다
취소는 오류 처리와 같지 않습니다. 사용자가 연결을 끊은 경우에는 서버 로그를 경고로 쌓거나 500 오류로 집계하지 않는 편이 좋습니다. 반면 서버가 정한 시간 제한을 넘긴 경우에는 의존 서비스 지연을 알려 주는 운영 지표가 되어야 합니다. 따라서 컨트롤러마다 취소 사유를 만들고, 호출 계층에는 AbortSignal만 전달하는 방식이 안전합니다. 하위 함수가 HTTP 프레임워크 객체를 직접 알 필요도 없습니다.
- 클라이언트 연결 종료: 더 이상 결과가 필요 없으므로 가능한 즉시 작업을 중단합니다.
- API 시간 제한: 외부 의존성의 무한 대기를 막고 504 또는 서비스 정책에 맞는 응답으로 변환합니다.
- 서버 종료: SIGTERM 수신 뒤 새 요청을 막고, 진행 중 작업에는 별도 종료 신호를 전달합니다.
- 업무 작업: 이미 외부에 변경을 만들었다면 단순 취소 대신 멱등 키, 보상 처리, 상태 조회가 필요합니다.
Express에서 요청 취소와 시간 제한을 합치는 예제
아래 코드는 Node.js 20 이상을 기준으로 합니다. 요청이 중단되면 req의 aborted 이벤트가 발생하고, AbortSignal.any로 이를 시간 제한 신호와 결합합니다. 외부 호출에는 반드시 signal을 넘기며, finally에서 리스너를 제거해 장기 실행 프로세스의 누적 등록을 막습니다.
import express from 'express';
const app = express();
app.get('/api/catalog/:id', async (req, res, next) => {
const clientController = new AbortController();
const timeoutSignal = AbortSignal.timeout(4_000);
const signal = AbortSignal.any([clientController.signal, timeoutSignal]);
const onAborted = () => clientController.abort(new Error('client disconnected'));
req.once('aborted', onAborted);
try {
const response = await fetch(`https://catalog.internal/items/${encodeURIComponent(req.params.id)}`, { signal });
if (!response.ok) throw new Error(`catalog status: ${response.status}`);
res.json(await response.json());
} catch (error) {
if (signal.aborted) {
if (!res.headersSent && timeoutSignal.aborted) return res.status(504).json({ code: 'UPSTREAM_TIMEOUT' });
return;
}
next(error);
} finally {
req.removeListener('aborted', onAborted);
}
});실제 서비스에서는 upstream URL을 사용자 입력으로 만들지 말고 허용된 서비스 주소만 사용해야 합니다. encodeURIComponent는 경로 파라미터를 안전하게 만들지만, SSRF 방어를 대신하지 않습니다. 또한 AbortSignal.timeout은 요청 단위 제한일 뿐입니다. 서비스 전체의 동시 요청 수, 외부 API별 연결 제한, 재시도 예산도 별도로 설계해야 합니다.
하위 함수까지 signal을 전달하는 방식
취소 처리가 컨트롤러에만 있으면 이후 추가되는 함수가 신호를 놓치기 쉽습니다. 함수 옵션에 signal을 명시적으로 둬 호출 경로 전체에서 전달하세요. 여러 작업을 Promise.all로 병렬 실행할 때 하나가 실패했다고 나머지 네트워크 요청이 자동으로 멈추지는 않습니다. 공통 컨트롤러를 만들고 치명적 실패 시 abort하는 방식이 필요할 수 있습니다.
import { setTimeout as sleep } from 'node:timers/promises';
export async function loadSummary(userId, { signal }) {
await sleep(100, undefined, { signal });
const response = await fetch(`https://profile.internal/users/${userId}`, { signal });
if (!response.ok) throw new Error('profile request failed');
return response.json();
}
const summary = await loadSummary(userId, { signal });데이터베이스 드라이버의 취소 지원은 제품과 버전에 따라 다릅니다. signal을 지원하지 않는 경우에는 쿼리 시간 제한을 데이터베이스 쪽에도 설정해야 합니다. 예를 들어 PostgreSQL은 statement_timeout을 적용해 오래 걸리는 SQL이 서버 안에서 계속 실행되는 것을 막을 수 있습니다. HTTP 취소만 넣고 DB 시간 제한을 생략하면 연결이 끊긴 뒤에도 무거운 쿼리가 계속 실행될 수 있습니다.
재시도와 취소를 함께 다룰 때 주의할 점
재시도 루프는 매 시도 전과 대기 중에 signal.aborted를 확인해야 합니다. 취소된 요청을 재시도하면 불필요한 부하가 늘고, 이미 사용자가 떠난 화면에 대한 외부 호출이 계속 발생합니다. POST처럼 상태를 바꾸는 요청은 네트워크 오류 뒤 결과를 알 수 없으므로 무조건 재시도하면 중복 생성 위험이 있습니다. 이 경우 멱등 키를 보내고 같은 키의 처리 상태를 조회할 수 있게 만드는 편이 낫습니다.
관측성도 함께 구성해야 합니다. 전체 오류율에 AbortError를 섞으면 장애 판단이 왜곡됩니다. 취소 사유별 카운터, 외부 호출 시간, 시간 제한 비율을 분리해 기록하면 사용자의 화면 전환이 많은 것인지 특정 의존성이 느린 것인지 구분할 수 있습니다. 로그에는 원문 인증 헤더나 요청 본문 대신 요청 ID와 안전한 오류 코드만 남깁니다.
적용 체크리스트
- 각 HTTP 요청에 명확한 시간 제한과 AbortSignal을 만들었는지 확인합니다.
- 외부 HTTP, 대기 타이머, 스트림, SDK 호출에 signal 또는 동등한 취소 수단을 전달합니다.
- 클라이언트 연결 종료와 서버 시간 제한을 다른 운영 지표로 기록합니다.
- 데이터베이스에는 쿼리 시간 제한과 커넥션 풀 제한을 함께 적용합니다.
- 상태 변경 요청은 취소만 믿지 않고 멱등 키와 결과 조회 경로를 설계합니다.
javascript
| No | 작성일 | Title |
|---|---|---|
| 2921 | 2026. 05. 31. | 브라우저 중복 탭 작업 제어: BroadcastChannel과 Web Locks로 자동 저장 충돌 막기 |
| 2866 | 2026. 05. 23. | Node.js 운영 추적 로그 설계: AsyncLocalStorage로 요청 ID와 장애 원인 연결하기 |
| 2787 | 2026. 05. 15. | Node.js fetch 타임아웃·재시도 표준 패턴: AbortSignal로 장애 전파 막기 |
| 2589 | 2026. 04. 23. | ES2026 JavaScript 신기능 완전 가이드: using/await using, Array.fromAsync, Error.isError, Math.sumPrecise 실전 적용 |
| 2490 | 2026. 04. 14. | ES2025/ES2026 완벽 정리: Iterator Helpers, Temporal API, using 키워드 실전 가이드 |
| 2469 | 2026. 04. 13. | ES2026 완벽 가이드: Temporal API, using/await using, Array/Set 신기능 총정리 |
| 2449 | 2026. 04. 12. | ES2025/ES2026 JavaScript 신기능 완벽 정리: Temporal API부터 using 키워드까지 |
| 2425 | 2026. 04. 11. | Node.js 24 LTS 핵심 신기능 완벽 가이드 - V8 13.6, Permission Model, URLPattern |
| 2389 | 2026. 04. 09. | ES2025 핵심 신규 기능 총정리 - Set 연산, Iterator Helpers, Temporal API까지 |
| 2368 | 2026. 04. 08. | ES2025/ES2026 완벽 정리: Temporal API, Resource Management, 새 문법 총정리 |