자바스크립트 프로미스: 소개

프로미스는 지연된 비동기 계산을 단순화합니다. 프로미스는 아직 완료되지 않은 작업을 나타냅니다.

제이크 아치볼드
제이크 아치볼드

개발자 여러분, 웹 개발 역사에서 매우 중요한 순간을 준비하세요.

[드럼 시작]

JavaScript에 프로미스가 추가되었습니다.

[불꽃놀이와 반짝이는 종이가 하늘에서 내리고 관중들이 열광함]

이때 여러분은 다음 카테고리 중 하나에 속합니다.

  • 사람들이 당신 주변에서 환호하고 있지만 그 호들갑이 무엇을 의미하는지 모릅니다. '약속'이 무엇인지조차 모를 수도 있습니다. 어깨를 으쓱하지만 반짝이는 종이가 어깨를 짓누릅니다. 그렇다면 걱정하지 마세요. 이 분야에 관심을 가져야 하는 이유를 이해하는 데 저도 오랜 시간이 걸렸습니다. 아마도 처음부터 하고 싶을 것입니다.
  • 기뻐하며 주먹을 들어올립니다! 때가 된 것 같죠? 지금까지는 이러한 Promise를 사용해 본 적이 있지만 모든 구현의 API가 약간 달라서 마음에 듭니다. 공식 JavaScript 버전을 위한 API란 무엇인가요? 용어부터 시작하는 것이 좋습니다
  • 당신은 그것에 대해 이미 알고 있고 그들에게 뉴스인 것처럼 흥분한 사람들을 비웃습니다. 잠시 시간을 내어 우월감을 갖고 바로 API 참조를 살펴보세요.

브라우저 지원 및 폴리필

브라우저 지원

  • 32
  • 12
  • 29
  • 8

소스

완전한 프로미스 구현이 없는 브라우저를 사양 준수까지 구현하거나 다른 브라우저 및 Node.js에 프로미스를 추가하려면 폴리필(2k gzip)을 확인하세요.

무슨 호들갑입니까?

JavaScript는 단일 스레드이므로 두 스크립트를 동시에 실행할 수 없으며 차례로 실행해야 합니다. 브라우저에서 JavaScript는 스레드를 브라우저마다 다른 다른 항목 로드와 공유합니다. 하지만 일반적으로 JavaScript는 그리기, 스타일 업데이트, 사용자 작업 처리 (예: 텍스트 강조표시, 양식 컨트롤 상호작용)와 동일한 큐에 있습니다. 이러한 작업 중 하나가 활동하면 다른 작업이 지연됩니다.

인간은 멀티스레드입니다. 여러 손가락으로 입력할 수 있으며 운전하면서 동시에 대화할 수 있습니다 처리해야 하는 유일한 차단 기능은 재채기이며, 재채기를 하는 동안 모든 현재 활동을 정지해야 합니다. 이는 특히 운전하면서 대화를 나눌 때 매우 성가십니다. 재채기와 같은 코드를 작성하고 싶지 않을 것입니다.

여러분은 아마 이 문제를 해결하기 위해 이벤트와 콜백을 사용했을 것입니다. 이벤트는 다음과 같습니다.

var img1 = document.querySelector('.img-1');

img1.addEventListener('load', function() {
  // woo yey image loaded
});

img1.addEventListener('error', function() {
  // argh everything's broken
});

재채기가 나는 소리는 아니에요. 이미지를 가져오고 몇 개의 리스너를 추가하면 JavaScript는 이 리스너 중 하나가 호출될 때까지 실행을 중지할 수 있습니다.

안타깝게도 위 예에서는 이벤트 리슨을 시작하기 전에 이벤트가 발생했을 수 있으므로 이미지의 'complete' 속성을 사용하여 이 문제를 해결해야 합니다.

var img1 = document.querySelector('.img-1');

function loaded() {
  // woo yey image loaded
}

if (img1.complete) {
  loaded();
}
else {
  img1.addEventListener('load', loaded);
}

