번역 작업 진행중입니다.

Promise는 비동기 조작의 최종 완료 또는 실패를 나타내는 객체입니다. 대부분의 사람들이 이미 만들어진 promises를 사용했었기 때문에 이 가이드에서는 어떻게 promise를 만드는지 설명하기 전에 반환된 promises의 소비를 먼저 설명합니다.

본질적으로 promise는 콜백을 함수에 넘기는 대신 붙인 콜백에서 반환된 객체라고 보면됩니다.

예) 두 가지 콜백이 필요한 예전 스타일의 함수 대신 마지막에 성공 혹은 실패하면 그 중 하나를 호출합니다.

function successCallback(result) {
  console.log("It succeeded with " + result);
}

function failureCallback(error) {
  console.log("It failed with " + error);
}

doSomething(successCallback, failureCallback);

…현대스타일(?) 함수는 promise를 반환하지만 대신 콜백을 연결할 수 있습니다.

const promise = doSomething(); 
promise.then(successCallback, failureCallback);

…혹은 더 단순히:

doSomething().then(successCallback, failureCallback);

우리는 이것을 비동기함수 호출이라고 부릅니다. 이 협약(convention)에는 몇 가지 이점이 있습니다. 우리는 각각을 알아볼 것입니다.

Guarantees

구닥다리 방식의 콜백과 달리 promise는 몇가지를 보장합니다.

  • 콜백은 자바스크립트 event loop의 completion of the current run이전에는 절대 호출되지 않습니다.
  • 위와같이 비동기 동작이 성공하거나 실패한 이후에도 .then으로 추가된 콜백이 호출됩니다.
  • .then을 여러 번 호출하여 여러 콜백을 추가 할 수 있으며 삽입 순서에 독립적으로 실행될 수 있습니다.

하지만 promise에서 바로 확인할 수 있는 이점은 채이닝(chaining)입니다.

Chaining

일반적으로 연속적인 두 개 이상의 비동기 작업을 실행하여 이전 작업이 성공하면 각 후속 작업이 시작되고 이전 작업의 결과가 반환할 필요가 있습니다. 우리는 이를 promise chan을 이용해 만들수 있습니다.

짜자잔~ then 함수가 새로운 promise를 반환합니다! 기존과는 다르죠.

const promise = doSomething();
const promise2 = promise.then(successCallback, failureCallback);

또는

const promise2 = doSomething().then(successCallback, failureCallback);

이 두 번째 promise는 doSomething() 뿐만 아니라 successCallback or failureCallback 의 완료를 나타내며 promise를 반환하는 다른 비동기 함수가 될 수 있습니다. 이 경우 promise2에 추가 된 콜백은 successCallback또는 failureCallback에 의해 반환 된 promise 뒤에 대기합니다.

기본적으로, 각각의 promise는 체인 안에서 서로 다른 비동기 단계의 완료를 나타냅니다.

예전에는 여러 비동기 작업을 연속적으로 수행하면 고전적인 콜백 피라미드의 생성으로 이어졌습니다:

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

With modern functions, we attach our callbacks to the returned promises instead, forming a promise chain:

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

The arguments to then are optional, and catch(failureCallback) is short for then(null, failureCallback). You might see this expressed with arrow functions instead:

doSomething()
.then(result => doSomethingElse(result))
.then(newResult => doThirdThing(newResult))
.then(finalResult => {
  console.log(`Got the final result: ${finalResult}`);
})
.catch(failureCallback);

Important: Always return results, otherwise callbacks won't catch the result of a previous promise.

Chaining after a catch

It's possible to chain after a failure, i.e. a catch, which is useful to accomplish new actions even after an action failed in the chain. Read the following example:

new Promise((resolve, reject) => {
    console.log('Initial');

    resolve();
})
.then(() => {
    throw new Error('Something failed');
        
    console.log('Do this');
})
.catch(() => {
    console.log('Do that');
})
.then(() => {
    console.log('Do this whatever happened before');
});

This will output the following text:

Initial
Do that
Do this whatever happened before

Note that the text Do this is not outputted because the Something failed error caused a rejection.

Error propagation

