Promise
Promise
Promise 객체는 비동기 연산의 최종 완료(또는 실패)와 그 결과 값을 나타낸다. promise가 동작하는 방식과 사용법을 배우려면 먼저 "Using promises" 문서를 읽어 보는 것이 좋다.
본문
개요
Promise는 promise가 생성될 때 아직 알 수 없는 값에 대한 프록시(proxy)이다. 비동기 작업의 최종 성공 값이나 실패 이유에 대해 핸들러를 연결할 수 있게 해 준다. 이를 통해 비동기 메서드가 동기 메서드처럼 값을 반환할 수 있다. 즉, 최종 값을 즉시 반환하는 대신, 비동기 메서드는 나중에 어떤 시점에 값을 제공하겠다는 promise를 반환한다.
Promise는 다음 상태 중 하나에 있게 된다:
- pending: 초기 상태. fulfilled도 rejected도 아니다.
- fulfilled: 연산이 성공적으로 완료되었음을 의미한다.
- rejected: 연산이 실패했음을 의미한다.
pending promise의 최종 상태는 값으로 fulfilled되거나 이유(오류)로 rejected될 수 있다. 두 경우 중 하나가 발생하면 promise의 then 메서드에 의해 대기열에 들어간 연관 핸들러들이 호출된다. 이미 fulfilled 또는 rejected된 promise에 해당 핸들러를 연결해도 핸들러는 호출되므로, 비동기 연산의 완료와 핸들러 연결 사이에 경쟁 조건(race condition)이 없다.
promise가 fulfilled 또는 rejected 상태(즉 pending이 아닌 상태)에 있으면 settled(결정됨) 되었다고 말한다.
또한 promise와 관련해 resolved라는 용어도 듣게 될 것이다. 이는 promise가 settled되었거나 다른 promise의 최종 상태와 일치하도록 "고정(locked-in)"되었으며, 더 이상 resolve하거나 reject해도 효과가 없음을 의미한다. 구어적으로 "resolved" promise는 종종 "fulfilled" promise와 동등하지만, resolved promise는 pending이나 rejected일 수도 있다. 예를 들어:
new Promise((resolveOuter) => {
resolveOuter(
new Promise((resolveInner) => {
setTimeout(resolveInner, 1000);
}),
);
});
이 promise는 생성 시점에 이미 resolved되었다(resolveOuter가 동기적으로 호출되기 때문). 하지만 다른 promise로 resolve되었으므로, 내부 promise가 fulfilled되는 1초 뒤까지는 fulfilled되지 않는다. 실제로 "resolution"은 종종 배경에서 이루어져 관찰할 수 없으며, 관찰되는 것은 fulfillment와 rejection뿐이다.
참고: 몇몇 다른 언어들도 지연 평가(lazy evaluation)와 연산 지연 메커니즘을 가지며 이를 "promise"라고 부른다(예: Scheme). JavaScript의 promise는 이미 진행 중인 프로세스를 나타내며 콜백 함수로 연결될 수 있다. 표현식을 지연 평가하고 싶다면 인자가 없는 함수(예: f = () => expression)를 사용해 지연 평가되는 표현식을 만들고, f()로 즉시 평가하는 것을 고려해 보라.
Promise 자체에는 취소를 위한 일급(first-class) 프로토콜이 없지만, 보통 AbortController를 사용해 기본 비동기 연산을 직접 취소할 수 있다.
체이닝된 Promise(Chained Promises)
promise 메서드 then(), catch(), finally()는 settled된 promise에 추가 동작을 연결하는 데 사용된다. then() 메서드는 최대 두 개의 인자를 받는다. 첫 번째 인자는 promise의 fulfilled 경우에 대한 콜백 함수이고, 두 번째 인자는 rejected 경우에 대한 콜백 함수이다. catch()와 finally() 메서드는 내부적으로 then()을 호출하며 오류 처리를 덜 장황하게 만든다. 예를 들어 catch()는 사실상 fulfillment 핸들러를 전달하지 않는 then()일 뿐이다. 이 메서드들이 promise를 반환하므로 체이닝할 수 있다. 예를 들어:
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("foo");
}, 300);
});
myPromise
.then(handleFulfilledA, handleRejectedA)
.then(handleFulfilledB, handleRejectedB)
.then(handleFulfilledC, handleRejectedC);
다음과 같은 용어를 사용하겠다: initial promise(초기 promise)는 then이 호출되는 promise이고, new promise(새 promise)는 then이 반환하는 promise이다. then에 전달된 두 콜백은 각각 fulfillment handler와 rejection handler라고 부른다.
초기 promise의 settled 상태가 어떤 핸들러를 실행할지 결정한다:
- 초기 promise가 fulfilled되면 fulfillment handler가 fulfillment 값과 함께 호출된다.
- 초기 promise가 rejected되면 rejection handler가 rejection 이유와 함께 호출된다.
핸들러의 완료 상태가 새 promise의 settled 상태를 결정한다:
- 핸들러가 thenable 값을 반환하면 새 promise는 반환된 값과 같은 상태로 settle된다.
- 핸들러가 thenable이 아닌 값을 반환하면 새 promise는 반환된 값으로 fulfilled된다.
- 핸들러가 오류를 throw하면 새 promise는 throw된 오류로 rejected된다.
- 초기 promise에 해당 핸들러가 연결되지 않으면 새 promise는 초기 promise와 같은 상태로 settle된다. 즉, rejection handler가 없으면 rejected promise는 같은 이유로 계속 rejected 상태를 유지한다.
예를 들어 위 코드에서 myPromise가 reject되면 handleRejectedA가 호출되고, handleRejectedA가 정상적으로 완료되면(throw하거나 rejected promise를 반환하지 않으면) 첫 번째 then이 반환하는 promise는 계속 rejected 상태로 남는 대신 fulfilled된다. 따라서 오류를 즉시 처리해야 하지만 체인 아래로 오류 상태를 유지하고 싶다면 rejection handler에서 어떤 타입의 오류든 throw해야 한다. 반면 즉각적인 필요가 없다면 최종 catch() 핸들러까지 오류 처리를 미룰 수 있다.
myPromise
.then(handleFulfilledA)
.then(handleFulfilledB)
.then(handleFulfilledC)
.catch(handleRejectedAny);
콜백 함수에 화살표 함수를 사용하면 promise 체인의 구현은 다음과 같아진다:
myPromise
.then((value) => `${value} and bar`)
.then((value) => `${value} and bar again`)
.then((value) => `${value} and again`)
.then((value) => `${value} and again`)
.then((value) => {
console.log(value);
})
.catch((err) => {
console.error(err);
});
참고: 더 빠른 실행을 위해 모든 동기 동작은 가급적 하나의 핸들러 안에서 수행해야 한다. 그렇지 않으면 모든 핸들러를 순서대로 실행하는 데 여러 tick이 걸릴 수 있다.
JavaScript는 작업 큐(job queue)를 유지한다. JavaScript는 매번 큐에서 작업을 하나 골라 완료될 때까지 실행한다. 작업들은 Promise() 생성자의 executor, then에 전달된 핸들러, 또는 promise를 반환하는 플랫폼 API에 의해 정의된다. 체인의 promise들은 이 작업들 사이의 의존 관계를 나타낸다. promise가 settle되면 그와 연관된 핸들러들이 작업 큐의 맨 뒤에 추가된다.
하나의 promise는 둘 이상의 체인에 참여할 수 있다. 다음 코드에서 promiseA의 fulfillment는 handleFulfilled1과 handleFulfilled2를 모두 작업 큐에 추가하게 한다. handleFulfilled1이 먼저 등록되었으므로 먼저 호출된다.
const promiseA = new Promise(myExecutorFunc);
const promiseB = promiseA.then(handleFulfilled1, handleRejected1);
const promiseC = promiseA.then(handleFulfilled2, handleRejected2);
이미 settled된 promise에 동작을 할당할 수도 있다. 이 경우 그 동작은 즉시 작업 큐의 맨 뒤에 추가되며 기존 작업들이 모두 완료된 후 수행된다. 따라서 이미 "settled"된 promise에 대한 동작은 현재 동기 코드가 완료되고 최소 한 번의 루프 tick이 지난 뒤에야 발생한다. 이는 promise 동작이 비동기임을 보장한다.
const promiseA = new Promise((resolve, reject) => {
resolve(777);
});
// At this point, "promiseA" is already settled.
promiseA.then((val) => console.log("asynchronous logging has val:", val));
console.log("immediate logging");
// produces output in this order:
// immediate logging
// asynchronous logging has val: 777
Thenables
JavaScript 생태계는 promise가 언어의 일부가 되기 훨씬 전부터 여러 promise 구현을 만들어 왔다. 내부적으로 다르게 표현되지만, 최소한 모든 Promise 유사 객체는 Thenable 인터페이스를 구현한다. thenable은 .then() 메서드를 구현하며, 이 메서드는 두 콜백(하나는 promise가 fulfilled될 때, 하나는 rejected될 때)과 함께 호출된다. Promise도 thenable이다.
기존 promise 구현과 상호 운용하기 위해, 언어는 promise 대신 thenable을 사용하는 것을 허용한다. 예를 들어 Promise.resolve는 promise뿐만 아니라 thenable도 추적한다.
// This is not a Promises/A+ compliant thenable! It calls onFulfilled
// synchronously. For demonstration only.
const thenable = {
then(onFulfilled, onRejected) {
onFulfilled({
// The thenable is fulfilled with another thenable
then(onFulfilled, onRejected) {
onFulfilled(42);
},
});
},
};
Promise.resolve(thenable); // A promise fulfilled with 42
then() 메서드는 제공된 onFulfilled와 onRejected 콜백의 실행을 스케줄링하는 책임이 있다. 오류 처리와 비동기를 포함한 그 의미론은 Promises/A+ 명세에 정확히 정의되어 있으므로 여기서 반복하지 않는다. thenable을 직접 구현해야 하는 경우는 매우 드물다. 네이티브 promise를 사용하지 않더라도 아마 Bluebird 같은 Promise 라이브러리를 사용할 것이다.
Promise 동시성(Promise concurrency)
Promise 클래스는 비동기 작업 동시성을 돕는 네 가지 주요 정적 메서드를 제공한다:
Promise.all()— 모든 promise가 fulfilled되면 fulfilled되고, 하나라도 rejected되면 rejected된다.Promise.allSettled()— 모든 promise가 settle되면 fulfilled된다.Promise.any()— 어느 하나라도 fulfilled되면 fulfilled되고, 모두 rejected되면 rejected된다.Promise.race()— 어느 하나라도 settle되면 settle된다. 즉 어느 하나라도 fulfilled되면 fulfilled되고, 어느 하나라도 rejected되면 rejected된다.
이 메서드들은 모두 promise(정확히는 thenable)의 iterable을 받아 새 promise를 반환한다. 모두 서브클래싱을 지원하며, 이는 Promise의 하위 클래스에서 호출될 수 있고 결과는 하위 클래스 타입의 promise가 된다는 뜻이다. 그러려면 하위 클래스의 생성자가 Promise() 생성자와 동일한 시그니처(resolve와 reject 콜백을 파라미터로 호출할 수 있는 단일 executor 함수를 받는)를 구현해야 한다. 하위 클래스는 또한 Promise.resolve()처럼 호출되어 값을 promise로 resolve할 수 있는 resolve 정적 메서드를 가져야 한다.
또한 두 가지 편의 정적 메서드가 더 있다: Promise.allKeyed()와 Promise.allSettledKeyed()는 각각 Promise.all()과 Promise.allSettled()처럼 동작하지만, promise들의 객체를 받아 같은 모양의 객체로 fulfilled되는 promise를 반환한다. 배열 대신 객체로 작업하면, 유지하기 어려운 임의의 배열 순서 대신 의미 있는 키에 결과를 연결할 수 있다.
이 메서드들은 then()을 사용해 각 입력 promise에 핸들러를 연결한다. 결과 promise가 일찍 settle되어도(예: Promise.race()에서 하나의 입력이 settle될 때) 다른 핸들러는 제거되지 않는다. 같은 pending promise를 동시성 메서드에 반복적으로 전달하면 그 핸들러가 결코 사용되지 않더라도 핸들러가 누적될 수 있다:
const pendingPromise = new Promise(() => {});
for (let i = 0; i < 1000; i++) {
await Promise.race([Promise.resolve(0), pendingPromise]);
}
// All tasks have completed, but pendingPromise retains the
// handlers attached by all 1000 races.
Promise는 이런 핸들러를 구독 해제하는 방법을 제공하지 않으며, 입력 promise가 pending이고 도달 가능한 동안 핸들러는 연결된 채로 남는다. 가능하다면 pending promise가 더 이상 유용하지 않을 때 AbortSignal을 사용해 기본 연산을 취소하라.
참고로 JavaScript는 본질적으로 단일 스레드이므로, 주어진 순간에는 하나의 작업만 실행되지만 제어가 서로 다른 promise 사이에서 이동해 promise 실행이 동시적으로 보일 수 있다. JavaScript에서 실제 병렬 실행은 오직 worker thread를 통해서만 가능하다.
생성자(Constructor)
Promise()— 새로운Promise객체를 만든다. 주로 promise를 아직 지원하지 않는 함수를 감싸는 데 사용된다.
정적 속성(Static properties)
Promise[Symbol.species]— promise 메서드에서 반환 값을 생성하는 데 사용되는 생성자를 반환한다.
정적 메서드(Static methods)
Promise.all()— promise들의 iterable을 입력으로 받아 단일Promise를 반환한다. 이 반환 promise는 입력의 모든 promise가 fulfilled될 때(빈 iterable을 전달할 때 포함) fulfillment 값 배열과 함께 fulfilled된다. 입력의 어느 promise라도 rejected되면 첫 번째 rejection 이유와 함께 rejected된다.Promise.allKeyed()—Promise.all()과 같지만 promise들의 객체를 받아 같은 모양의 객체로 fulfilled되는 promise를 반환하므로, 결과를 의미 있는 키와 연결할 수 있다.Promise.allSettled()— promise들의 iterable을 입력으로 받아 단일Promise를 반환한다. 이 반환 promise는 입력의 모든 promise가 settle될 때(빈 iterable 포함) 각 promise의 결과를 설명하는 객체 배열과 함께 fulfilled된다.Promise.allSettledKeyed()—Promise.allSettled()과 같지만 promise들의 객체를 받아 같은 모양의 객체로 fulfilled되는 promise를 반환한다.Promise.any()— promise들의 iterable을 입력으로 받아 단일Promise를 반환한다. 이 반환 promise는 입력의 어느 하나라도 fulfilled될 때 첫 fulfillment 값과 함께 fulfilled된다. 모든 promise가 rejected되면(빈 iterable 포함) rejection 이유 배열을 담은AggregateError와 함께 rejected된다.Promise.race()— promise들의 iterable을 입력으로 받아 단일Promise를 반환한다. 이 반환 promise는 먼저 settle되는 promise의 최종 상태로 settle된다.Promise.reject()— 주어진 이유로 rejected되는 새Promise객체를 반환한다.Promise.resolve()— 주어진 값으로 resolved되는Promise객체를 반환한다. 값이 thenable(즉then메서드를 가진)이면 반환 promise가 그 thenable을 "따라가며" 그 최종 상태를 채택하고, 그렇지 않으면 값으로 fulfilled된다.Promise.try()— 어떤 종류의 콜백(동기든 비동기든, 반환하든 throw하든)을 받아 그 결과를Promise로 resolve한다.Promise.withResolvers()— 새Promise객체와,Promise()생성자의 executor에 전달되는 두 파라미터에 해당하는 resolve·reject 함수 두 개를 담은 객체를 반환한다.
인스턴스 속성(Instance properties)
Promise.prototype에 정의되며 모든 Promise 인스턴스가 공유한다.
Promise.prototype.constructor— 인스턴스 객체를 생성한 생성자 함수. 초기 값은Promise생성자이다.Promise.prototype[Symbol.toStringTag]—[Symbol.toStringTag]속성의 초기 값은 문자열"Promise".Object.prototype.toString()에서 사용된다.
인스턴스 메서드(Instance methods)
Promise.prototype.catch()— promise에 rejection handler 콜백을 추가하고, 호출될 경우 콜백의 반환 값으로 resolve되는 새 promise를 반환한다. promise가 대신 fulfilled되면 원래 fulfillment 값으로 반환된다.Promise.prototype.finally()— promise에 핸들러를 추가하고, 원래 promise가 resolved될 때 resolved되는 새 promise를 반환한다. 핸들러는 promise가 fulfilled든 rejected든 settle될 때 호출된다.Promise.prototype.then()— promise에 fulfillment와 rejection 핸들러를 추가하고, 호출된 핸들러의 반환 값으로 resolve되는 새 promise를 반환한다. promise가 처리되지 않았으면(즉 해당 핸들러onFulfilled또는onRejected가 함수가 아니면) 원래 settled 값을 가진다.
예제
기본 예제
이 예제에서는 setTimeout(...)을 사용해 비동기 코드를 시뮬레이션한다. 실제로는 XHR이나 HTML API 같은 것을 사용할 것이다.
const myFirstPromise = new Promise((resolve, reject) => {
// We call resolve(...) when what we were doing asynchronously
// was successful, and reject(...) when it failed.
setTimeout(() => {
resolve("Success!"); // Yay! Everything went well!
}, 250);
});
myFirstPromise.then((successMessage) => {
// successMessage is whatever we passed in the resolve(...) function above.
// It doesn't have to be a string, but if it is only a succeed message, it probably will be.
console.log(`Yay! ${successMessage}`);
});
다양한 상황의 예제
이 예제는 Promise 기능을 사용하는 다양한 기법과 발생할 수 있는 다양한 상황을 보여 준다. 코드 블록 맨 아래로 스크롤해 promise 체인을 살펴보면 된다. 초기 promise가 제공되면 promise 체인이 이어질 수 있다. 체인은 .then() 호출로 구성되며, 일반적으로(항상은 아니지만) 끝에 단일 .catch()가 있고 선택적으로 .finally()가 뒤따른다. 이 예제에서 promise 체인은 직접 작성한 new Promise() 구조로 시작하지만, 실제로는 promise를 반환하는 API 함수(다른 사람이 작성한)로 시작하는 것이 더 흔하다.
예제 함수 tetheredGetNumber()는 promise 생성기가 비동기 호출을 설정하는 동안, 또는 콜백 안에서, 또는 둘 다에서 reject()를 사용함을 보여 준다. promiseGetWord() 함수는 API 함수가 자족적으로 promise를 생성하고 반환하는 방법을 보여 준다.
troubleWithGetNumber() 함수는 throw로 끝난다는 점에 유의하라. 이는 promise 체인이 오류 이후에도 모든 .then() promise를 통과하기 때문이며, throw가 없으면 오류가 "수정된" 것처럼 보이기 때문이다. 이는 번거로운 일이라, 체인 전체의 .then() promise에서 onRejected를 생략하고 최종 catch()에 단일 onRejected만 두는 것이 흔하다.
이 코드는 Node.js에서 실행할 수 있다. 실제 오류가 발생하는 것을 보면 이해가 더 쉬워진다. 더 많은 오류를 강제하려면 threshold 값을 변경하라.
// To experiment with error handling, "threshold" values cause errors randomly
const THRESHOLD_A = 8; // can use zero 0 to guarantee error
function tetheredGetNumber(resolve, reject) {
setTimeout(() => {
const randomInt = Date.now();
const value = randomInt % 10;
if (value < THRESHOLD_A) {
resolve(value);
} else {
reject(new RangeError(`Too large: ${value}`));
}
}, 500);
}
function determineParity(value) {
const isOdd = value % 2 === 1;
return { value, isOdd };
}
function troubleWithGetNumber(reason) {
const err = new Error("Trouble getting number", { cause: reason });
console.error(err);
throw err;
}
function promiseGetWord(parityInfo) {
return new Promise((resolve, reject) => {
const { value, isOdd } = parityInfo;
if (value >= THRESHOLD_A - 1) {
reject(new RangeError(`Still too large: ${value}`));
} else {
parityInfo.wordEvenOdd = isOdd ? "odd" : "even";
resolve(parityInfo);
}
});
}
new Promise(tetheredGetNumber)
.then(determineParity, troubleWithGetNumber)
.then(promiseGetWord)
.then((info) => {
console.log(`Got: ${info.value}, ${info.wordEvenOdd}`);
return info;
})
.catch((reason) => {
if (reason.cause) {
console.error("Had previously handled error");
} else {
console.error(`Trouble with promiseGetWord(): ${reason}`);
}
})
.finally((info) => console.log("All done"));
고급 예제
이 작은 예제는 Promise의 메커니즘을 보여 준다. testPromise() 메서드는 <button>이 클릭될 때마다 호출된다. setTimeout()을 사용해 1~3초마다 무작위로 promise count(1부터 시작하는 숫자)로 fulfilled될 promise를 만든다. Promise() 생성자가 promise를 만드는 데 사용된다.
promise의 fulfillment는 p1.then()으로 설정된 fulfill 콜백을 통해 로그된다. 몇몇 로그는 메서드의 동기 부분이 promise의 비동기 완료와 어떻게 분리되는지 보여 준다.
짧은 시간 안에 버튼을 여러 번 클릭하면 서로 다른 promise들이 차례로 fulfilled되는 것을 볼 수 있다.
"use strict";
let promiseCount = 0;
function testPromise() {
const thisPromiseCount = ++promiseCount;
const log = document.getElementById("log");
// begin
log.insertAdjacentHTML("beforeend", `${thisPromiseCount}) Started<br>`);
// We make a new promise: we promise a numeric count of this promise,
// starting from 1 (after waiting 3s)
const p1 = new Promise((resolve, reject) => {
// The executor function is called with the ability
// to resolve or reject the promise
log.insertAdjacentHTML(
"beforeend",
`${thisPromiseCount}) Promise constructor<br>`,
);
// This is only an example to create asynchronism
setTimeout(
() => {
// We fulfill the promise
resolve(thisPromiseCount);
},
Math.random() * 2000 + 1000,
);
});
// We define what to do when the promise is resolved with the then() call,
// and what to do when the promise is rejected with the catch() call
p1.then((val) => {
// Log the fulfillment value
log.insertAdjacentHTML("beforeend", `${val}) Promise fulfilled<br>`);
}).catch((reason) => {
// Log the rejection reason
console.log(`Handle rejected promise (${reason}) here.`);
});
// end
log.insertAdjacentHTML("beforeend", `${thisPromiseCount}) Promise made<br>`);
}
const btn = document.getElementById("make-promise");
btn.addEventListener("click", testPromise);
XHR로 이미지 로드하기
Promise와 XMLHttpRequest를 사용해 이미지를 로드하는 또 다른 예제이다. 각 단계에 주석이 달려 있어 Promise와 XHR 아키텍처를 밀접하게 따라갈 수 있다.
function imgLoad(url) {
// Create new promise with the Promise() constructor;
// This has as its argument a function with two parameters, resolve and reject
return new Promise((resolve, reject) => {
// XHR to load an image
const request = new XMLHttpRequest();
request.open("GET", url);
request.responseType = "blob";
// When the request loads, check whether it was successful
request.onload = () => {
if (request.status === 200) {
// If successful, resolve the promise by passing back the request response
resolve(request.response);
} else {
// If it fails, reject the promise with an error message
reject(
Error(
`Image didn't load successfully; error code: + ${request.statusText}`,
),
);
}
};
// Handle network errors
request.onerror = () => reject(new Error("There was a network error."));
// Send the request
request.send();
});
}
// Get a reference to the body element, and create a new image object
const body = document.querySelector("body");
const myImage = new Image();
const imgUrl =
"https://mdn.github.io/shared-assets/images/examples/round-balloon.png";
// Call the function with the URL we want to load, then chain the
// promise then() method with two callbacks
imgLoad(imgUrl).then(
(response) => {
// The first runs when the promise resolves, with the request.response
// specified within the resolve() method.
const imageURL = URL.createObjectURL(response);
myImage.src = imageURL;
body.appendChild(myImage);
},
(error) => {
// The second runs when the promise
// is rejected, and logs the Error specified with the reject() method.
console.log(error);
},
);
Incumbent settings object 추적(Incumbent settings object tracking)
settings object는 JavaScript 코드가 실행될 때 추가 정보를 제공하는 환경이다. 여기에는 realm과 module map, 그리고 origin 같은 HTML 특정 정보가 포함된다. incumbent settings object는 주어진 사용자 코드 조각에 대해 브라우저가 어떤 것을 사용할지 알도록 하기 위해 추적된다.
이를 더 잘 이해하기 위해 realm이 어떻게 문제가 될 수 있는지 살펴보자. realm은 대략 전역 객체로 생각할 수 있다. realm의 독특한 점은 JavaScript 코드를 실행하는 데 필요한 모든 정보를 보유한다는 것이다. 여기에는 Array와 Error 같은 객체가 포함된다. 각 settings object는 이들의 자체 "복사본"을 가지며 공유되지 않는다. 이는 promise와 관련해 예상치 못한 동작을 유발할 수 있다. 이를 해결하기 위해 incumbent settings object라고 부르는 것을 추적한다. 이는 특정 함수 호출을 담당하는 사용자 코드의 컨텍스트와 관련된 정보를 나타낸다.
이를 조금 더 설명하기 위해 문서에 내장된 <iframe>이 호스트와 통신하는 방법을 살펴보자. 모든 웹 API가 incumbent settings object를 알고 있으므로 다음은 모든 브라우저에서 동작한다:
<!doctype html>
<iframe></iframe>
<!-- we have a realm here -->
<script>
// we have a realm here as well
const bound = frames[0].postMessage.bind(frames[0], "some data", "*");
// bound is a built-in function — there is no user
// code on the stack, so which realm do we use?
setTimeout(bound);
// this still works, because we use the youngest
// realm (the incumbent) on the stack
</script>
같은 개념이 promise에도 적용된다. 위 예제를 조금 바꾸면 다음과 같다:
<!doctype html>
<iframe></iframe>
<!-- we have a realm here -->
<script>
// we have a realm here as well
const bound = frames[0].postMessage.bind(frames[0], "some data", "*");
// bound is a built in function — there is no user
// code on the stack — which realm do we use?
Promise.resolve(undefined).then(bound);
// this still works, because we use the youngest
// realm (the incumbent) on the stack
</script>
이를 바꿔 문서의 <iframe>이 post 메시지를 듣도록 하면 incumbent settings object의 효과를 관찰할 수 있다:
<!-- y.html -->
<!doctype html>
<iframe src="x.html"></iframe>
<script>
const bound = frames[0].postMessage.bind(frames[0], "some data", "*");
Promise.resolve(undefined).then(bound);
</script>
<!-- x.html -->
<!doctype html>
<script>
window.addEventListener("message", (event) => {
document.querySelector("#text").textContent = "hello";
// this code will only run in browsers that track the incumbent settings object
console.log(event);
});
</script>
위 예제에서 <iframe>의 내부 텍스트는 incumbent settings object가 추적될 때만 업데이트된다. incumbent을 추적하지 않으면 메시지를 보내는 데 잘못된 환경을 사용하게 될 수 있기 때문이다.
참고: 현재 incumbent realm 추적은 Firefox에서 완전히 구현되었고, Chrome과 Safari에서는 부분적으로 구현되었다.
명세(Specifications)
- ECMAScript® 2027 Language Specification — sec-promise-objects
브라우저 호환성
baseline 기준 2015년 7월부터 널리 사용 가능하다. 호환성 표는 JavaScript를 활성화해야 볼 수 있다.
참고 자료
Promise의core-js폴리필- Using promises 가이드
- Promises/A+ 명세
- JavaScript Promises: an introduction on web.dev (2013)
- Callbacks, Promises, and Coroutines: Asynchronous Programming Patterns in JavaScript (Domenic Denicola, 2011)