img1.addEventListener('error', function() {
  // argh everything's broken
});

이는 이벤트를 수신 대기하기 전에 오류가 발생한 이미지를 포착하지 않습니다. 안타깝게도 DOM은 그런 방법을 제공하지 않습니다. 또한 이미지 1개를 로드하고 있습니다. 이미지 집합이 로드된 시점을 알고 싶다면 훨씬 더 복잡해집니다.

이벤트가 최선의 방법이 아님

이벤트는 동일한 객체(keyup, touchstart 등)에서 여러 번 발생할 수 있는 작업에 적합합니다. 이러한 이벤트를 사용하면 리스너를 연결하기 전에 발생한 상황에 관해 크게 신경 쓰지 않아도 됩니다. 그러나 비동기 성공/실패의 경우 이상적으로 다음과 같은 작업을 해야 합니다.

img1.callThisIfLoadedOrWhenLoaded(function() {
  // loaded
}).orIfFailedCallThis(function() {
  // failed
});

// and…
whenAllTheseHaveLoaded([img1, img2]).callThis(function() {
  // all loaded
}).orIfSomeFailedCallThis(function() {
  // one or more failed
});

이것이 promise가 하는 일이지만 더 나은 이름을 사용합니다. HTML 이미지 요소에 프로미스를 반환하는 'ready' 메서드가 있는 경우 다음을 수행할 수 있습니다.

img1.ready()
.then(function() {
  // loaded
}, function() {
  // failed
});

// and…
Promise.all([img1.ready(), img2.ready()])
.then(function() {
  // all loaded
}, function() {
  // one or more failed
});

가장 기본적인 프로미스는 다음을 제외하면 이벤트 리스너와 약간 비슷합니다.

  • 프로미스는 한 번만 성공 또는 실패할 수 있습니다. 두 번 성공하거나 실패할 수 없으며, 성공에서 실패로 또는 그 반대로 전환될 수도 없습니다.
  • 프로미스가 성공 또는 실패한 후에 성공/실패 콜백을 추가하면 이벤트가 더 일찍 발생한 경우에도 올바른 콜백이 호출됩니다.

이는 비동기 성공/실패에 매우 유용합니다. 사용자는 무언가를 사용할 수 있게 된 정확한 시간에 관심이 적고 결과에 대한 응답에 더 관심이 많기 때문입니다.

프로미스(Promise) 용어

Domenic Denicola는 이 문서의 초안을 읽고 용어를 'F'로 채점했습니다. 저를 감옥에 넣고 States and Fates를 100번 흉내내고 부모님께 걱정 편지를 썼습니다. 그럼에도 불구하고 여전히 많은 용어가 혼동됩니다. 여하튼 기본적인 용어는 다음과 같습니다.

Promise의 특징은 다음과 같습니다.

  • fulfillment - 프로미스와 관련된 작업이 성공했습니다.
  • rejected - 프로미스와 관련된 작업이 실패했습니다.
  • pending: 아직 처리되거나 거부되지 않았습니다.
  • settled - 처리되거나 거부되었습니다.

사양에서도 thenable이라는 용어를 사용하여 then 메서드를 포함한다는 점에서 프로미스와 유사한 객체를 설명합니다. 이 용어를 보면 영국 축구 감독인 테리 베나블스가 떠올라서 최대한 적게 사용하겠습니다.

JavaScript로 된 프로미스 도입

프로미스는 다음과 같은 라이브러리 형식으로 한동안 사용되어 왔습니다.

위 프로미스와 JavaScript 프로미스는 Promises/A+라는 일반적인 표준화된 동작을 공유합니다. jQuery 사용자인 경우 Deferreds라는 유사한 동작을 사용합니다. 하지만 Deferreds는 Promise/A+와 호환되지 않아 미묘하게 다르고 덜 유용하므로 주의해야 합니다. jQuery에도 Promise 유형이 있지만 이는 Deferred의 하위 집합에 불과하며 동일한 문제가 있습니다.