You might recall seeing failureCallback three times in the pyramid of doom earlier, compared to only once at the end of the promise chain:

doSomething()
.then(result => doSomethingElse(result))
.then(newResult => doThirdThing(newResult))
.then(finalResult => console.log(`Got the final result: ${finalResult}`))
.catch(failureCallback);

Basically, a promise chain stops if there's an exception, looking down the chain for catch handlers instead. This is very much modeled after how synchronous code works:

try {
  const result = syncDoSomething();
  const newResult = syncDoSomethingElse(result);
  const finalResult = syncDoThirdThing(newResult);
  console.log(`Got the final result: ${finalResult}`);
} catch(error) {
  failureCallback(error);
}

This symmetry with synchronous code culminates in the async/await syntactic sugar in ECMAScript 2017:

async function foo() {
  try {
    const result = await doSomething();
    const newResult = await doSomethingElse(result);
    const finalResult = await doThirdThing(newResult);
    console.log(`Got the final result: ${finalResult}`);
  } catch(error) {
    failureCallback(error);
  }
}

It builds on promises, e.g. doSomething() is the same function as before. You can read more about the syntax here.

Promises solve a fundamental flaw with the callback pyramid of doom, by catching all errors, even thrown exceptions and programming errors. This is essential for functional composition of asynchronous operations.

Creating a Promise around an old callback API

A Promise can be created from scratch using its constructor. This should be needed only to wrap old APIs.

In an ideal world, all asynchronous functions would already return promises. Alas, some APIs still expect success and/or failure callbacks to be passed in the old way. The quintessential example is the setTimeout() function:

setTimeout(() => saySomething("10 seconds passed"), 10000);

Mixing old-style callbacks and promises is problematic. If saySomething fails or contains a programming error, nothing catches it.

Luckily we can wrap it in a promise. Best practice is to wrap problematic functions at the lowest possible level, and then never call them directly again:

const wait = ms => new Promise(resolve => setTimeout(resolve, ms));

wait(10000).then(() => saySomething("10 seconds")).catch(failureCallback);

Basically, the promise constructor takes an executor function that lets us resolve or reject a promise manually. Since setTimeout doesn't really fail, we left out reject in this case.

Composition

Promise.resolve() and Promise.reject() are shortcuts to manually create an already resolved or rejected promise respectively. This can be useful at times.

Promise.all() and Promise.race() are two composition tools for running asynchronous operations in parallel.

Sequential composition is possible using some clever JavaScript:

[func1, func2].reduce((p, f) => p.then(f), Promise.resolve());

Basically, we reduce an array of asynchronous functions down to a promise chain equivalent to: Promise.resolve().then(func1).then(func2);

This can also be done with a reusable compose function, which is common in functional programming:

const applyAsync = (acc,val) => acc.then(val);
const composeAsync = (...funcs) => x => funcs.reduce(applyAsync, Promise.resolve(x));

The composeAsync function will accept any number of functions as arguments, and will return a new function that accepts an initial value to be passed through the composition pipeline. This is beneficial because any or all of the functions may be either asynchronous or synchronous, and they are guaranteed to be executed in the correct order:

const transformData = composeAsync(func1, asyncFunc1, asyncFunc2, func2);
transformData(data);

In ECMAScript 2017, sequential composition can be done more simply with async/await:

for (const f of [func1, func2]) {
  await f();
}

Timing

To avoid surprise, functions passed to then will never be called synchronously, even with an already-resolved promise:

Promise.resolve().then(() => console.log(2));
console.log(1); // 1, 2

Instead of running immediately, the passed-in function is put on a microtask queue, which means it runs later when the queue is emptied at the end of the current run of the JavaScript event loop, i.e. pretty soon:

const wait = ms => new Promise(resolve => setTimeout(resolve, ms));

wait().then(() => console.log(4));
Promise.resolve().then(() => console.log(2)).then(() => console.log(3));
console.log(1); // 1, 2, 3, 4

See also

문서 태그 및 공헌자

태그: 
 이 페이지의 공헌자: SSJ-unclear, jadestern, limkukhyun
 최종 변경: SSJ-unclear,