비동기 타임아웃 관리 (Managing Async Timeouts)
자바스크립트에서 비동기 작업을 다룰 때 타임아웃 관리는 꽤 중요한 주제다. 비동기 작업이 너무 오래 걸리거나, 제한 시간 안에 끝나지 않으면 흐름을 제어할 필요가 생긴다. 이번에는 비동기 타임아웃을 관리하는 다양한 방법을 코드와 함께 하나씩 풀어보려고 한다.
타임아웃을 잘 다루면 비동기 작업의 안정성과 효율성을 크게 높일 수 있다. 단계별로 차근차근 알아보자.
타임아웃 기본 다루기
setTimeout
과 비동기 작업을 결합하는 가장 단순한 방법부터 보자:
function delay(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function fetchWithTimeout() {
await delay(2000);
console.log("작업 완료");
}
fetchWithTimeout();
// 2초 후 "작업 완료"
여기서는 단순히 지연 시간을 만들어서 비동기 작업을 흉내냈다. 하지만 실제로는 타임아웃을 강제해야 할 때가 많다.
1. Promise.race로 타임아웃 설정
Promise.race
를 사용하면 작업과 타임아웃 중 먼저 끝나는 쪽을 선택할 수 있다:
function timeout(ms, promise) {
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error("시간 초과")), ms);
});
return Promise.race([promise, timeoutPromise]);
}
async function slowTask() {
await delay(3000);
return "작업 완료";
}
async function run() {
try {
const result = await timeout(2000, slowTask());
console.log(result);
} catch (error) {
console.log(error.message);
}
}
run();
// 2초 후 "시간 초과"
작업이 3초 걸리는데 타임아웃은 2초로 설정했으니, 타임아웃이 먼저 발생했다.
2. 실제 API 호출에 적용
API 호출에 타임아웃을 추가해보자:
async function fetchWithTimeout(url, ms) {
const fetchPromise = fetch(url);
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error("API 호출 시간 초과")), ms);
});
const response = await Promise.race([fetchPromise, timeoutPromise]);
return response.json();
}
async function run() {
try {
const data = await fetchWithTimeout(
"https://jsonplaceholder.typicode.com/posts/1",
1000
);
console.log(data.title);
} catch (error) {
console.log(error.message);
}
}
run();
// 네트워크 상태에 따라 성공하거나 "API 호출 시간 초과" 출력
API 응답이 1초 안에 오지 않으면 타임아웃이 발생한다.
3. 타임아웃 취소 가능하게 만들기
clearTimeout
을 활용해서 타임아웃을 취소할 수 있게 해보자:
function timeoutWithCancel(ms, promise) {
let timeoutId;
const timeoutPromise = new Promise((_, reject) => {
timeoutId = setTimeout(() => reject(new Error("시간 초과")), ms);
});
return Promise.race([
promise.then((value) => {
clearTimeout(timeoutId);
return value;
}),
timeoutPromise
]);
}
async function fastTask() {
await delay(1000);
return "빠른 작업 완료";
}
async function run() {
try {
const result = await timeoutWithCancel(2000, fastTask());
console.log(result);
} catch (error) {
console.log(error.message);
}
}
run();
// 1초 후 "빠른 작업 완료" (타임아웃 취소됨)
작업이 타임아웃 전에 끝나면 clearTimeout
으로 타이머를 정리했다.
4. AbortController로 제어
AbortController
를 사용하면 타임아웃과 함께 작업 자체를 중단할 수 있다:
async function fetchWithAbort(url, ms) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), ms);
try {
const response = await fetch(url, { signal: controller.signal });
clearTimeout(timeoutId);
return response.json();
} catch (error) {
if (error.name === "AbortError") {
throw new Error("요청이 타임아웃으로 중단됨");
}
throw error;
}
}
async function run() {
try {
const data = await fetchWithAbort(
"https://jsonplaceholder.typicode.com/posts/1",
500
);
console.log(data.title);
} catch (error) {
console.log(error.message);
}
}
run();
// 0.5초 안에 응답 없으면 "요청이 타임아웃으로 중단됨"
AbortController
로 요청 자체를 중단해서 자원을 아꼈다.
5. 다중 타임아웃 관리
여러 비동기 작업에 각기 다른 타임아웃을 적용해보자:
async function taskWithTimeout(task, ms) {
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error("시간 초과")), ms);
});
return Promise.race([task, timeoutPromise]);
}
async function runMultiple() {
const tasks = [
taskWithTimeout(delay(1000), 2000),
taskWithTimeout(delay(3000), 2000),
taskWithTimeout(delay(500), 1000),
];
try {
const results = await Promise.all(tasks);
console.log("모두 성공");
} catch (error) {
console.log(error.message);
}
}
runMultiple();
// "시간 초과" (2번째 작업이 3초 걸리는데 타임아웃은 2초)
각 작업에 맞는 타임아웃을 설정해서 병렬로 관리했다.
6. 타임아웃 재시도 로직
타임아웃이 발생하면 재시도하는 방법을 추가해보자:
async function fetchWithRetry(url, ms, retries) {
for (let i = 0; i < retries; i++) {
try {
const response = await timeout(ms, fetch(url));
return response.json();
} catch (error) {
console.log(`시도 ${i + 1} 실패: ${error.message}`);
if (i === retries - 1) throw error;
await delay(1000);
}
}
}
async function run() {
try {
const data = await fetchWithRetry(
"https://jsonplaceholder.typicode.com/posts/1",
500,
3
);
console.log(data.title);
} catch (error) {
console.log("최종 실패: " + error.message);
}
}
run();
// 네트워크 상태에 따라 성공하거나 "최종 실패: 시간 초과"
3번 재시도하며 실패 시마다 1초 대기했다.
7. 타임아웃과 스트림 결합
비동기 스트림에 타임아웃을 적용해보자:
async function* streamWithTimeout() {
let count = 0;
while (count < 5) {
count++;
yield timeout(1000, delay(1500));
}
}
async function processStream() {
try {
for await (const value of streamWithTimeout()) {
console.log("스트림 진행");
}
} catch (error) {
console.log(error.message);
}
}
processStream();
// "시간 초과" (각 작업이 1.5초 걸리는데 타임아웃은 1초)
스트림 각 단계에 타임아웃을 적용해서 흐름을 제어했다.
8. 성능과 안정성 고려
타임아웃 관리가 코드에 어떤 영향을 주는지 살펴보자:
- 성능: 타임아웃으로 느린 작업을 끊어내니 전체 흐름이 빨라질 수 있다.
- 안정성: 작업이 무한정 대기하지 않도록 보장한다.
Promise.race와 AbortController를 상황에 맞게 활용하는 점이 핵심이다.
마무리
비동기 타임아웃 관리는 단순한 지연부터 API 호출, 스트림까지 다양한 상황에서 유용하다. Promise.race
, AbortController
, 재시도 로직 등을 조합하면 안정적이고 효율적인 비동기 흐름을 만들 수 있다.
'코딩 공부 > 자바스크립트' 카테고리의 다른 글
70. 자바스크립트 Fetch API 심화 (Advanced Fetch API) (1) | 2025.03.23 |
---|---|
69. 자바스크립트 히스토리 API (Browser History API) (3) | 2025.03.23 |
67. 자바스크립트 async/await 최적화 (Optimizing Async/Await) (1) | 2025.03.22 |
66. 자바스크립트 프로미스 병렬 처리 심화 (Advanced Promise Parallelism) (1) | 2025.03.22 |
65. 자바스크립트 이벤트 루프와 태스크 스케줄링 (Event Loop and Task Scheduling) (0) | 2025.03.21 |