자바스크립트/개념

자바스크립트 Promise 알아보기

Jake Seo 2022. 6. 25. 18:18

개요

Promise 객체는 비동기 작업을 코드로 깔끔하게 표현하기 위해 사용되는 포장지같은 것이다. 아직 결정되지 않은 값에 대한 유연한 처리를 제공하여 비동기 코드가 잘 녹아들 수 있게 해준다.

The Promise object represents the eventual completion (or failure) of an asynchronous operation and its resulting value.

상세

PromisePromise 가 생기는 시점에는 불가피하게 알 수 없는 값(not necessarily known)에 대한 프록시이다.

이를 통해 비동기 작업의 최종 성공으로 도출되는 값이나 실패에 대한 이유 처리를 담당하는 핸들러를 구성할 수 있다.

이를 이용해 비동기 메서드를 동기 메서드처럼 사용할 수 있다. 이를테면 바로 값을 반환하는 것이 아니라 값 대신 Promise 객체를 반환하고 비동기 작업이 완료되는 시점 언젠가에 값을 주거나 특정 액션을 취하게 할 수 있다.

Promise 의 상태

Promise 의 상태는 3가지만 있다.

  • pending: fulfilled 되거나 rejected 되지 않은 초기 상태이다.
    • fulfilled: 작업이 성공적으로 완료되었다는 의미이다.
    • rejected: 작업이 실패하였다는 의미이다.

위에서 설명한대로 pendingfulfilled 되거나 rejected 될 수 있다. 이 때에 각각 미리 입력된 핸들러에 의해 사용자가 정의한 작업을 수행할 수 있다.

Promise.prototype.then()Promise.prototype.catch() 메서드도 동일하게 Promise 를 반환하기 때문에 이를 메서드 체인으로 이용해 fulfilled 되었거나 rejected 되었을 때 실행할 콜백 함수를 미리 작성해놓으면 된다.

만일 fulfilled, rejected 상관 없이 Promise 비동기 작업이 끝나면(when settled) 실행시킬 작업이 있다면, Promise.prototype.finally() 를 사용할 수도 있다.

fulfilledrejected 된 상태를 문서에서는 영어로 'settled' 라고 표현한다.

사용 예제

예제 1: 기본 사용 예제

const promise1 = new Promise((resolve, reject) => {
  resolve('Success!');
});

promise1.then((value) => {
  console.log(value);
  // expected output: "Success!"
});

resolve() 가 호출되면, Promise 내부의 비동기 작업이 성공한 것이고, then() 에서 첫번째 콜백 메서드가 호출된다.

예제 2: 메서드 체인 예제

const myPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('foo');
  }, 3000);
});

myPromise
  .then(() => console.log("A"), () => console.log("AC"))
  .then(() => console.log("B"), () => console.log("BC"))
  .then(() => console.log("C"), () => console.log("CC"));

Promise.then() 의 반환 값은 Promise 이기 때문에 위와 같은 메서드 체인도 가능하다. A, B, C 가 순서대로 출력될 것이다.

Promise 의 생성자

Promise 의 생성자는 인자로 콜백 메서드를 받는다. 콜백 메서드에는 자동으로 resolvereject 인자가 전달되는데, 이는 값이 들어있는 인자가 아니라, 성공 혹은 실패 메서드를 호출하기 위한 인자이다.

  • resolve() 메서드가 호출되면 Promise 객체가 pending 상태에서 fulfilled 상태로 변한다.
  • reject() 메서드가 호출되면 Promise 객체가 pending 상태에서 rejected 상태로 변한다.

생성자의 인자로 들어가는 콜백 메서드의 내용에 비동기 함수를 집어넣고, 성공 시 resolve() 호출, 실패 시 reject() 호출을 하면 된다.

생성자 코드 예제 1: setTimeout() 과 함께 사용하기

const promise1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('foo');
  }, 3000);
});

Promise 객체는 3초 후에 fulfilled 상태가 될 것이다.

생성자 코드 예제 2: XMLHttpRequest 객체와 함께 사용하기

function myAsyncFunction(url) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest()
    xhr.open("GET", url)
    xhr.onload = () => resolve(xhr.responseText)
    xhr.onerror = () => reject(xhr.statusText)
    xhr.send()
  });
}

위와 같이 사용하면, ajax 혹은 fetch API 와 비슷한 구현을 할 수 있다. 단, fetch API 는 Response 객체를 반환한다.

Promise 가 해결하는 문제 알아보기: 콜백 지옥

당연한 말이지만, Promise 는 그냥 탄생한 것이 아니라, 기존에 제공하던 기술의 불편함 때문에 탄생하게 되었다.

doSomething(function (result) {
  doSomethingElse(result, function (newResult) {
    doThirdThing(newResult, function (finalResult) {
      console.log("Got the final result: " + finalResult);
    }, failureCallback);
  }, failureCallback);
}, failureCallback);