프로미스 구현은 표준화된 동작을 따르지만 전체 API는 다릅니다. JavaScript 프로미스는 API에서 RSVP.js와 유사합니다. 프로미스를 만드는 방법은 다음과 같습니다.

var promise = new Promise(function(resolve, reject) {
  // do a thing, possibly async, then…

  if (/* everything turned out fine */) {
    resolve("Stuff worked!");
  }
  else {
    reject(Error("It broke"));
  }
});

프로미스 생성자는 단일 인수, 즉자리 결정과 거부, 두 개의 매개변수를 갖는 콜백을 사용합니다. 콜백 내에서 작업(예: 비동기 작업)을 수행한 다음 모든 것이 작동하면 resolve를 호출하고 그렇지 않으면 cancel을 호출합니다.

이전 일반 자바스크립트의 throw와 마찬가지로 Error 객체를 사용하여 거부하는 것이 관례이지만 필수는 아닙니다. Error 객체는 스택 트레이스를 캡처하므로 디버깅 도구를 더 유용하게 만드는 이점이 있습니다.

이 Promise를 사용하는 방법은 다음과 같습니다.

promise.then(function(result) {
  console.log(result); // "Stuff worked!"
}, function(err) {
  console.log(err); // Error: "It broke"
});

then()는 성공 사례의 콜백과 실패 사례의 콜백 등 두 인수를 사용합니다. 둘 다 선택사항이므로 성공 사례에 대한 콜백 또는 실패 사례에 대한 콜백만 추가할 수 있습니다.

JavaScript 프로미스는 DOM에서 'Futures'로 시작하고 'Promises'로 이름을 바꾼 후 최종적으로 JavaScript로 이동합니다. DOM이 아닌 JavaScript로 하는 것이 좋습니다. 이러한 컨텍스트는 Node.js와 같이 브라우저가 아닌 JS 컨텍스트에서 사용할 수 있기 때문입니다. 즉, 핵심 API에서 JS 컨텍스트를 사용하는지 여부는 또 다른 문제입니다.

DOM은 JavaScript 기능이지만 사용하는 것을 두려워하지 않습니다. 실제로 비동기 성공/실패 메서드를 사용하는 모든 새 DOM API는 프로미스를 사용합니다. 이 기능은 할당량 관리, 글꼴 로드 이벤트, ServiceWorker, 웹 MIDI, 스트림 등에서 이미 발생하고 있습니다.

다른 라이브러리와의 호환성

JavaScript promise API는 then() 메서드가 있는 모든 항목을 promise와 유사 (또는 promise-speak sigh에서 thenable)로 취급하므로 Q 프로미스를 반환하는 라이브러리를 사용해도 괜찮습니다. 새 자바스크립트 프로미스와 잘 호환됩니다.

하지만 앞서 언급했듯이 jQuery의 Deferreds는 다소 유용하지 않습니다. 다행히 표준 프로미스로 캐스팅할 수 있으며 가능한 한 빨리 실행하는 것이 좋습니다.

var jsPromise = Promise.resolve($.ajax('/whatever.json'))

여기서 jQuery의 $.ajax는 Deferred를 반환합니다. then() 메서드가 있으므로 Promise.resolve()가 이 메서드를 자바스크립트 프로미스로 변환할 수 있습니다. 그러나 다음과 같이 deferred가 복수의 인수를 콜백에 전달하는 경우도 있습니다.

var jqDeferred = $.ajax('/whatever.json');

jqDeferred.then(function(response, statusText, xhrObj) {
  // ...
}, function(xhrObj, textStatus, err) {
  // ...
})

반면 JS 프로미스는 첫 번째를 제외한 모든 것을 무시합니다.

jsPromise.then(function(response) {
  // ...
}, function(xhrObj) {
  // ...
})

고맙게도 이는 일반적으로 개발자가 원하는 것이거나, 적어도 원하는 것에 대한 액세스 권한을 제공합니다. 또한 jQuery는 Error 객체를 거부로 전달하는 규칙을 따르지 않습니다.

