Node.js AbortController fetch 타임아웃 처리
Node.js에서 AbortController를 사용하여 fetch 요청의 타임아웃을 처리하는 방법을 다룹니다.
Introduction
Node.js에서 외부 API 호출 시 타임아웃을 설정하는 것은 중요합니다. AbortController와 AbortSignal을 사용하면 fetch 요청의 타임아웃을 효과적으로 관리할 수 있습니다.
Environment
node --version
# v20.11.0
# AbortController는 Node.js 15+에서 네이티브 지원
node -e "console.log(typeof AbortController)"
# functionProblem
API 호출 시 타임아웃이 설정되어 있지 않아 무한 대기 상태가 발생했습니다:
// 타임아웃 미설정
async function fetchData(url) {
const response = await fetch(url);
return response.json();
}
// 응답이 없으면 무한 대기
const data = await fetchData('https://api.slow-service.com/data');Solution
1. 기본 AbortController 사용
async function fetchWithTimeout(url, timeoutMs = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, {
signal: controller.signal,
});
clearTimeout(timeoutId);
return response;
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error(`Request timeout after ${timeoutMs}ms`);
}
throw error;
}
}
// 사용 예시
try {
const response = await fetchWithTimeout('https://api.example.com/data', 3000);
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Fetch failed:', error.message);
}2. 재시도 로직과 타임아웃
class FetchClient {
constructor(defaultTimeout = 5000, maxRetries = 3) {
this.defaultTimeout = defaultTimeout;
this.maxRetries = maxRetries;
}
async fetch(url, options = {}) {
const {
timeout = this.defaultTimeout,
retries = this.maxRetries,
retryDelay = 1000,
...fetchOptions
} = options;
let lastError;
for (let attempt = 0; attempt <= retries; attempt++) {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
const response = await fetch(url, {
...fetchOptions,
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response;
} catch (error) {
lastError = error;
if (error.name === 'AbortError') {
console.log(`Attempt ${attempt + 1} timed out`);
} else {
console.log(`Attempt ${attempt + 1} failed:`, error.message);
}
if (attempt < retries) {
await this.delay(retryDelay * Math.pow(2, attempt));
}
}
}
throw lastError;
}
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// 사용 예시
const client = new FetchClient(5000, 3);
try {
const response = await client.fetch('https://api.example.com/data', {
timeout: 3000,
retries: 2,
});
const data = await response.json();
} catch (error) {
console.error('All attempts failed:', error.message);
}3. 병렬 요청 타임아웃
async function fetchAllWithTimeout(urls, timeoutMs = 10000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const promises = urls.map(url =>
fetch(url, { signal: controller.signal })
.then(res => res.json())
);
const results = await Promise.allSettled(promises);
clearTimeout(timeoutId);
return results.map((result, index) => ({
url: urls[index],
status: result.status,
data: result.status === 'fulfilled' ? result.value : null,
error: result.status === 'rejected' ? result.reason.message : null,
}));
} catch (error) {
clearTimeout(timeoutId);
throw error;
}
}
// 사용 예시
const urls = [
'https://api1.example.com/data',
'https://api2.example.com/data',
'https://api3.example.com/data',
];
const results = await fetchAllWithTimeout(urls, 5000);
console.log(results);4. 커스텀 훅 스타일
function useFetchWithTimeout(url, options = {}) {
const { timeout = 5000, deps = [] } = options;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
const cleanup = () => {
clearTimeout(timeoutId);
};
return {
fetch: async () => {
try {
const response = await fetch(url, {
signal: controller.signal,
});
cleanup();
return response.json();
} catch (error) {
cleanup();
throw error;
}
},
abort: () => controller.abort(),
cleanup,
};
}
// 사용 예시
const { fetch: fetchData, abort } = useFetchWithTimeout(
'https://api.example.com/data',
{ timeout: 3000 }
);
// 3초 후 자동으로 요청 취소
const data = await fetchData();5. AbortController 재사용
class AbortManager {
constructor() {
this.controllers = new Map();
}
create(id, timeoutMs = 5000) {
// 기존 컨트롤러가 있으면 취소
if (this.controllers.has(id)) {
this.abort(id);
}
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
this.controllers.set(id, { controller, timeoutId });
return controller.signal;
}
abort(id) {
const entry = this.controllers.get(id);
if (entry) {
clearTimeout(entry.timeoutId);
entry.controller.abort();
this.controllers.delete(id);
}
}
abortAll() {
for (const [id] of this.controllers) {
this.abort(id);
}
}
}
// 사용 예시
const abortManager = new AbortManager();
// API 요청
const signal = abortManager.create('user-fetch', 3000);
const response = await fetch('/api/user', { signal });
// 요청 취소
abortManager.abort('user-fetch');Lessons Learned
- 타임아웃 설정 필수: 모든 외부 API 호출에 타임아웃을 설정하세요
- 에러 분류: AbortError와 다른 에러를 구분하여 처리하세요
- 재시도 전략: 타임아웃 발생 시 지수 백오프로 재시도하세요
- 리소스 정리: AbortController와 setTimeout을 적절히 정리하세요
- 병렬 처리: 여러 요청을 병렬로 처리할 때 전체 타임아웃을 설정하세요
This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.