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

then 에 넘겨지는 인자는 선택적(optional)입니다. 그리고 catch(failureCallback) 는 then(null, failureCallback) 의 축약입니다. 이 표현식을 화살표 함수로 나타내면 다음과 같습니다.

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

중요: 반환값이 반드시 있어야 합니다, 만약 없다면 콜백 함수가 이전의 promise의 결과를 받지 못합니다.
(화살표 함수 () => x는 () => {return x;}와 같습니다).

Chaining after a catch

chain에서 작업이 실패한 후에도 새로운 작업을 수행하는 것이 가능하며 매우 유용합니다. (예 : catch) 다음 예를 읽으십시오:

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

그러면 다음 텍스트가 출력됩니다.

Initial
Do that
Do this, whatever happened before

참고: "Do this" 텍스트가 출력되지 않은 것을 주의깊게 보십시오. "Something failed" 에러가 rejection을 발생시켰기 때문입니다.

Error propagation

'콜백 지옥'에서 failureCallback이 3번 발생한 것을 기억 할 것입니다. promise chain에서는 단 한 번만 발생하는것과 비교되죠.

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

기본적으로 promise chain은 예외가 발생하면 멈추고 chain의 아래에서 catch를 찾습니다. 이것은 동기 코드가 어떻게 동작 하는지 모델링 한 것입니다.

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

비동기 코드를 사용한 이러한 대칭성은 ECMAScript 2017에서 async/await 구문(Syntactic sugar) 에서 최고로 느낄 수 있습니다.

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

이것은 promise를 기반으로 합니다. doSomething()은 이전 함수와 같습니다. 문법은 이곳에서 확인 할 수 있습니다.

Promise는 모든 오류를 잡아내어, 예외 및 프로그래밍 오류가 발생해도 콜백 지옥의 근본적인 결함을 해결합니다. 이는 비동기 작업의 기능 구성에 필수적입니다.

Promise rejection events

Promise가 reject될 때마다 두 가지 이벤트 중 하나가 전역 범위로 전송됩니다 (일반적으로 이것은 window이거나 웹 작업자에서 사용되는 경우 Worker 또는 다른 작업자 기반 인터페이스). 두 가지 사건은 다음과 같습니다 :

rejectionhandled
executor의 reject함수에 의해 reject가 처리 된 후 primise가 reject 될 때 보냅니다.
unhandledrejection
promise가 reject되었지만 사용할 수 있는 reject 헨들러가 없을 때 전송됩니다.

두 경우 모두 PromiseRejectionEvent 유형의 이벤트는 reject된 promise를 나타내는 promise 속성을 멤버로 가지며 reason promise가 reject된 이유를 제공하는 속성을 가집니다

이를 통해 promise에 대한 대체 오류 처리 기능을 제공하고 promise 관리와 관련된 문제를 디버깅 할 수 있습니다. 이러한 핸들러는 컨텍스트마다 전역이므로 모든 오류는 소스에 관계없이 동일한 이벤트 핸들러로 이동합니다.

특히 유용한 사례 : 일반적으로 Node.js에 대한 코드를 작성할 때 프로젝트에 포함 된 모듈이 처리되지 않은 promise를 처리하지 못합니다. 이것들은 노드 런타임에 의해 콘솔에 기록됩니다. 다음과 같이 unhandledrejection_event.unhandledrejection 이벤트에 대한 핸들러를 추가하여 코드로 분석하고 처리하도록 캡쳐하거나 출력을 혼란스럽게하는 것을 피할 수 있습니다.

window.addEventListener("unhandledrejection", event => {
  /* You might start here by adding code to examine the
     promise specified by event.promise and the reason in
     event.reason */

  event.preventDefault();
}, false);

이벤트의 preventDefault () 메서드를 호출하면 reject 된 promise가 처리되지 않을 때 JavaScript 런타임이 기본 동작을 수행하지 않도록 지시합니다. 이 기본 동작은 대개 콘솔에 오류를 기록하는 작업을 포함하며 노드에 해당합니다.

