Promise를 만들어 놓고, 정작 resolve는 한참 뒤에 다른 함수에서 호출해야 하는 상황이 가끔 있다. 모달을 띄우고 사용자가 확인 버튼을 누를 때까지 기다린다거나, 이벤트 리스너가 한 번 발동하면 그때 값을 넘긴다거나.
이걸 new Promise로 하려면 항상 이런 꼴이 된다.
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
// 나중에 다른 데서
resolve(결과);
콜백 안에 갇혀 있는 resolve를 바깥 변수로 끄집어내는 작업이다. 동작은 하는데 매번 let으로 변수를 두 개 선언하고, 클로저로 주워 담는 게 영 지저분하다. TypeScript를 쓰면 resolve의 타입까지 직접 달아줘야 해서 더 번거롭다.
Promise.withResolvers()
이 패턴을 그대로 표준으로 만든 게 Promise.withResolvers()다. promise와 resolve, reject를 한 번에 객체로 돌려준다.
const { promise, resolve, reject } = Promise.withResolvers();
끝이다. new Promise도, 바깥에 둔 let도 필요 없다. 위 예시를 다시 쓰면 이렇게 줄어든다.
실전에서 많이 쓰는 건 "한 번짜리 이벤트를 await로 기다리기"다. 예를 들어 버튼 클릭을 기다렸다가 다음으로 넘어가고 싶을 때.
function waitForClick(button) {
const { promise, resolve } = Promise.withResolvers();
button.addEventListener('click', resolve, { once: true });
return promise;
}
// 호출부
await waitForClick(confirmButton);
console.log('사용자가 눌렀다');
모달 확인/취소도 같은 구조로 깔끔하게 빠진다.
function confirmDialog() {
const { promise, resolve } = Promise.withResolvers();
okButton.onclick = () => resolve(true);
cancelButton.onclick = () => resolve(false);
dialog.showModal();
return promise; // Promise<boolean>
}
const ok = await confirmDialog();
지원 범위
2024년에 모든 주요 브라우저와 Node.js(22+)에 들어가서 이제 Baseline에 올라왔다. 구형 환경까지 챙겨야 하면 core-js 폴리필이 있고, 직접 넣어도 세 줄이면 된다.
Promise.withResolvers ??= function () {
let resolve, reject;
const promise = new Promise((res, rej) => { resolve = res; reject = rej; });
return { promise, resolve, reject };
};
정리
new Promise 콜백 밖에서 resolve를 불러야 하는 코드를 짜고 있다면 거의 무조건 이걸로 바꾸면 된다. 변수 선언이 줄고, 의도도 한눈에 보인다. 별거 아닌 것 같지만 이벤트나 모달을 Promise로 감싸는 코드가 많은 프로젝트라면 체감이 꽤 크다.