해당 글은 MDN Web Docs에 작성된 Promise 문서를 번역한 것입니다.
(한국어에는 없는 내용도 있고, 외국어 연습을 위해 일본어와 영어를 번갈아보며 작성했습니다.)
Promise 는 비동기 연산이 종료된 이후 결과 값과 실패 사유를 처리하기 위한 처리기 연결을 위한 객체이다. Promise 사용 시 비동기 메서드에서 동기 메서드를 사용하는 것처럼 값을 반환할 수 있다. 다만, 최종 결과가 아닌 미래의 어떤 시점에 결과를 제공하겠다는 약속 을 반환한다.
Promise 는 경우에 따라 성공 또는 실패 결과를 반환한다. 이 때 then 메서드를 통해 대기열(큐)에 추가된 처리기가 호출되고, 이미 이행했거나 거부된 Promise 에 처리기를 연결해도 호출되므로, 비동기 연산과 처리기 연결 사이 경합 조건(race condition)이 일어나지 않는다.

promise 가 fulfilled 또는 rejected 상태일 때는 settled 되었다고 한다.
promise가 settled 되거나 다른 promise 의 최종 상태로 확정되어, 기존 promise가 fulfilled 또는 rejected 되어도 아무 영향이 일어나지 않는 경우는 resolved 되었다고 한다.
예시를 들자면 이렇다.
new Promise((resolveOuter) => {
resolveOuter(
new Promise((resolveInner) => {
setTimeout(resolveInner, 1000);
}),
);
});
이 promise 는 resolveOuter 가 동기적으로 실행되기 때문에 resolved 상태이지만, 다른 promise 를 호출하면서 resolved 되기 때문에, 호출한 promise 는 fulfilled 상태여도 resolveInner 가 fulfilled 되기 전까진 fulfilled 상태가 될 수 없다.
💡 MEMO
Scheme과 같은 일부 언어에선, 느긋한 평가(lazy evaluation)나 계산을 연기하는 promise 라는 메커니즘이 존재한다. JavaScript 에서의 promise 는 콜백 함수가 여러 개 연결된, 이미 실행되고 있는 프로세스를 나타낸다. 그러므로, 지금 실행이 아닌 나중에 실행되도록 하기 위해선 f = () => expression 과 같이 argument가 없는 함수를 사용하도록 한다. f() 는 표현식을 바로 실행시킨다.
then , catch , finally 메서드 사용으로, resovled 된 promise 에 더해서 코드를 실행시키는 것이 가능하다.
최대 2개의 파라미터를 가진다. 첫 번째는, promise 가 fulfilled 되었을 때의 콜백 함수, 두 번째는, rejected 되었을 때의 콜백함수이다. 각 .then() 은 새로 생성된 promise 객체를 반환한다. 이 객체는, 연계하여 사용할 수 있다.
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("foo");
}, 300);
});
myPromise
.then(handleFulfilledA, handleRejectedA)
.then(handleFulfilledB, handleRejectedB)
.then(handleFulfilledC, handleRejectedC);
.then() 에 promise 객체를 반환하는 콜백 함수가 없을 때도, 연계된 함수로 처리가 진행된다. 그러므로, 연쇄 시 마지막 .catch() 까지 모든 rejected 콜백 함수를 생략해도 된다.
각각 .then() 에서 rejected 된 promise 를 취급하면, promise 의 연쇄보다 먼저 영향을 끼친다. 에러를 즉시 처리해야하기 때문에, 선택의 여지가 없는 경우도 있다. 이런 경우에는, 연쇄적으로 에러 상태를 유지하기 위해, 어떤 에러를 발생시켜야 할 필요가 있다. 그러나, 바로 에러를 처리할 필요가 없다면 catch 상태까지 에러를 취급하지 않는 것이 단순합니다. catch 는, 실제로, promise 가 fulfilled 되었을 때의 콜백 함수가 없는 then 메서드에 가깝다.
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);
});
💡 MEMO
빠르게 실행하기 위해선, 가능한 모든 동기 실행을 하나의 핸들러로 처리한다. 그렇지 않으면, 모든 핸들러를 순서대로 실행하기 위해 시간이 걸릴 수도 있다.
promise 의 종료 조건은, 연쇄된 다음 promise 의 진행 여부를 결정한다. fulfilled 상태는 promise 가 성공, rejected 상태는 실패를 가리킨다. 실행된 각 promise 의 반환 값은 다음 .then() 에 넘겨져 rejected 된 이유는 다음 rejected 핸들러 함수로 넘겨진다.
다음으로 실행되는 promise 는, 중첩될 경우 스택처럼 제일 마지막에 삽입된 것이 가장 먼저 실행된다. 맨 처음 promise 는 제일 마지막에 실행된다. nextValue 가 promise 인 경우 동적으로 값이 치환된다. return 에 의해 프로미스가 꺼내지지만, nextValue 의 promise 는 호출한 promise 를 대체한다.
promise B 가 nextValue 로 promiseX 를 반환한다.
(promise D, (promise C, (promise B, (promise A) ) ) )
(promise D, (promise C, (promise X) ) ) // 이런 식으로 변한다.
promise 는 하나 이상 중첩될 수 있다. 다음 코드에선, promise A 가 settled 되면 두 개의 .then() 이 실행된다.
const promiseA = new Promise(myExecutorFunc);
const promiseB = promiseA.then(handleFulfilled1, handleRejected1);
const promiseC = promiseA.then(handleFulfilled2, handleRejected2);
이미 settled 된 promise 에 액션을 취할 수 있다. 그 경우, 액션은(적절하다면) 최초 비동기 함수 실행 시에 함께 시행된다. promise 가 비동기로 실행되어야 한다는 점을 주의해야 한다. 그러므로, 이미 settled 된 promise 에 대한 액션은, 스택이 비워짐과 함께 실행 시간이 되어야 시행된다. 해당 효과는 setTimeout(action, 0) 과도 같다.
const promiseA = new Promise((resolve, reject) => {
resolve(777);
});
// 이 시점에서、 "promiseA" 는 이미 settled 된다.
promiseA.then((val) => console.log("asynchronous logging has val:", val));
console.log("immediate logging");
// 출력 결과
// immediate logging
// asynchronous logging has val: 777
JavaScript 의 환경엔, promise 객체가 도입되기 전부터, 여러 promise 가 사용되어왔다. 내부에선 여러 형태로 표현되어왔지만, 최소 모든 promise 와 비슷한 객체는 Thenable 인터페이스를 사용하고 있다. Thenable 은 promise 가 fulfilled 되는 것 하나, rejected 되는 것 또 하나의 콜백 함수로 이루어진, .then() 메서드를 가지고 있다. promise 또한 thenable 과 비슷하다.
존재하는 promise 와 상호작용하기 위해선, 언어가 promise 대신 thenable 을 사용하는 것을 허용해야한다. 예를 들어, Promise.resolve 는 promise 를 resolved 할 뿐만 아니라 thenable 을 추적하기도 한다.
const aThenable = {
then(onFulfilled, onRejected) {
onFulfilled({
// thenable은 다른 thenable로 fulfilled 된다.
then(onFulfilled, onRejected) {
onFulfilled(42);
},
});
},
};
Promise.resolve(aThenable); // promise는 42라는 값을 반환한다.
promise 클래스는, 비동기 처리의 병행 처리를 쉽게 할 수 있게, 네 개의 정적 메서드를 제공한다.
Promise.all()
모든 promise 가 fulfilled 되어야만 fulfilled 되며, 하나의 promise 라도 rejected 되면 rejected 된다.
Promise.allSettled()
모든 promise 가 settled 되어야만 fulfilled 된다.
Promise.any()
하나의 promise 만 fulfilled 되어도 fulfilled 된다. 모든 promise 가 rejected 되면 rejected 된다.
Promise.race()
하나의 promise 만 settled 되어도 fulfilled 된다. 다르게 말하면, 하나의 promise 가 fulfilled 되면 fulfilled 되고, rejected 되면 rejected 된다.
모든 메서드는 thenable가 존재하는 여러 promise 를 취하고 새로운 promise 를 반환한다. 모두 서브클래스 화에 대응하고 있다. promise 는 서브클래스로서 호출할 수 있으며, 그 결과는 서브클래스의 종류를 가진 promise 가 된다. 그러기 위해선, 서브클래스의 생성자에 Promise() 와 같은 정의를 implement 할 필요가 있다. 즉, 단일 executor 함수를 취하며, resolve 와 reject 콜백 함수를 인수로 취하는 형태가 되어야 한다. 또, 서브클래스엔 정적 메서드인 resolve 또한 필요하다. 이건 Promise.resolve() 와 같이 호출할 수 있으며, 값을 promise 로 resolve 하기 위한 메서드이다.
JavaScript 는 원래 싱글 스레드로 작동하기 때문에, 한 번에 하나의 작업밖에 할 수 없지만, 서로 다른 promise 간 제어권이 이동되어, promise 이 동시에 실행되는 것처럼 보인다. JavaScript 에서 병렬 실행하기 위해선, 워커 스레드 를 사용할 수 밖에 없다.
Promise()
새로운 promise 객체를 생성한다. 이 생성자는 특히, promise에 대응하지 않는 함수를 감싸기 위해 사용된다.
Promise[@@species]
promise 는 메서드로부터 반환 값의 구조를 생성하기 위해 사용되는 생성자를 반환한다.
Promise.all()
입력으로 여러 promise 객체를 받으며, 단 하나의 promise 를 반환한다. 반환된 promise 는, 입력된 promise 가 모두 fulfilled 되었을 때 (빈 여러 객체를 전달 받았을 때도 포함) fulfilled 된 값의 배열을 반환한다. 입력된 promise 중 하나의 promise 라도 rejected 되면, 맨 처음 rejected 된 이유에 의해 rejected 된다.
Promise.allSettled()
입력으로 여러 promise 객체를 받으며, 단 하나의 promise 를 반환한다. 반환된 promise 는, 입력된 promise 가 모두 settled 되었을 때 fulfilled 된다. (빈 여러 객체를 전달 받았을 때도 포함)
Promise.any()
입력으로 여러 promise 객체를 받으며, 단 하나의 promise 를 반환한다. 반환된 promise 는, 단 하나의 promise 라도 fulfilled 된다면 fulfilled 되며, 맨 처음 fulfilled 된 promise 의 결과값을 반환한다. 입력된 promise 가 전부 rejected 되었을 때, (빈 여러 객체를 전달 받았을 때도 포함), rejected 된 이유가 AggregateError 의 배열로 반환된다.
Promise.race()
입력으로 여러 promise 객체를 받으며, 단 하나의 promise 를 반환한다. 반환된 promise 는, 맨 처음 settled 된 promise의 최종적인 상태로 결과값이 결정된다.
Promise.reject()
주어진 이유로 rejected 된 새로운 promise 객체를 반환한다.
Promise.resolve()
주어진 값으로 fulfilled 된 promise 객체를 반환한다. 값이 Thenable (즉, then 메서드를 가진 객체)라면, 반환되는 promise 는 그 Thenable 을 찾아, 결과를 반환한다. 그렇지 않다면, 반환되는 promise 는 주어진 값이 된다.
이 프로퍼티는 Promise.prototype 에 정의되어 있는, 모든 Promise 인스턴스에서 공유된다.
Promise.prototype.constructor
인스턴스 객체를 생성하는 함수
Promise.prototype[@@toStringTag]
@@toStringTag 프로퍼티의 초기 값은 문자열 "Promise" 이다. 이 프로퍼티는 Object.prototype.toString() 을 통해 사용된다.
Promise.prototype.catch()
promise 가 rejected 되었을 때 콜백 함수를 추가하고, 호출되었을 때 값을 반환하며, fulfilled 되었을 때, 원래 fulfilled 되었을 때 결과로 반환되었을 값을 반환한다.
Promise.prototype.finally()
promise 에 핸들러를 추가하며, 원래 promise 가 resolved 되었을 때 resolved 되는 새로운 promise 를 반환한다. 해당 핸들러는, 성공과 실패 여부에 관계 없이, 원래 promise 가 완료된 시점에 호출된다.
Promise.prototype.then()
promise 에 fulfilled 되었을 때와 rejected 되었을 때의 핸들러를 추가하며, 호출된 핸들러에선 새로운 promise 를 반환한다. promise 가 처리되지 않았을 경우 (즉, 관련 핸들러가 onFulfilled 또는 onRejected 가 아니라면) 원래 settled 되었을 때의 값을 반환한다.
const myFirstPromise = new Promise((resolve, reject) => {
// 동기적으로 실행한 것이 성공했을 때 resolve()를 호출, 실패했을 때 reject()를 호출한다.
// 이 예시에선, 동기적 코드를 실행하기 위해서 setTimeout()을 사용한다.
// 실제로는, XHR 또는 HTML API 같은 것을 실행한다.
setTimeout(() => {
resolve("Success!"); // 무조건 resolve()를 호출한다.
}, 250);
});
myFirstPromise.then((successMessage) => {
// resolve() 함수로 전달한 문자열 === successMessage
console.log(`Yay! ${successMessage}`);
});
// threshold 값은 랜덤으로 에러를 생성한다.
const THRESHOLD_A = 8; // 0으로 설정 시 무조건 에러를 발생시킨다.
// 비동기 호출 시, 또는 콜백 함수 내, 또는 reject를 사용해
// promise를 생성하는 경우를 나타낸다.
// API 함수가 어떻게 자기 완성형 promise를 반환하는 지 나타낸다.
function tetheredGetNumber(resolve, reject) {
setTimeout(() => {
const randomInt = Date.now();
const value = randomInt % 10;
if (value < THRESHOLD_A) {
resolve(value);
} else {
reject(`Too large: ${value}`);
}
}, 500);
}
function determineParity(value) {
const isOdd = value % 2 === 1;
return { value, isOdd };
}
// promise 실행 중 모든 then을 통과하고,
// 에러가 발생하면, throw 없이 에러가 해결된 것처럼 보이도록 한다.
// 보통은, then()마다 onRejected를 작성하지 않고, 마지막 catch에서 작성한다.
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(`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"));
<button id="make-promise">프로미스 완성</button>
<div id="log"></div>
"use strict";
let promiseCount = 0;
function testPromise() {
const thisPromiseCount = ++promiseCount;
const log = document.getElementById("log");
// 개시
log.insertAdjacentHTML("beforeend", `${thisPromiseCount}) 개시<br>`);
// 새로운 프로미스를 생성한다. 이 프로미스는 3초 후에 1부터 카운트를 시작한다.
const p1 = new Promise((resolve, reject) => {
// 프로미스를 fulfill 또는 reject 하는 기능을 가진 함수가 호출된다.
log.insertAdjacentHTML(
"beforeend",
`${thisPromiseCount}) Promise 생성자<br>`,
);
// 비동기 호출을 위해 setTimeout를 사용한다.
setTimeout(
() => {
// 프로미스를 resolve한다.
resolve(thisPromiseCount);
},
Math.random() * 2000 + 1000,
);
});
// 프로미스가 fulfill 되었을 때 then()을 호출한다.
// 프로미스가 reject 되었을 때 catch()를 호출한다.
p1.then((val) => {
// fulfill 시 반환하는 값을 로그로 남긴다.
log.insertAdjacentHTML("beforeend", `${val}) Promise fulfilled<br>`);
}).catch((reason) => {
// reject 시 반환하는 값(에러 이유)를 로그로 남긴다.
console.log(`reject된 프로미스 (${reason}) 를 처리한다.`);
});
// 종료
log.insertAdjacentHTML("beforeend", `${thisPromiseCount}) Promise 생성<br>`);
}
const btn = document.getElementById("make-promise");
btn.addEventListener("click", testPromise);
설정 객체는, JavaScript 에서 코드 실행 시 추가 정보를 제공하는 환경을 뜻한다. 영역, 모듈 맵 외에, 출처 등의 HTML 고유 정보도 포함한다. 실행되는 설정 객체가 추적되는 것은, 특정 사용자 코드에 대해 설정 객체를 사용할지 브라우저가 파악하기 위해서이다.
영역(realm)은, 대략적으로 말하자면 전역 객체를 뜻한다. 영역의 특징은, JavaScript 의 코드를 실행하기 위해서 필요한 정보를 모두 가지고 있다는 것이다. 이것에 대해선 배열이나 에러 등의 객체도 포함된다. 각각의 설정 객체는 복사본을 지니고 있으며, 공유하지 않는다. 때문에, promise 와의 관계에서 예기치 못한 행동을 하는 경우가 있다. 이를 위해, 현재 실행되는 객체(incumbent settings object)라는 것을 추적한다. 이건, 존재하는 함수를 호출하는 유저 코드의 콘텍스트에 고유 정보를 나타낸다.
<!doctype html> <iframe></iframe>
<!-- 이 곳부터 영역이다. -->
<script>
// 이 곳도 영역이다.
const bound = frames[0].postMessage.bind(frames[0], "some data", "*");
// bound는 built-in 함수이다.
// 스택 상엔 유저코드가 없지만, 어떤 영역을 사용할까?
setTimeout(bound);
// 물론 동작한다. 스택 위의 가장 먼저 들어온 (현재 실행되는)
// 영역을 사용하기 때문이다.
</script>
이를 promise 에 적용하면 다음과 같다.
<!doctype html> <iframe></iframe>
<!-- 이 곳부터 영역이다. -->
<script>
// 이 곳도 영역이다.
const bound = frames[0].postMessage.bind(frames[0], "some data", "*");
// bound는 built-in 함수이다.
// 스택 상엔 유저코드가 없지만, 어떤 영역을 사용할까?
Promise.resolve(undefined).then(bound);
// 물론 동작한다. 스택 위의 가장 먼저 들어온 (현재 실행되는)
// 영역을 사용하기 때문이다.
</script>
코드를 조금 변경시켜, 문서 내의 <iframe> 이 post 메시지를 가지도록 하면, 현재 실행되는 설정 객체의 효과를 관찰할 수 있다.
<!-- 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";
// 이 코드는 현재 실행되늰 설정 객체를 추적하는 브라우저에서만 작동한다.
console.log(event);
},
false,
);
</script>
상기 예시에선, 현재 실행되는 설정 객체가 추적될 때의 <iframe> 의 내부 텍스트를 업데이트한다. 이건 현재 실행되는 것을 추적하지 않으면, 메시지를 보내는 환경을 잘못 알 수도 있기 때문이다.