포인트3 테크니컬 하우스

Promise, async/await

윤수민
윤수민May 19, 2024
4 min read|

1.Promise란?

  • Promise는 자바스크립트에서 비동기 처리를 좀 더 쉽게 다루기 위해 도입된 개념이다.

비동기처리란?

  • 비동기 작업이란, 서버에 데이터를 요청하는 것과 같이 시간이 걸리는 작업을 수행하면서, 그 작업이 끝나기를 기다리지 않고 별도의 다른 작업을 계속 진행하는 것을 말한다.

Promise가 도입된 이유

  • 비동기는 특정 코드의 실행이 완료될 때까지 기다리지 않고 다음 코드를 먼저 수행하는 방식이기 때문에, 만일 비동기 작업의 결과에 따라 다른 작업을 수행해야 할 때는 전통적으로 콜백 함수를 사용했다.
  • 콜백 함수란 비동기 작업이 완료되면 호출되는 함수의 의미로서, 비동기 함수의 매개변수로 함수 객체를 넘기는 기법을 말한다. 그래서 함수 내부에서 함수 호출을 통해 비동기 작업의 결과를 받아서 인자로 주면 이를 통해 후속 처리 작업을 수행할 수 있다.

  • 하지만 콜백 함수를 사용하면 코드가 복잡하고 가독성이 떨어지는 문제가 있다. 특히, 여러 개의 비동기 작업을 순차적으로 수행해야 할 때는 콜백 함수가 중첩되어 코드의 깊이가 깊어지는 현상이 발생한다. 이러한 현상을 콜백 지옥(callback hell) 이라고 부른다.
  • 이에 ES6에서는 비동기 처리인 콜백함수를 해결하기 위한 또 다른 패턴으로 프로미스(Promise)를 도입했다. Promise는 전통적인 콜백 패턴이 가진 단점을 보완하여 비동기 처리 시점을 명확하게 표현할 수 있다는 장점이 있다.

프로미스 객체 생성

const Promise = new Promise((resolve, reject) => {
	// 비동기 작업 수행
    const data = fetch('서버로부터 요청할 URL');
    
    if(data)
    	resolve(data); 
    else
    	reject("Error"); 
})
typescript
  • Promise 객체를 생성하려면 new 키워드와 Promise 생성자 함수를 사용하면 된다. 이때 Promise 생성자 안에 두개의 매개변수를 가진 콜백 함수를 넣게 되는데, 첫 번째 인수는 작업이 성공했을 때 성공(resolve)임을 알려주는 객체이며, 두 번째 인수는 작업이 실패했을 때 실패(reject)임을 알려주는 오류 객체이다.

Promise는 비동기 처리가 성공(fulfilled)하였는지 또는 실패(rejected)하였는지 등의 상태(state) 정보를 갖는다.

  • pending: 비동기 처리가 아직 수행되지 않은 상태
  • fulfilled: 비동기 처리가 수행된 상태 (성공)
  • rejected: 비동기 처리가 수행된 상태 (실패)
  • settled: 비동기 처리가 수행된 상태 (성공 또는 실패)

let promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("비동기 작업 완료!");
  }, 1000);
});

promise
  .then((message) => {
    console.log(message); // "비동기 작업 완료!"
  })
  .catch((error) => {
    console.log('Error:', error);
  });
javascript

Promise 호출 과정

  1. 비동기 함수 내에서 Promise 객체를 생성하고 그 내부에서 비동기 처리를 구현한다. 이때 비동기 처리에 성공하면 resolve 메소드를 호출한다.
  1. 이때 resolve 메소드의 인자로 비동기 처리 결과를 전달 하는데, 이 처리 결과는 Promise 객체의 후속 처리 메소드로 전달된다.
  1. 만약 비동기 처리에 실패하면 reject 메소드를 호출한다. 이때 reject 메소드의 인자로 에러 메시지를 전달한다. 이 에러 메시지는 Promise 객체의 후속 처리 메소드로 전달된다.

후속 처리 메소드에는 대표적으로 then(Promise 반환)과 catch(예외)가 있다.

then
catch


콜백지옥에 이은 프로미스 지옥

fetch("https://point3.io/users")
  .then((response) => {
    if (response.ok) {
      return response.json();
    } else {
      throw new Error("Network Error");
    }
  })
  .then((users) => {
    return users.map((user) => user.login);
  })
  .then((logins) => {
    return logins.join(", ");
  })
  .then((result) => {
    console.log(result);
  })
  .catch((error) => {
    console.error(error);
  });
typescript
  • 그런데 콜백 못지않게 프로미스의 then() 메서드가 지나치게 체인되어 반복되면 코드가 장황해지고 가독성이 굉장히 떨어질 수 가 있다.
  • 위와 같이 then을 늘어뜨려 놓으면 코드가 길어지고, 각 then 메서드가 어떤 값을 반환하는지 파악하기 어렵게 된다. 또한, catch 메서드가 마지막에 한 번만 사용되어 있기 때문에, 중간에 발생할 수 있는 에러나 예외 상황에 대응하기 어렵다.

위와 같은 프로미스지옥과 같은 상황을 극복하기 위해 또다른 자바스크립트 문법 나오게 된다.

