자바스크립트 인터뷰 박살내기: 프러미스란?

  • 이 글은 Master the JavaScript Interview: What is a Promise?를 번역한 글입니다.

  • 아직 입문자이다보니 오역을 한 경우가 있을 수 있습니다. 양해 부탁드립니다.

  • 매끄러운 문맥을 위하여 의역을 한 경우가 있습니다. 원문의 뜻을 최대한 해치지 않도록 노력했으니 안심하셔도 됩니다.

  • 영어 단어가 자연스러운 경우 원문 그대로의 영단어를 적었습니다.

  • 저의 보충 설명은 인용문에 이탤릭체로 달았습니다.

"자바스크립트 인터뷰 박살내기"는 중간 ~ 시니어 수준의 자바스크립트 포지션에 지원하는 사람들이 만나게 될 확률이 높은 흔한 질문들을 준비할 수 있도록 고안된 포스팅 시리즈입니다. 실제로 제가 인터뷰에서 종종 사용하는 질문이기도 합니다.

Promise란?

Promise란 향후에 언젠가 사용하게 될 값을 생산해내는 객체입니다. 여기서 값은 얻을 수 있거나(resolved), 혹은 값을 얻지 못하는 대신에 그렇게 된 이유를 얻게 됩니다(rejected). 여기서 얻지 못하는 경우는 네트워크 장애 등을 들 수 있겠습니다. Promise는 다음 3가지 상태 중 하나를 가집니다. Fulfilled, Rejected, Pending 중 하나이지요. Promise를 사용할 때에, fulfilled된 값을 다루는 콜백, 또는 rejected된 이유를 다루는 콜백을 (인자로) 첨부할 수 있습니다.

  • Promise로부터 기대한 값을 얻은 경우, 이를 resolved value 또는 fulfilled value라고 부릅니다. 앞으로 해결된 값 으로 부르겠습니다.

  • Promise로부터 해결된 값을 얻지 못한 경우, 그렇게 된 이유를 rejected reason 또는 rejected value라고 부릅니다. 앞으로 거절된 이유 라고 부르겠습니다.

Promise는 매우 열정(eager)이 넘칩니다. 그말인즉슨 Promise는 자신의 생성자가 호출되기 무섭게 당신이 전달한 작업을 시작할 겁니다. 약간 느린 것을 원하신다면, observables이나 tasks를 참고해보세요.

Promise의 끝나지 않은 역사

Promise와 Future(비슷한 아이디어)의 초기 구현과 미래는 1980년대의 MultiLisp와 Concurrent Prolog와 같은 언어에서 나타나기 시작했습니다. "Promise"라는 용어는 Barbara Liskov와 Liuba Shrira가 1988년에 만들었습니다.

제가 자바스크립트에서 Promise에 대하여 처음 들었을 때, Node는 막 새로나온 물건이었고 사람들은 비동기 방식을 다루는 최선의 방법을 논하고 있었습니다. Promise를 가지고 여러가지 실험이 벌어지기도 하였지만, 결국 Node의 표준인 오류-우선 콜백 패턴을 주로 사용하게 되었습니다.

같은 시각, Dojo(Dojo Toolkit)는 Promise를 Deferred API에 추가했습니다. 계속해서 커지는 관심과 활동은 Promises/A 라는, Promise의 활용도를 높여주는 새로운 명세를 만들어냈습니다.

jQuery의 비동기 방식은 Promise로 리팩터되었습니다. jQuery의 Promise 지원은 Dojo의 Deferred와 상당히 유사한 점이 많았고, jQuery의 압도적인 인기 덕분에 가장 흔하게 사용되는 Promise 구현이 되었습니다 - 한동안은 말이죠. 하지만, jQuery의 Promise는 사람들이 Promise를 다루는 툴에 있어서 중요하게 생각하는 Fulfilled/Reject 2채널 체이닝과 예외 관리를 지원하지 않았습니다.

