개요
요즘 자바스크립트에서는 Promise 가 많이 쓰입니다. 이른바 비동기 작업이라고도 하죠. 여러가지 시간이 좀 걸리는 작업들을 다룰 때 편하기 때문인데요, 그런데 너무 오래 걸리는 작업을 취소하고 싶지 않나요?
예를 들어 게시글의 댓글을 불러오기 위해 api 서버에 요청을 날렸는데, 2~3초가 지나도 묵묵부답이라면, 이건 api 서버가 잘못되었다고 보고, 작업을 취소하고, 유저에게 “서버로부터 응답이 없습니다. 잠시 후 다시 시도해주세요.” 라는 에러 메시지를 보내야 할 것입니다. 그러나 요청이 스스로 시간초과 에러다! 라고 알기까지는 10초가 넘어갈 수도 있습니다.
10초간 계속 로딩 애니메이션이 돌고 있다면? 짜증나겠죠. 이를 조기에 빨리 없애버리도록 해보자구요.
목표
시간초과한 Promise 작업을 취소할 수 있다.
사전 지식
- Promise,
async
,await
개념과 사용법 - 화살표 함수
- Javascript 코드를 실행시킬 수 있는 환경 (브라우저 또는 Node.js)
- 만약 TypeScript 를 한다면, Node.js 환경
setTimeout
setTimeout
은 자바스크립트에서 대표적인 비동기 작업입니다. 하는 일은 주어진 시간만큼 기다리다가 어떤 함수를 실행시키는 것이지요. 이 setTimeout
을 이용하여 단순히 기다리기만 하는 Promise 를 손쉽게 만들 수 있습니다.
new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, 1000);
}).then(() => {
// 1초 뒤 할 일
});
너무 긴가요? 조금만 줄여봅시다. 아래 코드는 위 코드와 정확히 똑같이 동작하지요!
new Promise(resolve => setTimeout(resolve, 1000)).then(() => {
// 1초 뒤 할 일
});
위 코드가 왜 저렇게 되는지 헷갈린다면 화살표 함수를 확실하게 익혀봅시다.
그런데, 이렇게 쓸 바에야 그냥 setTimeout
을 그대로 쓰면 될 텐데, 왜 이렇게 하냐구요? 그것은 async
함수 안에서 await
을 쓰기 위함이지요. 아래 코드를 봅시다.
// async 함수 내부입니다.
await new Promise(resolve => setTimeout(resolve, 1000))
// 1초 뒤 할 일
훨씬 간단해졌죠? 이렇게 Promise 식으로 바꾼 setTimeout
을 오늘은 이용해 볼 겁니다.
Promise.race
작업시간을 초과하는지 아닌지 구분짓는 방법은 많이 복잡하지 않습니다. 우리는 Promise.race
를 사용할 .겁니다 race 는 경주라는 뜻이죠, 이 함수에 Promise의 배열을 넣으면, 그 중 가장 먼저 끝나는 Promise 를 리턴합니다. 다음 간단한 예제를 보시죠.
Promise.race([
new Promise((resolve) => setTimeout(() => resolve("Promise1"), 1000)),
new Promise((resolve) => setTimeout(() => resolve("Promise2"), 500)),
])
.then((result) => {
console.log(result);
})
.catch((e) => {
console.error(e);
});
결과는 Promise2
가 출력될 것입니다.
가장 먼저 끝난다는 건, 성공하든 실패하든 상관이 없습니다. 실패의 경우를 볼까요?
Promise.race([
new Promise((resolve) => setTimeout(() => resolve("hahaha"), 1000)),
new Promise((resolve, reject) => setTimeout(() => reject(new Error()), 500)),
])
.then((result) => {
console.log(result);
})
.catch((e) => {
console.error(e);
});
Error
at Timeout._onTimeout (/Users/th.kim/Desktop/playground-js/promise.race.js:3:60)
at listOnTimeout (internal/timers.js:557:17)
at processTimers (internal/timers.js:500:7)
에러가 발생하면서 끝납니다.
이러한 특성을 이용해서, setTimeout
을 2초로 설정해두고, 우리의 작업과 타이머를 race 시키면, 우리의 작업이 먼저 끝날 시 그 Promise가 반환이 될 것이고, 시간이 2초보다 오래 걸린다면 setTimeout
한 Promise가 반환이 되겠지요?
분기 나누기
아래 코드에서 jobDurationMS
를 timeoutMS
보다 더 크게 해보고, 더 작게 해보고 하면서 결과를 비교해보세요. job
은 우리가 가상으로 만든 “작업” Promise 입니다.
const jobDurationMS = 3000;
const timeoutMS = 2000;
const job = new Promise((resolve) =>
setTimeout(() => resolve("job end"), jobDurationMS)
);
let timer;
Promise.race([
job,
new Promise((resolve) => {
timer = setTimeout(() => resolve("timeout"), timeoutMS);
}),
])
.then((result) => {
if (result === "timeout") {
console.log("시간이 초과되었습니다!");
} else {
console.log("시간 내에 작업을 완료하였습니다.");
}
})
.finally(() => clearTimeout(timer));
마지막의 finally
는 작업이 시간초과를 재는 타이머보다 먼저 끝났을 때 타이머를 종료하는 역할입니다.
async 함수로 모듈화하기
async 함수로 만들어서, 다른 여러 코드에 사용할 수 있도록 해봅시다.
자바스크립트 버전
export async function timeout(promise, ms) {
let timer;
const res = await Promise.race([
promise,
new Promise(resolve => {
timer = setTimeout(() => resolve('timeout'), ms);
})
]).finally(() => clearTimeout(timer));
if (res === 'timeout') {
throw new Error(`${ms}ms timeout`);
}
return res;
}
타입스크립트 (Node.js) 버전
export async function timeout<T>(promise: Promise<T>, ms: number): Promise<T> {
let timer: NodeJS.Timeout;
const res = await Promise.race([
promise,
new Promise<'timeout'>(resolve => {
timer = setTimeout(() => resolve('timeout'), ms);
})
] as const).finally(() => clearTimeout(timer));
if (res === 'timeout') {
throw new Error(`${ms}ms timeout`);
}
return res;
}
저렇게 함수를 만들어놓고 await timeout(job, 1000);
이렇게 하면, 작업이 1초 안에 끝나지 않을 시 throw
가 발동하면서 예외를 발생시킵니다. 로직화하는 데 훨씬 편하겠죠?
마치며
Promise 와 Promise.race
와 setTimeout
과 async
, await
등을 이미 잘 알고 있다면, 이런 응용은 아마 쉬운 축에 속할 테지만, 아직 해당 개념이 익숙하지 않은 상태에서는 몬가 본 글이 훌륭한 예시가 되지 않을까 싶어요. 여러분들의 자바스크립트 여행을 응원합니다.