그것은 바로 “async/await”

2. async/await란?

  • async/await는 자바스크립트의 최신 비동기 처리 패턴이다.


async/ await를 쓰는 이유는?

  • 이전의 비동기 처리 방식인 콜백 함수와 프로미스의 단점을 보완한다.
    (콜백 함수와 프로미스는 콜백 지옥(callback hell)과 프로미스 체이닝(프로미스 지옥) 과 같은 복잡성과 가독성 문제를 야기한다.)
  • 비동기 코드를 동기 코드처럼 읽고 쓸 수 있어 비동기 로직을 더 쉽게 이해하고 사용할 수 있다.

유의해야 할 점

async/await가 Promise를 대체하기 위한 기능은 아니다.

  • 내부적으로는 여전히 Promise를 사용해서 비동기를 처리하고, 단지 코드 작성 부분을 프로그래머가 유지보수하게 편하게 보이는 문법만 다르게 해줄 뿐이다.

async function f() {
  return 1;
}

f().then(alert); // 1
javascript
  • async는 함수 앞에 위치하며, 이를 사용하여 함수를 정의하면 해당 함수는 항상 Promise를 반환한다. 반환값이 Promise가 아닌 경우, JavaScript는 자동으로 값을 Promise로 감싸 반환한다.

async function foo() {
  let promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve("완료!"), 1000)
  });

  let result = await promise; // Promise가 이행될 때까지 기다림

  alert(result); // "완료!"
}

foo();
javascript

await의 장점

  • await는 async 함수 내에서만 사용되며, Promise의 결과가 나올 때까지 기다려주는 역할을 한다. 이를 통해 비동기 작업을 마치 동기 작업인 것처럼 코딩할 수 있다.
  • await는 말 그대로 Promise가 처리될 때까지 함수 실행을 기다리게 만든다. Promise가 처리되면 그 결과와 함께 실행이 재개된다. 프라미스가 처리되길 기다리는 동안엔 엔진이 다른 일 (다른 스크립트를 실행, 이벤트 처리 등)을 할 수 있기 때문에, CPU 리소스가 낭비되지 않는다.
  • await를 사용하지 않았다면 데이터를 받아온 시점에 콘솔을 출력할 수 있게 콜백 함수나 .then( )등을 사용해야 했을 것이다. 하지만 async/await 문법 덕에 좀 더 직관적인 비동기 처리를 할수 있게 되었다.
  • 또한, await는 promise.then 보다 좀 더 간결하게  result 값을 얻을 수 있고 가독성이 좋고 쓰기도 쉽다는 장점이 있다.

async await 에러 제어

async function f() {

  try {
    let response = await fetch('http://유효하지-않은-주소');
  } catch(err) {
    alert(err); // TypeError: failed to fetch
  }
}

f();
javascript
  • 위 코드와 같이 async 함수는 await가 던진 에러를 try/catch를 사용해 쉽게 잡을 수 있다. 에러가 발생하게 되면 제어 흐름이 catch 블록으로 바로 넘어가게 된다.

async/await를 사용할때 주의할점

  • await를 깊은 이해 없이 막무가내로 사용하게 된다면 성능 문제 및 기타 문제가 발생할 수 있다. await 자체가 Promise가 해결될 때까지 함수 실행을 일시 중지하는 것인데, 병렬적으로 멀티로 처리할 수 있는 작업을 억지로 동기적으로 처리하게 함으로써 오히려 1초만에 해결할 로직을 3초씩이나 걸리게 할 수 있기 때문이다.

예시 코드

async function Clothes() {
  let a = await getShirts(); //  비동기 처리를 요청하고, 요청이 처리될때 까지 기다림 (1초 소요)
  let b = await getPants(); //  비동기 처리를 요청하고, 요청이 처리될때 까지 기다림 (1초 소요)
  console.log(`${a} and ${b}`); // 총 2초 소요
}
typescript

개선된 코드

async function Clothes(){

  let getShirtsPromise = getShirts(); // async함수를 미리 논블록킹으로 실행 
  let getPantsPromise = getPants();  
  
  // 이렇게 하면 거의 동시에 실행되게 된다.
  console.log(getShirtsPromise)
  console.log(getPantsPromise)
  
  let a = await getShirtsPromise; // 위에서 받은 프로미스객체 결과 변수를 await을 통해 꺼낸다.
  let b = await getPantsPromise; // 위에서 받은 프로미스객체 결과 변수를 await을 통해 꺼낸다.
  
  console.log(`${a} and ${b}`); // 본래라면 2초를 기다려야 하는데, 위에서 1초기다리는 함수를 바로 연속으로 비동기로 불려와서 대충 1.01초 정도만 기다리면 처리된다.
typescript

출처:

https://inpa.tistory.com/entry/JS-📚-비동기처리-Promise

https://inpa.tistory.com/entry/JS-📚-비동기처리-async-await

https://joshua1988.github.io/web-development/javascript/promise-for-beginners/

https://joshua1988.github.io/web-development/javascript/js-async-await/

https://mjn5027.tistory.com/85

https://velog.io/@syoung125/JS-Promise란

Subscribe to our newsletter

Get the latest news and updates from our team