이러한 단점에도 불구하고 jQuery는 공식적으로 자바스크립트 Promise를 대세로 만들었고, Q, When, Bluebird와 같은 더욱 향상된 Promise 라이브러리들은 더욱 대중화되었습니다. jQuery의 구현은 호환성이 좋지 않았고, 이는 Promise에 대한 명세를 보다 명료하게 다시 작성하도록 동기를 부여했습니다. 이것은 Promises/A+ 명세의 작성으로 이어졌습니다.

ES6은 Promises/A+ 를 따르는 Promise를 표준에 추가하였고, 일부 중요한 API들은 새로운 Promise 표준을 기반으로 만들어졌습니다. 예를 들어 WHATWG Fetch 명세, Async 함수 표준(이 글을 작성하는 현재 3단계 초안)말입니다.

여기에서 설명하는 Promise는 Promises/A+ 명세에 부합하는 것으로, ECMAScript 표준 Promise 구현에 초점을 맞추도록 하겠습니다.

Promise의 작동 방식

Promise는 비동기 함수로부터 동기적으로 반환되는 객체입니다. Promise는 3가지 상태 중 하나를 가집니다:

  • Fulfilled: onFulfilled()가 호출된다 (ex) resolve() 호출)
  • Rejected: onRejected()가 호출된다 (ex) reject() 호출)
  • Pending: 아직 fulfilled 또는 rejected 상태가 아님

Promise가 pending 상태가 아니면, settled 상태라고 말합니다. 즉, resolved 또는 rejected 상태입니다. 종종 사람들은 resolvedsettled 를 같은 의미로 부르기도 합니다(pending 상태가 아님).

한번 settled 상태가 되고 나면, Promise는 다시 settled 될 수 없습니다. 즉, resolve() 또는 reject()의 호출이 아무런 효과가 없습니다. settled 상태인 Promise가 갖는 이 불변성은 아주 중요한 특징입니다.

Promise는 자신의 상태를 노출시키지 않습니다. Promise를 속이 보이지 않는 블랙 박스와 같이 다뤄야 합니다. 오직 Promise를 생성하는 함수만이 Promise의 상태에 대하여 알거나, resolvereject 콜백에 접근할 수 있습니다.

정해진 시간이 흐르고 나면 resolve를 호출하는 Promise를 반환하는 함수입니다:

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

wait(3000).then(() => console.log('Hello!')); // 'Hello!'

wait(3000)은 3000ms(3초)를 기다린 뒤, 콘솔 화면에 'Hello!'를 출력할 것입니다. 명세를 준수하는 Promise는 .then()메서드를 정의해야 하며, 해결된 값 또는 거절된 이유를 전달받을 핸들러(콜백)을 사용하려면 이 .then()이 필요합니다.

ES6의 Promise 생성자는 인자로 함수를 받습니다. 이 함수는 resolve()reject() 2개의 인자를 받습니다. 위의 예시에서는 resolve()만 사용하기 때문에, 인자 목록에서 reject()를 제외시켰습니다. 마지막으로 setTimeout()으로 대기 시간을 만든 뒤, 그 대기가 종료되고 나면 resolve()를 호출했습니다.

resolve()reject()를 호출할 때 인자로 값을 전달할 수도 있습니다. 이때 이 값은 .then()에 전달하는 콜백에서 인자로 받을 수 있습니다.

reject()에 인자를 전달하여 호출하는 경우, 나는 항상 Error 객체를 전달합니다. 흔히 2가지 종류의 해결 상태 - 평범하게 Happy한 경로, 앞선 Happy한 경로를 저지하는 그 외의 모든 경로 - 를 예측하게 되는데, Error 객체를 사용하면 이런 양상을 명시적으로 확인할 수 있습니다.

중요한 Promise 규칙들

Promise를 위한 표준은 Promises/A+ 명세 커뮤니티에서 정의되었습니다. 이 표준을 따르는 구현은 자바스크립트 표준 ECMASCript Promise를 비롯하여 많은 것들이 존재합니다.