복잡한 비동기 코드를 더 쉽게 만들기

이제 몇 가지를 코딩해 보겠습니다. 다음을 수행하려 한다고 가정해 보겠습니다.

  1. 스피너를 시작하여 로드 표시
  2. 스토리의 JSON을 가져와 제목과 각 장의 URL을 제공합니다.
  3. 페이지에 제목 추가
  4. 각 챕터 가져오기
  5. 페이지에 스토리 추가
  6. 스피너 중지

... 도중에 문제가 발생한 경우에도 사용자에게 알립니다. 이때 스피너도 정지해야 합니다. 그러지 않으면 스피너가 계속 회전하고 어지럽고 다른 UI에 충돌할 수 있습니다.

물론 HTML로 제공하는 것이 더 빠르므로 JavaScript를 사용하여 스토리를 전달하지 않지만 API를 처리할 때는 흔히 사용되는 패턴입니다. 다중 데이터 가져오기를 수행하고 완료되면 작업을 수행합니다.

먼저 네트워크에서 데이터를 가져오는 것을 다루겠습니다.

XMLHttpRequest 프로미스화

이전 API는 이전 버전과 호환되는 방식으로 가능한 경우 프로미스를 사용하도록 업데이트됩니다. XMLHttpRequest가 주요 후보이지만 그동안 GET 요청을 하는 간단한 함수를 작성해 보겠습니다.

function get(url) {
  // Return a new promise.
  return new Promise(function(resolve, reject) {
    // Do the usual XHR stuff
    var req = new XMLHttpRequest();
    req.open('GET', url);

    req.onload = function() {
      // This is called even on 404 etc
      // so check the status
      if (req.status == 200) {
        // Resolve the promise with the response text
        resolve(req.response);
      }
      else {
        // Otherwise reject with the status text
        // which will hopefully be a meaningful error
        reject(Error(req.statusText));
      }
    };

    // Handle network errors
    req.onerror = function() {
      reject(Error("Network Error"));
    };

    // Make the request
    req.send();
  });
}

이제 이를 사용해 보겠습니다.

get('story.json').then(function(response) {
  console.log("Success!", response);
}, function(error) {
  console.error("Failed!", error);
})

이제 XMLHttpRequest를 수동으로 입력하지 않고도 HTTP 요청을 할 수 있습니다. XMLHttpRequest의 격렬한 카멜 표기법을 볼 필요가 적을수록 인생이 더 행복해지기 때문입니다.

체이닝

then()는 스토리의 끝이 아닙니다. then를 함께 연결하여 값을 변환하거나 추가 비동기 작업을 차례로 실행할 수 있습니다.

값 변환

새 값을 반환하여 값을 변환할 수 있습니다.

var promise = new Promise(function(resolve, reject) {
  resolve(1);
});

promise.then(function(val) {
  console.log(val); // 1
  return val + 2;
}).then(function(val) {
  console.log(val); // 3
})

실제적인 예로 다시 돌아가 보겠습니다.

get('story.json').then(function(response) {
  console.log("Success!", response);
})

응답은 JSON이지만 현재 일반 텍스트로 수신 중입니다. get 함수를 변경하여 JSON responseType를 사용하도록 할 수 있지만 Promise로 해결할 수도 있습니다.

get('story.json').then(function(response) {
  return JSON.parse(response);
}).then(function(response) {
  console.log("Yey JSON!", response);
})

JSON.parse()는 단일 인수를 사용하고 변환된 값을 반환하므로 단축어를 만들 수 있습니다.

get('story.json').then(JSON.parse).then(function(response) {
  console.log("Yey JSON!", response);
})

실제로 getJSON() 함수를 매우 쉽게 만들 수 있습니다.

function getJSON(url) {
  return get(url).then(JSON.parse);
}

getJSON()는 URL을 가져온 후 응답을 JSON으로 파싱하는 프로미스를 여전히 반환합니다.