기존에는 비동기 메서드를 다루는 도구가 따로 있지 않고, 그냥 위와 같이 콜백 메서드를 인자로 주는 방식이 많았다. 비동기의 결과를 다른 비동기에 활용하려고 하면, 위와 같이 콜백의 들여쓰기가 깊어져 코드를 알아보기 매우 어려워졌었다.

Promise 를 이용한 문제 해결

doSomething()
  .then(function (result) {
    return doSomethingElse(result);
  })
  .then(function (newResult) {
    return doThirdThing(newResult);
  })
  .then(function (finalResult) {
    console.log("Got the final result: " + finalResult);
  })
  .catch(failureCallback);

콜백에서 반환(return) 값으로 Promise 를 전해주어 들여쓰기가 깊어지지 않고 그냥 일반적인 코드처럼 순차적으로 한줄씩 진행하는 것과 같이 읽을 수 있어서 가독성이 좋아진다.

Promise 여러개 다루기 (Promise Composition)

한 번에 비동기 메서드를 여러개 다룰 때, 모든 응답이 다 오면 작동하는 콜백 메서드를 지정하고 싶거나 아니면 응답이 오는 순서대로 작동하는 콜백 메서드를 지정하고 싶을 때가 있을 것이다. 그럴 때는 Promise 객체에서 자체적으로 제공하는 static method 를 사용하면 된다.

Promise.all(): 모든 Promise 가 완료(settled) 되었을 때 실행하기

const promise1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve("ONE");
    }, 1000);
});

const promise2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve("TWO");
    }, 2000);
});

const promise3 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve("THREE");
    }, 3000);
});

Promise.all([promise1, promise2, promise3]).then((res) => {
    console.log(res); // ["ONE", "TWO", "THREE"]
});

위의 코드 Promise.all() 은 3초가 소요되어 가장 늦게 완료될 promise3 의 작업까지 기다렸다가, ["ONE", "TWO", "THREE"] 를 출력하게 될 것이다. 모든 작업이 완료되어야 실행되는 것이기 때문에 결과의 형태도 배열이 된다.

Promise.all() 의 인자로 준 Promise 배열에서 만일 하나의 작업이라도 reject 된다면, 에러를 던진다. (Using_promises: Uncaught (in promise)) 그래서 사실상 Promise.all() 은 배열 안의 모든 원소가 정상적으로 resolve() 될 것이라고 믿는 상태이거나, 모든 원소가 정상적으로 resolve() 될 것이라고 믿을 때에 사용하는 것이 좋다.

Promise.allSettled(): 실패해도 일단은 결과를 반환하기

Promise.allSettled() 는 기본적인 동작이 Promise.all() 과 비슷하게 모든 작업이 완료되었을 때 배열의 형태로 결과를 반환한다는 것은 같지만, 동작과 결과의 형태가 아주 약간 다르다. Promise.allSettled() 는 배열의 원소로 들어온 Promisereject() 가 일어나도 에러를 던지지 않는다. 다만 이에 따라서 결과의 형태도 약간 다르다.

[{status: 'fulfilled', value: 'ONE'}
{status: 'fulfilled', value: 'TWO'}
{status: 'rejected', reason: 'THREE'}]

위와 같이 배열 내부에 오브젝트가 있는 형태이고, 정상적으로 resolve 된 경우, statusfulfilled 라는 값이 들어오고 value 가 들어온다. reject 가 된 경우에는 rejected 가 들어오고 reason 이 들어온다.

Promise.race(): 가장 빠른 Promise가 완료(settled) 되었을 때 실행하기

const promise1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve("ONE");
    }, 1000);
});

const promise2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve("TWO");
    }, 2000);
});

const promise3 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve("THREE");
    }, 3000);
});

Promise.race([promise1, promise2, promise3]).then((res) => {
    console.log(res); // ONE
});

위의 코드 Promise.race() 는 가장 빠르게 완료되는 promise1 이 완료될 때 ONE 을 출력한다. 가장 빠르게 완료되는 하나만 처리하기 때문에 결과도 이전의 Promise.all() 과는 다르게 배열이 아닌 스칼라 값을 출력한다.

자주 저지르는 실수

// Bad example! Spot 3 mistakes!

doSomething()
  .then(function (result) {
    // (*) 포인트 1: Forgot to return promise from inner chain + unnecessary nesting
    doSomethingElse(result)
      .then((newResult) => doThirdThing(newResult));
  })
  .then(() => doFourthThing());
// (*) 포인트 2: Forgot to terminate chain with a catch!
  • 위의 주석으로 표기해놓은 포인트1 에서는 2가지 실수가 있다.
    • Promise 를 반환(return)하지도 않아서 체인이 제대로 이루어지지 않을 것이다.
    • Promise 를 반환하면서 메서드 체인으로 잇는 것이 좋은데, 굳이 중첩된 코드를 작성했다.
  • 마지막 .then() 만 해줄 것이 아니라 catch() 로 체인을 마무리 지어주는 것이 좋다.

레퍼런스

반응형