스펙을 준수하는 Promise는 다음의 규칙들을 따릅니다:

  • Promise 또는 "thenable"은 표준에 의거한 .then() 메서드를 제공하는 객체이다.
  • pending 상태의 Promise는 fulfilled 또는 rejected 상태로 전이할 수 있다.
  • fullfilled 또는 rejected 상태는 settled 된 것으로, 다른 상태로 전이할 수 없다.
  • 한번 settled 된 Promise는 값을 가진다.(이 값은 undefined일 수 있다) 이 값은 변할 수 없다.

- Promise의 Executor(Promise 생성자에 전달되는 콜백)는 반환값을 갖지 않습니다. return을 통하여 값을 반환하더라도 이후 .then()에서 무시됩니다. .then()으로 데이터를 전달하려면 resolve()reject()를 호출할 때에 인자로 전달해야 합니다.

- resolve()를 다른 함수의 콜백으로 넘겨줘야 하는데(setTimeout 등) 이때 데이터도 함께 넘겨줘야 한다면, 익명 함수로 감싸주거나 bind()를 사용하면 됩니다.

이 맥락에서 말하는 '변한다'는 항등 연산자(===)에 대한 것을 말합니다. 해결된 값으로 객체가 사용될 수 있으며, 이때 객체의 멤버는 변할 수 있습니다(mutate).

모든 Promise는 반드시 다음의 Signature를 따르는 .then()메서드를 제공합니다.

promise.then() (
  onFulfilled?: Function,
  onRejected?: Function
) => Promise

