프라미스와 async, await

345·2023년 7월 10일

모던 JavaScript

목록 보기
22/23

프라미스 API

Promise 클래스에 존재하는 5가지 정적 메서드에 대해 알아봅시다.

Promise.all

let promise = Promise.all([...promises...]);

Promise.all([
  new Promise(resolve => setTimeout(() => resolve(1), 3000)), // 1
  new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2
  new Promise(resolve => setTimeout(() => resolve(3), 1000))  // 3
]).then(alert); // 프라미스 전체가 처리되면 1, 2, 3이 반환
// 각 프라미스는 배열을 구성하는 요소가 됨

여러 개의 프라미스를 동시에 실행시키고 모든 프라미스의 이행을 기다릴 때 사용합니다.
각 프라미스를 배열에 담아 전달하고, 각각의 결과는 result 배열의 요소가 됩니다.

  • 인수: 프라미스가 담긴 배열
  • 반환값: 프라미스를 반환, result 가 인수로 온 각 프라미스의 이행 결과를 담은 배열

Promise.all 에 전달된 프라미스 중 하나라도 거부되면 Promise.allrejected 상태의 프라미스를 반환합니다.

한 프라미스에서 에러가 발생했다고 다른 프라미스가 중지되는 것은 아닙니다.
다른 프라미스는 그대로 처리됩니다. Promise.all 의 결과가 거부되는 것 뿐입니다.

따라서 다음처럼 모든 프라미스가 이행되어야 하는 상황에 사용합니다.

Promise.all([
  fetch('/template.html'),
  fetch('/style.css'),
  fetch('/data.json')
]).then(render); // render 메서드는 fetch 결과 전부가 있어야 제대로 동작

Promise.allSettled

Promise.allSettledPromise.all 과 달리 일부 프라미스에서 에러가 나도
여전히 나머지 프라미스의 결과가 필요할 때 사용합니다.

Promise.allSettled 는 결과를 배열로 반환하고, 각 요소는 프라미스 성공여부에 대한 status 와 결과값을 가집니다.

  • 응답이 성공할 경우: {status:"fulfilled", value:result}
  • 에러가 발생한 경우: {status:"rejected", reason:error}
let urls = ['...'];
            
Promise.allSettled(urls.map(url => fetch(url)))
  .then(results => { // (*)
    results.forEach((result, num) => {
      if (result.status == "fulfilled") { 
        alert(`${urls[num]}: ${result.value.status}`);
      }
      if (result.status == "rejected") {
        alert(`${urls[num]}: ${result.reason}`);
      }
    });
  });

Promise.race

Promise.race 는 가장 먼저 처리되는 프라미스의 결과를 반환하며, 다른 프라미스는 무시합니다.

let promise = Promise.race(iterable);

Promise.race([
  new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
  new Promise((resolve, reject) => setTimeout(() => reject(new Error("에러 발생!")), 2000)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).then(alert); // 1

🔔 마이크로태스크

프라미스 핸들러 then, catch, finally 는 항상 비동기적으로 실행됩니다.
그런데, 프라미스가 즉시 이행되더라도 핸들러 이후의 코드가 먼저 실행되는 것을 확인할 수 있습니다.

let promise = Promise.resolve();

promise.then(() => alert("프라미스 성공!"));

alert("코드 종료"); // 얼럿 창이 가장 먼저 뜸

핸들러가 나중에 트리거 된 이유에 대해 알아봅시다.

마이크로태스크 큐

비동기 작업 처리를 위해 PromiseJobs 라는 내부 큐가 있는데, V8 엔진은 이를 마이크로태스크 큐 라고 부릅니다.
마이크로태스크 큐는 다음과 같이 동작합니다.

  • 먼저 들어온 작업을 먼저 실행 (FIFO)
  • 실행할 것이 아무것도 없을 때만 마이크로태스크 큐에 있는 작업 실행

여기서 핸들러가 나중에 트리거 된 이유를 알 수 있습니다.
핸들러의 작업은 마이크로태스크 큐에 들어가는데, 실행할 스크립트가 남아있으면(콜 스택에 작업이 남아있으면) 핸들러의 실행은 그 뒤로 미뤄집니다.
따라서 스크립트 실행이 끝난 후에 핸들러가 동작합니다.

핸들러는 비동기적으로, 핸들러끼리는 큐에 들어간 순서대로 동기적으로 실행된다고 볼 수 있습니다.


일반적인 함수는 콜 스택에, 프라미스와 관련한 비동기 작업은 마이크로태스크 큐에,
setTimeout, setInterval 등의 작업은 매크로태스크 큐에 쌓입니다.

이벤트 루프는 콜 스택이 비어있을 때 동작하여 내부 큐의 작업을 실행하도록 합니다.

프라미스 에러 핸들러를 setTimeout 으로 트리거되도록 했을 때 catch 가 아닌 unhandledrejection 이벤트 핸들러가 먼저 실행되는 것은 두 작업이 위치하는 큐가 다르기 때문입니다.

unhandledrejection 이벤트는 마이크로태스크 큐의 작업이 모두 끝나고도 거부된 프라미스가 있을 때 발생합니다. 즉, 매크로태스크 큐의 코드는 살피지 않으므로 에러가 처리되지 않고 이벤트가 발생합니다.


✅ async, await

async, await 문법을 쓰면 프라미스를 더 쉽게 사용할 수 있습니다.

  • async

async 키워드를 함수 앞에 붙이면, 그 함수는 항상 프라미스를 반환합니다.
함수가 1을 반환한다고 가정하면, 이를 resolve(1) 을 호출한 프라미스로 바꾸어 반환합니다.

async function f() {
  return 1;
}

f().then(alert); // 1

함수가 프라미스를 반환한다면, 그걸 그대로 사용하겠죠?


  • await

awaitasync 함수 안에서만 동작하는 키워드로, 프라미스가 처리될 때까지 기다리도록 합니다.
프라미스가 이행되면, 프라미스의 result 값을 반환합니다.

async function f() {

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

  // message 변수에 프라미스 객체 promise 의 result 값을 할당함 
  let message = await promise; // 프라미스가 이행될 때까지 기다림 (*)

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

f();

await 줄에서 함수 실행이 잠시 중단되었다가 프라미스가 처리되면 다시 실행을 재개합니다.
즉, await 이후의 코드는 프라미스 처리가 완료된 후에 실행됩니다.
await 가 완료된 프라미스의 결과값을 반환하므로 바로 result 를 변수에 할당할 수 있습니다.

await 는 호환성을 위해 then 메서드를 사용할 수 있는 thenable 객체를 받습니다.


에러 핸들링

await promise 에서 발생한 에러는 throw 로 에러를 던진 것과 동일한 상황입니다.

async function f() {
  await Promise.reject(new Error("에러 발생!"));
}

// 같음
async function f() {
  throw new Error("에러 발생!");
}

따라서, try..catch 를 사용하여 에러를 잡을 수 있습니다.

profile
기록용 블로그 + 오류가 있을 수 있습니다🔥

0개의 댓글