[JS] 프로미스(Promise)는 어떻게 동작할까?

📄 Promise란?

자바스크립트를 사용한 프로젝트에서 데이터를 페칭한다면 데이터를 받아오기도 전에 마치 데이터를 받아온 것처럼 화면에 데이터가 표시되고 에러가 발생합니다.

이는 자바스크립트의 이벤트루프가 동작하여 동기적으로 코드가 실행되기 때문입니다.

필요한 데이터를 모두 받아온 후 나머지 로직이 실행되게 만들고 싶을 때, 즉 로직을 비동기로 동작하게 할 때 Promise를 사용합니다.

Promise는 자바스크립트 ES6부터 추가된 내장 객체로 비교적 최근에 생성되었습니다.

📄 promise 처리 흐름

1. 프로미스가 생성자를 통해 Pending(대기) 상태로 생성된다.

const myPromise = new Promise((resolve, reject) => {}); //pending

2. Promise의 executor(resolve, reject)함수의 인자를 통해 순서를 제어한다.

resolve()가 실행되는 경우 → 프로미스가 fulfilled(이행) 상태가 됩니다.

const myPromise = new Promise((resolve, reject) => {
  // pending상태
  // ... 비동기적인 상황이 되는 처리가 벌어짐.
  resolve(); // fulfilled
});

reject()가 실행되는 경우 → 프로미스가 rejected(거부) 상태가 됩니다.

const myPromise = new Promise((resolve, reject) => {
  // pending상태
  reject(); //rejected
});


프로미스가 실행되는 조건에 따라 이후 실행되는 로직을 결정할 수 있습니다.

  • resolve()가 실행되는 경우 → fulfilled 상태가 되어 .then 콜백 함수 실행
  • reject()가 실행되는 경우 → rejected 상태가 되어 .catch 콜백 함수 실행
myPromise
    .then((result) => {
      console.log("성공:", result);
    })
    .catch((error) => {
      console.error("실패:", error);
    });


reject() 함수를 이용해 구체적인 에러핸들링도 가능합니다. 보통 reject 함수를 실행하여 rejected 되는 이유를 넘기는데, 표준 내장 객체인 Error의 생성자를 이용해 Error 객체를 만들 수 있습니다.

reject(new Error('bad'));
catch((error) => {...}); 

3. 최종으로 실행되는 로직의 경우 finally를 설정해 실행한다.

프로미스 객체가 fulfilled(이행)되거나 rejected 되고 나서 가장 마지막으로 실행되어야 하는 작업이 있다면 finally 콜백함수를 설정합니다.

myPromise
    .then((result) => {
      console.log("성공:", result);
    })
    .catch((error) => {
      console.error("실패:", error);
    })
    .finally(() => {
      console.log("finally 블록 실행: 작업 종료 후 항상 실행됩니다.");
    });


📄 Callback vs Promise

콜백함수도 비동기 작업을 할 수 있습니다.
콜백 함수를 사용할 때 함수가 많아질 경우 유명한 콜백 지옥에 빠지게 되어 한눈에 봐도 불안한 모습이 됩니다.

function c(callback) {
  setTimeout(() => {
    callback();
  }, 1000);
}

c(() => {
  console.log("1000ms 후에 callback 함수가 실행됩니다.");
});

c(() => {
  c(() => {
    c(() => {
      console.log("3000ms 후에 callback 함수가 실행됩니다");
    });
  });
}); //callback hell (콜백 지옥)


이런 경우 프로미스 체이닝 (Promise Chaning)을 통해 콜백 지옥을 해결 할 수 있습니다. 콜백 함수보다 가독성이 좋습니다.

function p() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve();
    }, 1000);
  });
}

p()
  .then(() => {
    return p(); // 다시 새로운 프로미스 객체를 만들어서 리턴한다.
  })
  .then(() => p())
  .then(p)
  .then(() => {
    console.log("4000ms 후에 fulfilled 됩니다.");
  });


📄 여러개의 Promise(프로미스) 객체를 다루는 경우

여러개의 프로미스를 다룰 때는 배열을 통해 다룰 수 있습니다.

1. Promise.all

프로미스 객체들을 배열에 저장해 순차적으로 실행하는 경우에 Promise.all을 사용합니다.
Promise.all은 모든 프로미스가 fulfilled 되었을 경우, 각 프로미스의 결과를 배열에 담아 리턴합니다.
Promise.all에 전달되는 프로미스 중 하나라도 거부되면, Promise.all이 반환하는 프로미스는 에러와 함께 바로 거부됩니다.

function fetchData(url) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        const data = `${url}에서 가져온 데이터`;
        resolve(data);
      }, Math.random() * 2000); // 랜덤한 시간 후에 데이터를 반환
    });
  }

// 프로미스 객체들을 배열로 저장
const promises = [
  fetchData('https://example.com/data1'),
  fetchData('https://example.com/data2'),
  fetchData('https://example.com/data3')
];

try {
	const results = await Promise.all(promises);
  console.log("모든 데이터를 성공적으로 가져왔습니다:", results);
} catch (error) {
  console.error("데이터를 가져오는 도중 오류가 발생했습니다:", error);
}


2. Promise.allSettled

Promise.all의 프로미스 객체들이 하나라도 실패했을 경우 바로 프로미스 체인을 중단한다면, Promise.allSettled 는 이행/실패 여부와 상관없이 모든 프로미스 객체를 실행합니다.
이후 각 프로미스의 결과에 대한 정보를 담은 배열을 리턴합니다. 여러개의 작업을 병렬로 실행하고 각 작업의 성공 또는 실패 여부를 알아야 할 때 유용합니다.

  • 응답이 성공할 경우
    {status:"fulfilled", value:result}
    
  • 에러가 발생한 경우
    {status:"rejected", reason:error}
    


promise.allSettled 는 다음과 같이 활용할 수 있습니다.

let urls = [
  "https://api.github.com/users/iliakan",
  "https://api.github.com/users/Violet-Bora-Lee",
  "https://no-such-url",
];

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}`);
    }
  });
});


3. Promise.race

Promise.race 는 가장 먼저 종료(fullfilled 또는 rejected)된 프로미스 객체의 결과 또는 에러를 반환합니다.

Promise.race([promise1, promise2, promise3])
  .then(result => {
    console.log('가장 빠른 프로미스 결과:', result);
  })
  .catch(error => {
    console.error('가장 빠른 프로미스 에러:', error);
  });


📝 정리

  1. 동기적으로 실행되는 작업을 비동기적으로 만들 때 Promise를 사용해 제어할 수 있다.
  2. 순서 뿐만 아니라 fullfilled / rejected 콜백 함수를 통해 조건 제어도 가능하다

    성공시) pending → fullfilled → then → finally

    실패시) pending → rejected → catch → finally

  3. 프로미스를 사용하면 콜백 지옥을 해결 할 수 있다.
  4. 여러개의 프로미스를 다루는 것도 가능하다
    1. Promise.all ⇒ 하나의 프로미스라도 실패할 경우 프로미스 체이닝 중단
    2. Promise.allSettled ⇒ 프로미스 개별의 성공/실패 여부와 상관없이, 모든 프로미스를 실행시키고 결과를 반환
    3. Promise.race ⇒ 프로미스 개별의 성공/실패 여부와 상관없이, 가장 먼저 실행되는 프로미스 순으로 결과를 반환

출처

Leave a comment