.then 메서드는 다음의 규칙을 따릅니다:

  • onFulfilled()onRejected()는 생략 가능하다.
  • 제공된 인자가 함수 타입이 아닌 경우 무시된다.
  • onFulfilled()는 Promise가 fulfilled된 뒤 해당 Promise의 값(promise's value)을 첫번째 인자로 갖고서 호출된다.
  • onRejected()는 Promise가 rejected된 뒤 그 이유(reason)를 첫번째 인자로 갖고서 호출된다. 이유는 모든 유효한 자바스크립트 값이 사용될 수 있지만, 거절은 대부분의 경우 예외이므로, Error 객체를 사용할 것을 권한다.
  • onFulfilled()onRejected() 둘 다 단 한번만 호출될 수 있다.
  • .then()은 동일 Promise로부터 여러번 호출될 수 있다. 즉, Promise는 여러 콜백을 통합하는 데에 사용할 수 있다. (Promise Chaining)
  • .then()은 반드시 새로운 Promise, promise2를 반환해야 한다.
  • onFulfilled() 또는 onRejected()가 값 x를 반환하고, x가 Promise라면, promise2는 x와 동일시된다.(동일한 상태와 값을 가진다고 간주된다) 그렇지 않다면(x가 Promise가 아니라면), promise2는 값 x로써 fulfilled 상태가 된다.
  • onFulfilled 또는 onRejected가 예외 e를 던진다면, promise2e를 이유로서 거절되어야 한다.
  • onFulfilled가 함수가 아니고 promise1가 fulfilled 상태라면, promise2promise1과 동일한 값을 갖고서 fulfilled 상태가 되어야 한다.
  • onRejected가 함수가 아니고 promise1가 rejected 상태라면, promise2promise1와 동일한 이유로 거절되어야 한다.
  • .then()이 Promise x를 반환할 경우, 이 x는 또 하나의 비동기 작업이라고 생각하면 됩니다. 따라서 x를 생성할 때에 resolve, reject에 대한 시나리오를 콜백에서 작성해줘야 합니다.

  • .then()이 Promise가 아니라 단순한 값 x를 반환할 경우, .then()의 반환값은 x를 결과값으로 가지며 fultilled 상태인 Promise입니다.

  • .then()의 인자로 콜백이 아닌 값을 전달했다면, .then()을 호출하기 직전의 Promise를 그 상태 그대로 받습니다.

Promise 체이닝

.then()이 항상 새로운 Promise를 반환하므로, 예외 처리를 어떻게 할지 정밀하게 통제하여 Promise를 연쇄적으로 호출하는 것도 가능합니다. Promise는 평범한 동기 방식에서의 try/catch 코드를 흉내낼 수 있도록 해줍니다.

체이닝을 하면 동기 방식의 코드와 같이 순서대로 동작하는 결과를 만들 수 있습니다. 즉, 이런 식입니다:

fetch(url)
  .then(process)
  .then(save)
  .catch(handleErrors)
;

fetch(), process(), save()가 모두 Promise를 반환한다고 가정하면, process()fetch()가 완료되길 기다린 뒤에 실행할 것이고, save()process()가 완료되길 기다린 뒤에 실행할 것입니다. handleErrors()는 이전 Promise 중에서 Rejected인 경우가 있을 때에만 실행할 것입니다.

여러 개의 Rejected인 경우가 존재하는 복잡한 Promise 체이닝의 경우입니다:

const wait = time => new Promise(
  res => setTimeout(() => res(), time)
);

wait(200)
  // `onFulfilled()` 는 새로운 Promise `x`를 반환
  .then(() => new Promise(res => res('foo')))
  // 다음 Promise에서는 `x`의 상태를 그대로 받아들임
  .then(a => a)
  // 바로 위에서는 `x`의 값(`'foo'`)를 그대로 반환했다
  // 따라서 바로 위의 `.then()`은 값을 그대로 가지고서 fulfilled 상태의 Promise를 반환한다
  .then(b => console.log(b)) // 'foo'
  // null 또한 Promise의 값으로 유효하다
  .then(() => null)
  .then(c => console.log(c)) // null
  // 다음에서 예외는 보고되지 않는다:
  .then(() => {throw new Error('foo');})
  // 대신, 반환된 Promise는 Rejected 상태가 되고,
  // 함께 전달되는 오류 객체는 이유가 된다
  .then(
    // 직전의 예외 때문에 여기서는 아무 일도 일어나지 않는다:
    d => console.log(`d: ${ d }`),
    // 여기서 직전의 예외가 처리된다 (거절된 이유)
    e => console.log(e)) // [Error: foo]
  // 직전의 예외가 처리되었으므로, 계속할 수 있다:
  .then(f => console.log(`f: ${ f }`)) // f: undefined
  // 다음에서는 아무 일도 일어나지 않는다. Error e는 이미 처리되었다.
  .catch(e => console.log(e))
  .then(() => { throw new Error('bar'); })
  // Promise가 Rejected 상태이면, Resolve를 위한 .then()은 무시된다
  // 'bar' 예외로 인하여 다음에서는 아무 것도 표시되지 않는다:
  .then(g => console.log(`g: ${ g }`))
  .catch(h => console.log(h)) // [Error: bar]
;

예외 처리

Promise는 fulfilled와 rejected 모든 경우에 대하여 콜백을 가지며, 아래와 같은 코드는 적법합니다.

save().then(
  handleSuccess,
  handleError
);

하지만 handleSuccess()가 오류를 던지면 어떻게 할까요? .then()에서 반환된 Promise는 rejected 상태를 가지지만, 이를 처리해줄 함수가 아무 것도 없게 됩니다. 이말인즉슨, 앱에서 발생하는 예외가 묻힐 수도 있다는 것이죠. 오우 이런!

이런 이유로, 일부 사람들은 위의 코드를 안티-패턴으로 간주하고 다음과 같은 코드를 추천합니다:

save()
  .then(handleSuccess)
  .catch(handleError)
;

차이는 미묘하지만, 아주 중요합니다. 첫번째 예시에서는 save() 동작이 발생시킨 예외는 잘 처리되지만, handleSuccess()가 발생시킨 예외는 묻힐 것입니다.
그림 1.catch()가 없다면, handleSuccess()에서 오류가 발생하게 되면 이것은 처리될 수 없습니다.

두번째 예시에서는 .catch()save(), handleSuccess() 양쪽 어디에서든 발생한 오류를 모두 처리해줄 수 있습니다.
그림 2.catch()가 있으므로, 두 예외는 모두 처리됩니다.

물론 save()가 발생시킨 오류는 통신 장애이고, handleSuccess()가 발생시킨 오류는 개발자가 처리하길 깜빡 한 것일 공산이 크죠. 각각을 따로 처리하고 싶으면 어떻게 할까요? 그렇게 만들 수도 있습니다.

save()
  .then(
    handleSuccess,
    handleNetworkError
  )
  .catch(handleProgrammerError)
;

어느 쪽을 선호하든, 모든 Promise 체이닝을 .catch()로 끝내길 추천합니다. 중요하니까 두번 반복하죠.

모든 Promise 체이닝을 .catch()로 끝내길 추천합니다.

Promise를 취소하려면?

Promise를 처음 사용하는 사람들이 흔히 궁금해하는 것 중 하나가 Promise를 취소하는 방법입니다. 이건 어떨까요? "취소되었음" 이라는 이유로 Promise를 거절하는 겁니다. "통상적인" 오류와 다르게 처리해야 한다면, 오류 핸들러 내에서 다르게 분기 처리를 하면 됩니다.

여기서는 사람들이 각자 나름대로 Promise를 취소하고자 할 때 흔히 범하는 실수들을 보여드립니다.

Promise에 .cancel() 추가하기

.cancel()을 추가하면 Promise가 표준을 지키지 않을 뿐 아니라, Promise의 또다른 규칙을 어기게 됩니다. 오직 Promise를 생성한 함수만이 Promise를 Resolved, Rejected, Cancelled 중 하나의 상태로 만들 수 있습니다. 이런 상태를 노출시키는 것은 캡슐화를 어기는 것이고, 굳이 그럴 필요 없는 상황에서 Promise를 조작하는 코드를 만들도록 조장하는 것입니다. Promise를 망치는 스파게티 코드를 지양합시다.

뒷정리 까먹기

일부 영리한 분들은 취소를 하는 데에 Promise.race()를 활용하는 방법이 있다는 것을 깨달았습니다. 문제는, Promise를 생성한 함수 바깥으로 취소에 대한 통제가 넘어가게 되면, 이 함수 안에서 벌어졌던 timeout을 해제하거나, 데이터 참조에 대한 메모리 해제 등의 청소 작업을 제대로 할 수가 없게 된다는 것이죠.

Promise 취소가 거절되었을 때의 처리

Chrome 브라우저는 Promise 거절 처리를 하지 않으면 콘솔에서 경고 메세지를 보낸다는 것 알고 계신가요? 오우 이런!

과도한 복잡성

취소와 관련하여 철회된 TC39 제안은 취소를 위한 별도의 메세지 채널을 제안했습니다. 여기서는 취소 '토큰'이라는 새로운 컨셉도 쓰였죠. 제 생각에는, 이 해법은 Promise 명세를 너무 과하게 부풀릴 것입니다. 그리고 이 기능을 추가하면 생겨날 예상치 못한 문제는 거절과 취소의 분리입니다. 제 생각에 이런건 아예 시작하지 않는 것이 좋습니다.

우리는 예외와 취소 중에 고르고 싶어질까요? 당연히 그럴 겁니다. 그러면 그것이 Promise의 역할일까요? 제 생각엔, 아니오, 아닙니다.

Promise 취소 다시 생각해보기

보통 저는 Promise가 resolve / reject / cancel을 어떻게 수행할지에 대한 정보를 Promise가 생성되는 시점에 Promise에 전달합니다. 그러면 Promise에 .cancel() 메서드가 필요 없어집니다. 나중에 취소를 할지 말지를 Promise가 생성되는 시점에 어떻게 알 수가 있나구요?

'취소를 할지 말지 아직 모른다면, Promise를 생성할 때에 무엇을 전달할지 어떻게 알까?'

미래에 사용될 값을 대신할 수 있는 객체같은 게 있다면... 잠깐만요.

취소 여부를 표현하는 값으로 Promise를 사용할 수도 있겠군요. 그건 이렇게 생겼을 겁니다:

const wait = (
  time,
  cancel = Promise.reject()
) => new Promise((resolve, reject) => {
  const timer = setTimeout(resolve, time);
  const noop = () => {};

  cancel.then(() => {
    clearTimeout(timer);
    reject(new Error('Cancelled'));
  }, noop);
});

const shouldCancel = Promise.resolve(); // Yes, cancel
// const shouldCancel = Promise.reject(); // No cancel

wait(2000, shouldCancel).then(
  () => console.log('Hello!'),
  (e) => console.log(e) // [Error: Cancelled]
);

Promise는 '향후에 언젠가 사용하게 될 값을 만들어내는 객체'이므로, wait가 만들어내는 Promise와 cancel의 기본값 Promise 둘 다 실행 최초에는 아무런 값도 내부에 갖지 않을 것입니다. wait의 Promise는 setTimeout에 의하여 기다렸다가 실행될 것이고, cancel의 Promise는 기본값 상태가 rejected 이므로 noop 메서드에 의하여 아무 것도 실행되지 않습니다. 따라서 각 Promise에 들어있는 resolvereject 는 모두 실행되지 않죠. 결국 cancel Promise에 따라 wait Promise의 상태도 결정되는 것을 알 수 있습니다.

기본적으로는 취소가 되지 않도록 기본 인자를 세팅해줍니다. 그러면 cancel 인자를 편리하게 선택적으로 조작할 수 있죠. 그 다음, 늘 하던 대로 timeout을 설정하는데, 이번에는 timeout의 ID를 기억해서 나중에 해제할 수 있도록 합시다.

cancel.then() 메서드를 사용해서 취소 작업과 자원 정리를 처리합니다. 이 작업은 cancel이 resolved 되지 않는 이상 절대로 실행되지 않습니다. cancel의 초기 상태가 rejected이기 때문이죠.

noop() 함수의 용도가 궁금하신가요? noop은 no-op을 뜻하는 것으로, 말 그대로 아무 것도 안 한다는 의미입니다. 이게 없으면 V8이 경고 메세지: UnhandledPromiseRejectionWarning: Unhandled promise rejection을 던질 것입니다. 설령 아무 것도 할 처리가 없더라도, Promise 거절은 항상 처리하는 것이 좋습니다.

Promise 취소를 추상화하기

wait() 타이머는 잘 만들어낸 듯 합니다. 그런데 이 발상은 우리가 기억해야 하는 것들 모두를 캡슐화하는 데에 활용할 수 있습니다.

  1. 처음에는 기본값으로 cancel Promise를 거절 - cancel Promise가 전달되지 않았다면, 취소하거나 오류를 던지지 않는다.
  2. 취소를 거절했을 경우, 반드시 자원을 정리한다.
  3. onCancel이 오류를 발생시킬 수도 있으므로, 이 오류에 대한 처리도 필요하다. (위의 wait 예시에도 오류 처리가 생략되어있다.)

cancel.then() 다음에 .catch()로 예외 처리를 해주면 되겠군요.

Promise를 그대로 사용하면서도 취소가 가능한 Promise 유틸리티를 만들어봅시다. 이를 사용하면, 통신 요청과 같은 것을 제어하는 데에 활용할 수 있을 겁니다. Signature는 다음과 같습니다:

speculation(fn: SpecFunction, shouldCancel: Promise) => Promise

SpecFunction은 Promise 생성자에 전달하는 함수와 거의 같은데, onCancel() 핸들러를 취한다는 것이 다릅니다.

SpecFunction(resolve: Function, reject: Function, onCancel: Function) => Void
// 네이티브 Promise API를 고차 함수(HOF) 'speculation'이 래핑
// 이를 통하여 shouldCancel Promise와 onCancel() 콜백을 추가
const speculation = (
  fn,
  cancel = Promise.reject() // 기본값은 취소하지 않음
) => new Promise((resolve, reject) => {
  const noop = () => {};

  const onCancel = (
    handleCancel
  ) => cancel.then(
      handleCancel,
      // 예상 대로의 취소 거절 무시:
      noop
    )
    // onCancel의 오류 처리
    .catch(e => reject(e))
  ;

  fn(resolve, reject, onCancel);
});

위의 예시는 작동 방식을 간단히 설명해주는 코드로, 실제로는 예외적인 경우를 좀 더 따져야합니다. 예를 들어 이 코드에서는 speculation의 Promise가 이미 settled된 뒤에 cancel Promise를 통하여 취소를 시도해도 handleCancel이 호출됩니다.

정말 그럴까요? 위와 같은 경우가 왜 발생하는지 생각해보는 것도 재미있을 듯 합니다.

저는 이런 예외 경우를 모두 해결하여 프로덕션 레벨의 오픈소스 라이브러리 Speculation를 만들었습니다.

더 나아진 라이브러리 추상화를 활용하여 취소가 가능한 wait() 유틸리티를 다시 작성해봅시다. 우선 Speculation을 설치합니다.

npm install --save speculation

이제 모듈을 불러와서 사용할 수 있습니다.

import speculation from 'speculation';

const wait = (
  time,
  cancel = Promise.reject() // 기본값: 취소하지 않음
) => speculation((resolve, reject, onCancel) => {
  const timer = setTimeout(resolve, time);

  // onCancel을 사용하여 남아있는 자원을 정리
  // 그 다음에 reject()를 호출.
  // 여기서 거절의 이유를 전달할 수도 있습니다.
  onCancel(() => {
    clearTimeout(timer);
    reject(new Error('Cancelled'));
  });
}, cancel); // cancel을 반드시 전달해야 함!

wait(200, wait(500)).then(
  () => console.log('Hello!'),
  (e) => console.log(e)
); // 'Hello!'

wait(200, wait(50)).then(
  () => console.log('Hello!'),
  (e) => console.log(e)
); // [Error: Cancelled]

이렇게 하면 noop()에 대한 걱정, onCancel()에 대한 예외 처리, 그 외의 자잘한 예외를 신경 쓸 필요가 줄어듭니다. 그런 세세한 부분들은 speculation()을 통하여 추상화되어 처리되니까요. 한번 살펴보시고, 실제 프로젝트에서 잘 활용하시길 바랍니다.

Native Promise API와 관련된 기타 사항

네이티브 Promise 객체는 몇몇 편리한 유틸리티 메서드를 제공합니다.

  • Promise.reject(): rejected 상태의 Promise를 반환합니다.
  • Promise.resolve(): resolved 상태의 Promise를 반환합니다.
  • Promise.race(): Promise로 이루어진 배열 또는 반복자를 취하여 가장 먼저 resolve된 Promise의 값 또는 가장 먼저 reject된 Promise의 이유를 가진 Promise를 반환합니다.
  • Promise.all(): Promise로 이루어진 배열 또는 반복자를 취하여 그 안에 들어있는 모든 Promise가 resolve된 뒤의 값들 또는 가장 먼저 reject된 Promise의 이유를 가진 Promise를 반환합니다.

결론

Promise는 여러 자바스크립트 기능들 - 현대적인 AJAX 요청을 다루는 데에 사용하는 WHATWG Fetch, 비동기 코드를 동기 코드처럼 만들어주는 Async 함수 - 에서 필수적으로 사용되고 있습니다.

Async 함수는 이 글을 적는 시점에는 Stage 3 표준이지만, 제 생각에 이것은 조만간 아주 대중화될 것이고, 자바스크립트로 비동기 프로그래밍을 하는 데에서 아주 흔하게 사용되는 해법이 될 것입니다. 그말인즉슨, Promise를 제대로 감상하기 위한 공부는 가까운 미래의 자바스크립트 개발자들에게는 아주 중요할 것이라는 뜻이지요.

예를 들어, 여러분이 Redux를 사용한다면, redux-saga를 한번 살펴보세요. Redux를 사용하면서 생기는 사이드 이펙트를 다루는 데에 Async 함수에 크게 의존하고 있습니다.

숙련된 개발자들까지도, Promise가 무엇이고 어떻게 작동하는지, 그리고 어떻게 해야 잘 사용할 수 있는지에 대하여 이 글을 통하여 배우실 수 있었기를 바랍니다.