Promise 작업 시간 초과하면 실패로 간주하기 (feat. setTimeout, Promise.race)

개요

요즘 자바스크립트에서는 Promise 가 많이 쓰입니다. 이른바 비동기 작업이라고도 하죠. 여러가지 시간이 좀 걸리는 작업들을 다룰 때 편하기 때문인데요, 그런데 너무 오래 걸리는 작업을 취소하고 싶지 않나요?

예를 들어 게시글의 댓글을 불러오기 위해 api 서버에 요청을 날렸는데, 2~3초가 지나도 묵묵부답이라면, 이건 api 서버가 잘못되었다고 보고, 작업을 취소하고, 유저에게 “서버로부터 응답이 없습니다. 잠시 후 다시 시도해주세요.” 라는 에러 메시지를 보내야 할 것입니다. 그러나 요청이 스스로 시간초과 에러다! 라고 알기까지는 10초가 넘어갈 수도 있습니다.

10초간 계속 로딩 애니메이션이 돌고 있다면? 짜증나겠죠. 이를 조기에 빨리 없애버리도록 해보자구요.

목표

시간초과한 Promise 작업을 취소할 수 있다.

사전 지식

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가 반환이 되겠지요?

분기 나누기

아래 코드에서 jobDurationMStimeoutMS 보다 더 크게 해보고, 더 작게 해보고 하면서 결과를 비교해보세요. 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.racesetTimeoutasync, await 등을 이미 잘 알고 있다면, 이런 응용은 아마 쉬운 축에 속할 테지만, 아직 해당 개념이 익숙하지 않은 상태에서는 몬가 본 글이 훌륭한 예시가 되지 않을까 싶어요. 여러분들의 자바스크립트 여행을 응원합니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

Scroll to top