비동기 작업을 큐에 추가

then를 연결하여 비동기 작업을 순서대로 실행할 수도 있습니다.

then() 콜백에서 무언가를 반환하는 것은 마술과 약간 비슷합니다. 값을 반환하면 그 값을 사용하여 다음 then()이 호출됩니다. 그러나 promise와 유사한 것을 반환하면 그다음 then()는 계속 대기하고 promise가 해결 (성공/실패)될 때만 호출됩니다. 예를 들면 다음과 같습니다.

getJSON('story.json').then(function(story) {
  return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
  console.log("Got chapter 1!", chapter1);
})

여기서 story.json에 비동기 요청을 하여 요청할 URL 집합이 제공되면 첫 번째 URL을 요청합니다. 이때부터 프로미스가 간단한 콜백 패턴에서 빛을 발하기 시작합니다.

더 간단한 방법으로 챕터를 가져올 수도 있습니다.

var storyPromise;

function getChapter(i) {
  storyPromise = storyPromise || getJSON('story.json');

  return storyPromise.then(function(story) {
    return getJSON(story.chapterUrls[i]);
  })
}

// and using it is simple:
getChapter(0).then(function(chapter) {
  console.log(chapter);
  return getChapter(1);
}).then(function(chapter) {
  console.log(chapter);
})

getChapter가 호출될 때까지 story.json를 다운로드하지 않지만 다음에 getChapter가 호출될 때 스토리 프로미스를 재사용하므로 story.json를 한 번만 가져옵니다. 프로미스!

오류 처리

앞서 살펴봤듯이 then()는 성공과 실패에 대한 인수 (또는 promise-speak에서 처리 및 거부) 등 두 인수를 사용합니다.

get('story.json').then(function(response) {
  console.log("Success!", response);
}, function(error) {
  console.log("Failed!", error);
})

catch()를 사용할 수도 있습니다.

get('story.json').then(function(response) {
  console.log("Success!", response);
}).catch(function(error) {
  console.log("Failed!", error);
})

catch()에 대한 특별한 것은 없습니다. then(undefined, func)를 위한 설탕에 불과하지만 가독성은 더 높습니다. 위의 두 코드 예는 동일하게 동작하지 않으며 후자는 다음과 동일합니다.

get('story.json').then(function(response) {
  console.log("Success!", response);
}).then(undefined, function(error) {
  console.log("Failed!", error);
})

차이는 미묘하지만 매우 유용합니다. 프로미스 거부는 거부 콜백 (또는 동등한 것이므로 catch())을 사용하여 다음 then()로 건너뜁니다. then(func1, func2)를 사용하면 func1 또는 func2가 호출되며 둘 다 호출되지 않습니다. 그러나 then(func1).catch(func2)를 사용하면 둘 다 체인에서 별도의 단계이므로 func1가 거부되면 둘 다 호출됩니다. 다음을 살펴보세요.

asyncThing1().then(function() {
  return asyncThing2();
}).then(function() {
  return asyncThing3();
}).catch(function(err) {
  return asyncRecovery1();
}).then(function() {
  return asyncThing4();
}, function(err) {
  return asyncRecovery2();
}).catch(function(err) {
  console.log("Don't worry about it");
}).then(function() {
  console.log("All done!");
})

위의 흐름은 일반 자바스크립트 try/catch와 매우 유사합니다. 'try'를 사용하여 발생하는 오류는 즉시 catch() 블록으로 이동합니다. 다음은 이를 플로 차트로 나타낸 것입니다 (제가 플로 차트를 좋아하기 때문).

promise는 파란색 선을 따르고 프로미스의 경우에는 적색선을 따릅니다.

JavaScript 예외 및 프로미스

프로미스가 명시적으로 거부될 때 거부가 발생하며, 생성자 콜백에서 오류가 발생하는 경우에도 암시적으로 거부됩니다.