물론 이상적으로는 reject된 promis를 검토하여 이러한 이벤트를 막기전에 실제 코드에 버그가 없는지 확인해야합니다.

오래된 콜백 API를 사용하여 Promise만들기

Promise는 생성자를 사용하여 처음부터 생성 될 수 있습니다. 이것은 오래된 API를 감쌀 때만 필요합니다.

이상적인 프로그래밍 세계에서는 모든 비동기 함수는 promise을 반환해야 하지만. 불행히도 일부 API는 여전히 success 및 / 또는 failure 콜백을 전달하는 방식일거라 생각합니다.. 예를 들면 setTimeout () 함수가 있습니다.

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

예전 스타일의 콜백과 Promise를 합치는것 문제가 있습니다. 함수 saySomething()이 실패하거나 프로그래밍 오류가 있으면 아무 것도 잡아 내지 않습니다. setTimeout의 문제점 입니다.

다행히도 우리는 setTimeout을 Promise로 감쌀 수 있습니다. 가장 좋은 방법은 가능한 가장 낮은 수준에서 문제가되는 함수를 래핑 한 다음 다시는 직접 호출하지 않는 것입니다.

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

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

기본적으로 promise constructor는 promise를 직접 해결하거나 reject 할 수 있는 실행자 함수를 사용합니다. setTimeout()은 함수에서 fail이 일어나거나 error가 발생하지 않기 때문에 이 경우 reject를 사용하지 않습니다.

Composition

Promise.resolve ()Promise.reject ()는 각각 이미 resolve되거나 reject 된 promise를 여러분이 직접 생성하기위한 바로 가기입니다. 가끔 유용하게 사용됩니다.

Promise.all()Promise.race()는 비동기 작업을 병렬로 실행하기위한 두 가지 구성 도구입니다.

우리는 병렬로 작업을 시작하고 모든 작업이 다음과 같이 끝날 때까지 기다릴 수 있습니다.

Promise.all([func1(), func2(), func3()])
.then(([result1, result2, result3]) => { /* use result1, result2 and result3 */ });

Sequential composition is possible using some clever JavaScript:

고급진 JavaScript를 사용하여 순차적 구성이 가능합니다.

[func1, func2, func3].reduce((p, f) => p.then(f), Promise.resolve())
.then(result3 => { /* use result3 */ });

기본적으로 Promise.resolve().then(func1).then(func2).then(func3);과 같은 약속 체인으로 비동기 함수 배열을 축소합니다.

이것은 재사용 가능한 작성 기능으로 만들 수 있는데, 이는 함수형 프로그래밍에서 일반적인 방식입니다.

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

composeAsync() 함수는 여러 함수를 인수로 받아들이고 composition 파이프 라인을 통해 전달되는 초기 값을 허용하는 새 함수를 반환합니다.

const transformData = composeAsync(func1, func2, func3);
const result3 = transformData(data);

ECMAScript 2017에서는 async / await를 사용하여 순차적 구성을보다 간단하게 수행 할 수 있습니다.

let result;
for (const f of [func1, func2, func3]) {
  result = await f(result);
}
/* use last result (i.e. result3) */

Timing

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

놀라움(역자 주. 에러가 난다거나, 코드가 문제가 생긴다거나..했을때의 그 놀라움..)을 피하기 위해 then()에 전달 된 함수는 already-resolved promise에 있는 경우에도 동기적으로 호출되지 않습니다.

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

즉시 실행되는 대신 전달 된 함수는 마이크로 태스크 대기열에 저장됩니다. 즉, 자바 스크립트 이벤트 루프의 현재 실행이 끝날 때 대기열이 비면 나중에 실행됩니다.

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

Nesting

역자 주. 아래부분에 대한 번역이 미흡합니다. 원문을 참고해 주세요

간단한 promise 체인은 중첩이 부주의 한 구성의 결과 일 수 있으므로 중첩하지 않고 평평하게 유지하는 것이 가장 좋습니다. common mistakes를 참조하십시오.

