형주의 블로그

Promise, async and await

2022-05-20

자바스크립트에서 비동기 함수를 순차적으로 호출하는 방법 중에는 콜백, Promise, async 함수를 이용한 방법이 있습니다.

콜백을 이용하는 방법

function pickApple(callback) {
  setTimeout(() => {
    console.log("picking an apple...");
    callback("apple");
  }, 1000);
}
 
function eat(food, callback) {
  setTimeout(() => {
    console.log(`eating ${food}...`);
    callback(food);
  }, 3000);
}
 
function goToSleep(food) {
  setTimeout(() => {
    console.log(`ate ${food}...going to bed`);
  }, 3000);
}
 
pickApple((food) => {
  eat(food, (food) => goToSleep(food));
});

콜백으로 비동기 로직을 처리하기 위해서는 비동기 로직 후에 그 다음 함수가 실행되도록 전의 비동기 함수에 callback 파라미터를 받아 함수 안에서 비동기 로직이 모두 실행된 다음 callback으로 다음 함수를 실행할 수 있도록 해야합니다.

다음과 같은 문제가 있었습니다.

  • 비동기 함수 다음 처리해야 하는 로직이 있을 때마다 콜백을 argument로 넘겨줘야 한다.
  • 에러 처리를 할 경우 에러 핸들링 함수도 콜백으로 넘겨주어야 한다.
  • 순서대로 진행되어야 하는 함수가 많을수록 더 깊은 콜백이 필요하다.

Promise를 쓰는 이유

  • Promise의 resolve를 통해 다음 순서에 필요한 콜백을 parameter로 넘겨줄 필요 없이 호출된 코드 라인으로 값을 넘겨줄 수 있다.
  • resolve를 통해 전달된 결과 값을 then으로 받아 처리할 수 있다.
  • reject를 통해 에러를 전달받아 핸들링 할 수 있다.

Promise를 이용하는 방법

function pickApple() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("picking an apple...");
      resolve("apple");
    }, 1000);
  });
}
 
function eat(food) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(`eating ${food}...`);
      resolve(food);
    }, 3000);
  });
}
 
function goToSleep(food) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(`ate ${food}...going to bed`);
    }, 3000);
  });
}
 
pickApple().then((food) => {
  eat(food).then(() => {
    pickApple().then((food) => {
      eat(food).then((food) => {
        goToSleep(food);
      });
    });
  });
});

콜백 지옥과 Promise.then 중첩의 차이점

  • 콜백과 달리 promise로 순서가 있는 비동기 함수를 실행할 때 promise의 리턴값이 then으로 넘겨지기 때문에 then이 계속해서 중첩되게 된다.

async await 키워드를 이용하는 방법

function hello () {
	return Promise.resolve(‘hello’);
}
 
async function hello () {
	return ‘hello’;
}

위의 두 코드는 같습니다. 두 번째 코드는 첫 번째 코드의 syntatic sugar라고 할 수 있습니다.

function delay() {
  return new Promise((resolve) => {
    setTimeout(resolve, 1000);
  });
}
 
async function pickApple() {
  await delay();
  return "apple";
}
 
async function pickBanana() {
  await delay();
  return "banana";
}
 
async function asyncPickAllFruits() {
  const applePromise = pickApple();
  const bananaPromise = pickBanana();
  const apple = await applePromise;
  const banana = await bananaPromise;
 
  console.log([apple, banana].join(" + "));
}
 
function pickAllFruits() {
  Promise.all([pickApple(), pickBanana()]).then((fruits) => {
    console.log(fruits.join(" + "));
  });
}
 
pickAllFruits();

await await연산자는 Promise를 기다리기 위해 사용됩니다. 연산자는 async function 내부에서만 사용할 수 있습니다.

await vs return vs return await

다음 예제를 통해 async await이 어떻게 동작하게 되는지 예측해보며 개념에 대한 이해를 더 정확하게 체크할 수 있었습니다. 해당 예제는 여기에서 가져왔습니다.

async function waitAndMaybeReject() {
  // Wait one second
  await new Promise((r) => setTimeout(r, 1000));
  // Toss a coin
  const isHeads = Boolean(Math.round(Math.random()));
 
  if (isHeads) return "yay";
  throw Error("Boo!");
}

just calling

async function foo() {
  try {
    waitAndMaybeReject();
  } catch (e) {
    return "caught";
  }
}

waitAndMaybeReject는 async 함수이기 때문에 무조건 Promise를 반환합니다.
일반 함수였다면 50퍼센트의 확률로 'yay'라는 문자열을 반환했겠지만,
async 함수이기 때문에 50퍼센트의 확률로 'yay'를 resolve하는 Promise 혹은 Error('Boo!')로 reject하는 Promise 가 반환됩니다.
두 값 모두 resolve, reject가 되지 않은 pending 상태의 Promise이기 때문에 위의 코드에서 절대 에러가 발생하지 않게 됩니다.

awaiting

async function foo() {
  try {
    await waitAndMaybeReject();
  } catch (e) {
    return "caught";
  }
}

await 키워드를 사용하므로써, pending상태의 Promise가 resolve 또는 reject 처리가 됩니다.
waitAndMaybeReject에서 isHeads가 true이면 foo에서 resolve처리가 되고, isHeads가 false이면 foo에서 error가 발생하게 됩니다.

returning

async function foo() {
  try {
    return waitAndMaybeReject();
  } catch (e) {
    return "caught";
  }
}

await 키워드 없이 waitAndMaybeReject를 바로 return할 경우에도 첫번째 just calling의 경우와 같이 pending 상태의 Promise이기 때문에 catch 블록이 절대 실행되지 않습니다.

return-awaiting

async function foo() {
  try {
    return await waitAndMaybeReject();
  } catch (e) {
    return "caught";
  }
}

그렇다면 Promise를 받아 await 처리를 하고, 이를 return하는 async 함수는 어떻게 동작할까요?

isHeads = true로 나왔다면

  1. waitAndMaybeReject로부터 pending 상태의 Promise를 받는다.
  2. await를 통해 resolve 처리한다.
  3. resolve 된 경우 이 값을 다시 Promise.relove('yay')로 반환한다.

isHeads = false로 나왔다면

  1. waitAndMaybeReject로부터 pending 상태의 Promise를 받는다.
  2. await를 통해 reject 처리한다.
  3. Error('Boo!')로 인해 catch 블록이 실행된다.

try-catch 문이 없다면 Promise의 resolve, reject 처리가 된 결과값을 다시 Promise로 반환하는 것은 redundant한 일입니다.
그렇기 때문에 try-catch 블록을 사용하지 않는 경우에 한에 이를 제한하는 eslint 규칙이 존재합니다.

참고자료

Guide on using promises
Promise apis
await vs return vs return await