var jsonPromise = new Promise(function(resolve, reject) {
  // JSON.parse throws an error if you feed it some
  // invalid JSON, so this implicitly rejects:
  resolve(JSON.parse("This ain't JSON"));
});

jsonPromise.then(function(data) {
  // This never happens:
  console.log("It worked!", data);
}).catch(function(err) {
  // Instead, this happens:
  console.log("It failed!", err);
})

즉, 프로미스 생성자 콜백 내에서 프로미스와 관련된 모든 작업을 실행하는 것이 유용하므로 오류는 자동으로 포착되고 거부됩니다.

then() 콜백에서 발생하는 오류도 마찬가지입니다.

get('/').then(JSON.parse).then(function() {
  // This never happens, '/' is an HTML page, not JSON
  // so JSON.parse throws
  console.log("It worked!", data);
}).catch(function(err) {
  // Instead, this happens:
  console.log("It failed!", err);
})

실제 오류 처리

스토리와 챕터에서 catch를 사용하여 사용자에게 오류를 표시할 수 있습니다.

getJSON('story.json').then(function(story) {
  return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
  addHtmlToPage(chapter1.html);
}).catch(function() {
  addTextToPage("Failed to show chapter");
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
})

story.chapterUrls[0] 가져오기에 실패하면 (예: http 500 또는 사용자가 오프라인인 경우) 응답을 JSON으로 파싱하려고 시도하는 getJSON()의 성공 콜백을 포함하여 이후의 모든 성공 콜백을 건너뛰고 페이지에 Cha1.html을 추가하는 콜백도 건너뜁니다. 대신 catch 콜백으로 이동합니다. 따라서 이전 작업 중 하나라도 실패하면 'Failed to show section'이 페이지에 추가됩니다.

JavaScript의 try/catch처럼 오류가 포착되고 후속 코드가 계속 실행되므로 스피너는 원하는 대로 항상 숨겨집니다. 위 버전이 다음의 비차단 비동기 버전이 됩니다.

try {
  var story = getJSONSync('story.json');
  var chapter1 = getJSONSync(story.chapterUrls[0]);
  addHtmlToPage(chapter1.html);
}
catch (e) {
  addTextToPage("Failed to show chapter");
}
document.querySelector('.spinner').style.display = 'none'

오류 복구 없이 로깅 목적으로 단순히 catch()하는 것이 좋습니다. 이렇게 하려면 오류를 다시 발생시키면 됩니다. getJSON() 메서드에서 이를 실행할 수 있습니다.

function getJSON(url) {
  return get(url).then(JSON.parse).catch(function(err) {
    console.log("getJSON failed for", url, err);
    throw err;
  });
}

챕터 1개를 가져왔지만, 모든 장을 가져오려고 합니다. 한번 해 보죠.

동시 로드 및 시퀀싱: 둘 다 최대한 활용

비동기를 생각하는 것은 쉽지 않습니다. 시작하는 데 어려움을 겪고 있다면 동기 코드인 것처럼 코드를 작성해 보세요. 이 경우에는 다음과 같습니다.

try {
  var story = getJSONSync('story.json');
  addHtmlToPage(story.heading);

  story.chapterUrls.forEach(function(chapterUrl) {
    var chapter = getJSONSync(chapterUrl);
    addHtmlToPage(chapter.html);
  });

  addTextToPage("All done");
}
catch (err) {
  addTextToPage("Argh, broken: " + err.message);
}

document.querySelector('.spinner').style.display = 'none'

맞습니다! 하지만 동기화 상태이며 다운로드되는 동안 브라우저를 잠급니다. 이를 비동기로 작동하게 하려면 then()를 사용하여 차례로 발생하게 합니다.

getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  // TODO: for each url in story.chapterUrls, fetch & display
}).then(function() {
  // And we're all done!
  addTextToPage("All done");
}).catch(function(err) {
  // Catch any error that happened along the way
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  // Always hide the spinner
  document.querySelector('.spinner').style.display = 'none';
})

하지만 챕터 URL을 반복하고 순서대로 가져오려면 어떻게 해야 할까요? 이 방법은 작동하지 않습니다.