(Simple promise chains are best kept flat without nesting, as nesting can be a result of careless composition. See common mistakes.)

중첩은 catch 문 범위를 제한하는 제어 구조입니다. 특히, 중첩 된 catch는 중첩 된 범위 외부의 체인에있는 오류가 아닌 범위 및 그 이하의 오류 만 잡습니다. 올바르게 사용하면 오류 복구에서보다 정확한 결과를 얻을 수 있습니다.

doSomethingCritical()
.then(result => doSomethingOptional(result)
  .then(optionalResult => doSomethingExtraNice(optionalResult))
  .catch(e => {})) // Ignore if optional stuff fails; proceed.
.then(() => moreCriticalStuff())
.catch(e => console.log("Critical failure: " + e.message));

여기에 있는 선택적 단계는 들여 쓰기가 아닌 중첩되어 있지만 주위의 바깥 쪽 ( 및 ) 의 규칙적이지 않은 배치를 하지않도록 조심하세요.

inner neutralizing catch 문은 doSomethingOptional()doSomethingExtraNice()에서 발생한 오류를 catch 한 후에 코드가 moreCriticalStuff()로 다시 시작됩니다. 중요하게도 doSomethingCritical()이 실패하면 해당 오류는 최종 (외부) catch에 의해서만 포착됩니다.

Common mistakes

promise chains을 작성할 때 주의해야 할 몇 가지 일반적인 실수는 다음과 같습니다. 이러한 실수 중 몇 가지는 다음 예제에서 나타납니다.

// Bad example! Spot 3 mistakes!

doSomething().then(function(result) {
  doSomethingElse(result) // Forgot to return promise from inner chain + unnecessary nesting
  .then(newResult => doThirdThing(newResult));
}).then(() => doFourthThing());
// Forgot to terminate chain with a catch!

첫 번째 실수는 제대로 체인을 연결하지 않는 것입니다. 이것은 우리가 새로운 promise를 만들었지 만 그것을 반환하는 것을 잊었을때 일어납니다. 결과적으로 체인이 끊어 지거나 오히려 두 개의 독립적 인 체인이 경쟁하게됩니다. 즉, doFourthThing()doSomethingElse() 또는 doThirdThing()이 완료 될 때까지 기다리지 않고 의도하지 않은 것처럼 병렬로 실행됩니다. 별도의 체인은 별도의 오류 처리 기능을 가지고있어서 잡기 어려운 오류가 발생합니다.

두 번째 실수는 불필요하게 중첩되어 첫 번째 실수를 가능하게 만드는 것입니다. 중첩은 내부 오류 처리기의 범위를 제한합니다. 의도하지 않은 경우 에러가 캐치되지 않는 오류가 발생할 수 있습니다. 이 변형은 promise constructor anti-pattern입니다.이 패턴은 이미 약속을 사용하는 코드를 감싸기 위해 promise 생성자의 중복 사용과 중첩을 결합합니다.

세 번째 실수는 catch로 체인을 종료하는 것을 잊는 것입니다. 약속되지 않은 약속 체인은 대부분의 브라우저에서 예상하지 못한 약속 거부를 초래합니다.

좋은 경험 법칙은 항상 약속의 사슬을 반환하거나 종결하는 것이며, 새로운 약속을 얻 자마자 즉각적으로 돌려서 물건을 평평하게하는 것입니다.

doSomething()
.then(function(result) {
  return doSomethingElse(result);
})
.then(newResult => doThirdThing(newResult))
.then(() => doFourthThing())
.catch(error => console.log(error));

() => x 은  () => { return x; }.의 축약형임을 참고하세요

이제는 적절한 오류 처리 기능을 갖춘 결정성있는 단일 체인이 있습니다.

async/await를 사용하면 대부분의 문제를 해결할 수 있습니다. 이러한 문법의 가장 흔한 실수는 await키워드를 빼먹는 것입니다.

See also

문서 태그 및 공헌자

태그: 
최종 변경자: eojina,