이 번역은 완료되지 않았습니다. 이 문서를 번역해 주세요.

Promise는 비동기 조작의 최종 완료나 실패를 표현해주는 객체입니다. 대부분 이미 만들어진 promise를 사용했었기 때문에 이 가이드에서는 어떻게 promise를 만드는지 설명하기에 앞서 반환된 promise의 사용에 대해 설명합니다.

기본적으로 promise는 함수에 콜백을 전달하는 대신에, 콜백을 첨부하는 방식의 객체입니다.

비동기로 음성 파일을 생성해주는  createAudioFileAsync()라는 함수가 있었다고 생각해보세요. 해당 함수는 음성 설정에 대한 정보를 받고, 두 가지 콜백 함수를 받습니다. 하나는 음성 파일이 성공적으로 생성되었을때 실행되는 콜백, 그리고 다른 하나는 에러가 발생했을때 실행되는 콜백입니다.

createAudioFileAsync()는 함수는 아래와 같이 사용됩니다.  

function successCallback(result) {
  console.log("Audio file ready at URL: " + result);
}

function failureCallback(error) {
  console.log("Error generating audio file: " + error);
}

createAudioFileAsync(audioSettings, successCallback, failureCallback);

…모던한 함수들은 위와 같이 콜백들을 전달받는 형태보다는 Promise를 반환해주기도 합니다.

만약 createAudioFileAsync() 함수가 Promise를 반환해주도록 수정해본다면, 다음과 같이 간단하게 사용되어질 수 있습니다.

createAudioFileAsync(audioSettings).then(successCallback, failureCallback);

…조금 더 간단하게 써보자면:

const promise = createAudioFileAsync(audioSettings); 
promise.then(successCallback, failureCallback);

우리는 이와 같은 것을 비동기 함수 호출이라고 부릅니다. 이런 관례는 몇 가지 장점을 갖고 있습니다. 각각에 대해 한번 살펴보도록 합시다.

Guarantees

콜백 함수를 전달해주는 고전적인 방식과는 달리, Promise는 아래와 같은 특징들이 보장됩니다.

  • 콜백은 자바스크립트 Event Loop이 현재 실행중인 콜 스택을 완료하기 이전에는 절대 호출되지 않습니다.
  • 비동기 작업이 성공하거나 실패한 뒤에 then() 을 이용하여 추가한 콜백의 경우에도 위와 같습니다.
  • then()을 여러번 사용하여 여러개의 콜백을 추가 할 수 있습니다. 그리고 각각의 콜백은 주어진 순서대로 하나 하나 실행되게 됩니다.

Promise의 가장 뛰어난 장점 중의 하나는 chaining입니다.

Chaining

보통 하나나 두 개 이상의 비동기 작업을 순차적으로 실행해야 하는 상황을 흔히 보게 됩니다. 순차적으로 각각의 작업이 이전 단계 비동기 작업이 성공하고 나서 그 결과값을 이용하여 다음 비동기 작업을 실행해야 하는 경우를 의미합니다. 우리는 이런 상황에서 promise chain을 이용하여 해결하기도 합니다.

then() 함수는 새로운 promise를 반환합니다. 처음에 만들었던 promise와는 다른 새로운 promise입니다.

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

또는

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

두 번째 promise는 doSomething() 뿐만 아니라 successCallback or failureCallback 의 완료를 의미합니다. 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);

모던한 방식으로 접근한다면, 우리는 콜백 함수들을 반환된 promise에 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

문서 태그 및 공헌자

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