story.chapterUrls.forEach(function(chapterUrl) {
  // Fetch chapter
  getJSON(chapterUrl).then(function(chapter) {
    // and add it to the page
    addHtmlToPage(chapter.html);
  });
})

forEach는 비동기를 인식하지 않으므로 챕터는 다운로드 순서대로 표시되며 이는 기본적으로 펄프 픽션이 작성된 방식입니다. 펄프 픽션이 아니므로 수정해 봅시다

시퀀스 만들기

chapterUrls 배열을 프로미스 시퀀스로 변환하려고 합니다. then()를 사용하면 됩니다.

// Start off with a promise that always resolves
var sequence = Promise.resolve();

// Loop through our chapter urls
story.chapterUrls.forEach(function(chapterUrl) {
  // Add these actions to the end of the sequence
  sequence = sequence.then(function() {
    return getJSON(chapterUrl);
  }).then(function(chapter) {
    addHtmlToPage(chapter.html);
  });
})

Promise.resolve()는 이번이 처음입니다. 이 메서드는 지정된 값으로 확인되는 프로미스를 만듭니다. Promise의 인스턴스를 전달하면 해당 인스턴스가 반환됩니다 (참고: 이는 일부 구현에서 아직 따르지 않는 사양의 변경사항입니다). promise와 같은 항목 (then() 메서드 포함)을 전달하면 동일한 방식으로 처리/거부하는 정품 Promise가 생성됩니다. 다른 값(예: Promise.resolve('Hello')이면 해당 값으로 처리하는 프로미스를 만듭니다. 위와 같이 값 없이 호출하면 'undefined'를 사용하여 처리합니다.

제공된 값 (또는 undefined)을 사용하여 거부되는 프로미스를 만드는 Promise.reject(val)도 있습니다.

array.reduce를 사용하여 위의 코드를 정리할 수 있습니다.

// Loop through our chapter urls
story.chapterUrls.reduce(function(sequence, chapterUrl) {
  // Add these actions to the end of the sequence
  return sequence.then(function() {
    return getJSON(chapterUrl);
  }).then(function(chapter) {
    addHtmlToPage(chapter.html);
  });
}, Promise.resolve())

이전 예와 동일한 작업을 하지만 별도의 'sequence' 변수가 필요하지 않습니다. 배열의 각 항목에 대해 축소 콜백이 호출됩니다. 'sequence'는 처음에는 Promise.resolve()이지만 나머지 호출에서는 이전 호출에서 반환된 값입니다. array.reduce는 배열을 단일 값(이 경우에는 프로미스)으로 축소하는 데 매우 유용합니다.

지금까지의 내용을 종합해 보겠습니다.

getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  return story.chapterUrls.reduce(function(sequence, chapterUrl) {
    // Once the last chapter's promise is done…
    return sequence.then(function() {
      // …fetch the next chapter
      return getJSON(chapterUrl);
    }).then(function(chapter) {
      // and add it to the page
      addHtmlToPage(chapter.html);
    });
  }, Promise.resolve());
}).then(function() {
  // And we're all done!
  addTextToPage("All done");
}).catch(function(err) {
  // Catch any error that happened along the way
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  // Always hide the spinner
  document.querySelector('.spinner').style.display = 'none';
})

거기에 동기 버전의 완전 비동기 버전이 있습니다. 하지만 더 잘할 수 있습니다. 현재 페이지가 다음과 같이 다운로드되고 있습니다.

브라우저는 한 번에 여러 항목을 다운로드하는 데 매우 능숙하므로 장을 차례로 다운로드하면 성능이 저하됩니다. 모든 장을 동시에 다운로드한 다음 모두 도착했을 때 처리하는 것이 목표입니다. 다행히 이를 위한 API가 있습니다.

Promise.all(arrayOfPromises).then(function(arrayOfResults) {
  //...
})

