Back
Featured image of post Promise와 Async/await

Promise와 Async/await

비동기 콜백지옥에서 벗어나자..

Promise

function callbackDelay(sec, callback) {
  setTimeout(() => {
    callback('콜백 Delay>>' + new Date().toString());
  }, sec * 1000);
}

callbackDelay(1, (result) => {
  console.log(1, result);
  callbackDelay(1, (result) => {
    console.log(2, result);
    callbackDelay(1, (result) => {
      console.log(3, result);
    });
  });
});
/*result
1 콜백 Delay>>Sun Jun 07 2020 18:43:56 GMT+0900 (GMT+09:00)
promise.js:26
2 콜백 Delay>>Sun Jun 07 2020 18:43:57 GMT+0900 (GMT+09:00)
promise.js:28
3 콜백 Delay>>Sun Jun 07 2020 18:43:58 GMT+0900 (GMT+09:00)
*/

자바스크립트에서 비동기 동작을 할때 우리는 항상 해당 함수에 콜백 함수를 넣어줬다. 만약 타이머로 1초마다 해당 시간을 출력하는 함수를 예시로 들면..
대표적인 비동기 함수인 setTimeout을 호출할 때 이렇게 매 순간 콜백 함수를 전달해야 1..2..3..형태로 출력되는 것을 볼 수 있다. 하지만 이 코드의 불편함은 콜백이 콜백을 전달함으로써 **콜백지옥(callback hell)**이 펼쳐진다는것이다. 이를 개선하기 위해 Promise객체를 사용하면 더욱 간결하게 비동기 로직에 대응할 수 있다.

사용법

Promise에는 3가지 상태가 있다. (대기 - 이행 - 실패)

  1. 대기는 프로미스를 생성자로 호출한 순간이다. 프로미스는 콜백 함수를 인수로 받고, 성공(resolve) - 실패(reject) 상태를 대응할 수 있다.

    new Promise(function (resolve, reject) {
      //code..
    });
    
  2. resolve를 호출할 시 이행 상태가 된다.

    new Promise(function (resolve, reject) {
      resolve();
    });
    
  3. 이행(완료) 상태가 되면 then()메서드로 콜백함수를 전달할 수 있다.

    new Promise(function (resolve, reject) {
      const result = 'success!';
      resolve();
    }).then(function (res) {
      console.log(res);
    });
    
    //success!
    
  4. 실패 상태가 되면 catch()메서드로 받을 수 있다.

    new Promise(function (resolve, reject) {
      const fail = 'fail!';
      reject(new Error(fail));
    })
      .then()
      .catch(function (res) {
        console.log(res);
      });
    
    //fail!
    

또한 여러개의 Promise들의 인스턴스를 순차적으로 연결시킬 수 있다.

function getData() {
  return new Promise(function (resolve, reject) {
    resolve('success!!'); //then method chain
  });
}

getData()
  .then(function (data) {
    console.log(data + 'first');
    return data;
  }) //success!!first
  .then(function (data) {
    console.log(data + 'second');
    return data;
  })
  .then(function (data) {
    console.log(data + 'third');
  });

이제 기존 단순히 콜백을 전달하는 방식으로 작성한 타이머 코드를 프로미스 형태로 개선해보자.

function promiseDelay(sec) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('프로미스 Delay>>' + new Date().toString());
    }, sec * 1000);
  });
}

promiseDelay(1)
  .then((res) => {
    console.log(1, res);
    return promiseDelay(1); //프로미스를 다시 호출하지 않으면 undefined
  })
  .then((res) => {
    console.log(2, res);
  });

return 활용법

프로미스 내부에서는 return 을 사용하지 않고, resolve()만 시행시킨다면 후행의 코드들도 모두 실행한다.
따라서 즉시 then(이행 상태)으로 넘어가려면 return을 해주면 된다. (함수 return 방식과 동일하다.)

return new Promise((resolve, reject) => {
  attach.readFile((data) => {
    resolve(data);
    console.log('success'); // 출력
  });
});

catch문 내부에서 return을 통해 에러를 효율적으로 처리할 수있다.
에러가 발생하면 catch메서드에서 처리하게되는데 , 다시 then으로 넘겨 에러 이후의 처리들도 할 수 있다.

const getUserInfo = () =>
  new Promise((resolve, reject) => {
    setTimeout(() => resolve({ id: 'abc', pw: 'qwerty' }), 1000);
  });

const executeHandler = (userInfo) =>
  new Promise((resolve, reject) => {
    console.log(JSON.stringify(userInfo));
    setTimeout(() => reject(new Error('fail process')), 1000);
  });

getUserInfo()
  .then((userInfo) => executeHandler(userInfo))
  .catch((error) => {
    return `'${error}' handlling success!!`;
  }) //promise Chain, 오류가 발생하더라도 다시 값을 처리해 .then(result)로 넘길 수 있다.
  .then((result) => console.log(result));

Async-await

Promise를 더욱 간결하게 처리할 수 있는 문법이다. 함수 앞에 async 키워드를 선언하면 된다.
await예약어는 해당 함수가 꼭 프로미스 객체를 반환해야 한다.

function getlUserApi(){
	//promise 반환 (비동기 함수)
	return new Promise((resolve, reject) => { //..code });
};

async function showUser(){
	const userInfo = await getUserApi();
	return userInfo;
}

위 코드는 아래와 같이 Promise를 반환시킨다. 따라서 프로미스와 마찬가지로 then을 통해 콜백 함수를 처리한다.

async function showUser() {
  const userInfo = getUssrApi();
  return Promise.resolve(userInfo);
}

만약 사용자의 정보를 받아오는 함수가 있다면 아래와 같이 사용할 수 있다.

//AJAX 대신 setTimeout으로 대체;
function getUserApi() {
  return new Promise((resolve, reject) => {
    //Promise 반환
    setTimeout(() => {
      resolve({ name: 'jiny', age: 27 });
    }, 3000);
  });
}

//async 함수 내부에 await 선언
async function showUser() {
  const userInfo = await getUserApi();
  return userInfo;
}

//이행 처리
showUser().then((data) => {
  console.log(JSON.stringify(data));
});

에러처리

async 함수내에서 오류가 발생하면 try-catch문으로 에러를 핸들링해줄 수 있다.
물론 catch()로 프로미스에서 해결하지 못한 에러들도 추가적으로 처리가 가능하다.

async function showUser() {
  try {
    const result = await getUserApi();
    return userInfo;
  } catch (error) {
    console.error(error);
  }
}

Top-level await

chrome 89+, Node.js 14+ 버전부터 async 함수 예외에서도 await을 사용할 수 있다고한다.
최신 브라우저에서는 async로 굳이 감싸지 않아도 될듯하다.. (실제 테스트해보았더니 잘 실행된다!)

await getUserApi()
	.then(function(res){ console.log(JSON.stringify(res) });

참고자료

  • Node.js 교과서 - 프로미스 (조현영 저)
  • 자바스크립트 코딩의 기술 - async/await (조 모건 저)
  • async와 await - Frontend.INFO
  • toplevel await - v8.dev