Promise.all는 프로미스 배열을 사용하여 모든 프로미스가 성공적으로 완료될 때 처리하는 프로미스를 만듭니다. 전달한 프로미스와 동일한 순서로 결과 배열 (프로미션이 처리한 항목)을 가져옵니다.

getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  // Take an array of promises and wait on them all
  return Promise.all(
    // Map our array of chapter urls to
    // an array of chapter json promises
    story.chapterUrls.map(getJSON)
  );
}).then(function(chapters) {
  // Now we have the chapters jsons in order! Loop through…
  chapters.forEach(function(chapter) {
    // …and add to the page
    addHtmlToPage(chapter.html);
  });
  addTextToPage("All done");
}).catch(function(err) {
  // catch any error that happened so far
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
})

연결에 따라 이는 하나씩 로드하는 것보다 몇 초 빠를 수 있으며 첫 번째 시도보다 코드가 적습니다. 장은 어떤 순서로든 다운로드할 수 있지만 화면에 올바른 순서로 표시됩니다.

하지만 인지되는 성능을 개선할 수는 있습니다. 1장이 도착하면 페이지에 추가해야 합니다 이렇게 하면 사용자는 나머지 장이 도착하기 전에 읽기를 시작할 수 있습니다. 3장이 도착하면 사용자가 2장이 누락되었음을 인식할 수 없으므로 페이지에 추가하지 않습니다. 2장이 도착하면 2장, 3장 등을 추가할 수 있습니다.

이렇게 하려면 모든 장의 JSON을 동시에 가져온 다음 시퀀스를 만들어 문서에 추가합니다.

getJSON('story.json')
.then(function(story) {
  addHtmlToPage(story.heading);

  // Map our array of chapter urls to
  // an array of chapter json promises.
  // This makes sure they all download in parallel.
  return story.chapterUrls.map(getJSON)
    .reduce(function(sequence, chapterPromise) {
      // Use reduce to chain the promises together,
      // adding content to the page for each chapter
      return sequence
      .then(function() {
        // Wait for everything in the sequence so far,
        // then wait for this chapter to arrive.
        return chapterPromise;
      }).then(function(chapter) {
        addHtmlToPage(chapter.html);
      });
    }, Promise.resolve());
}).then(function() {
  addTextToPage("All done");
}).catch(function(err) {
  // catch any error that happened along the way
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
})

두 가지의 장점을 모두 얻어 냈습니다. 모든 콘텐츠를 전송하는 데 동일한 시간이 소요되지만 사용자는 첫 번째 콘텐츠를 더 빨리 가져옵니다.

이 간단한 예에서는 모든 장이 거의 동시에 도착하지만 한 번에 하나씩 표시하는 것의 이점은 더 많은 챕터에서 과장됩니다.

Node.js 스타일 콜백 또는 이벤트로 위 코드를 실행하면 코드의 약 두 배이지만 따라하기가 쉽지 않습니다. 그러나 이는 promise에 관한 끝이 아닙니다. 다른 ES6 기능과 결합하면 훨씬 더 쉬워집니다.

보너스 라운드: 확장된 기능

이 문서를 처음 작성한 이후 프로미스를 사용할 수 있는 기능이 크게 확장되었습니다. Chrome 55부터 비동기 함수를 사용하면 기본 스레드를 차단하지 않고 마치 동기 함수인 것처럼 promise 기반 코드를 작성할 수 있습니다. 자세한 내용은 my async functions article를 참고하세요. 주요 브라우저에서는 프로미스 및 비동기 함수가 모두 광범위하게 지원됩니다. 자세한 내용은 MDN의 프로미스비동기 함수 참조에서 확인하세요.

이 글을 읽고 수정/제안해 주신 앤 반 케스테렌, 도메닉 데니콜라, 톰 애쉬워스, 레미 샤프, 애디 오스마니, 아서 에반스, 히라노 유타카에게 감사드립니다.

또한 이 문서의 다양한 부분을 업데이트해 주신 마티아스 비넨스에게도 감사드립니다.