Promise.withResolver
는 2024년에 새롭게 등장한 Promise Static Method 입니다.
사내 개발자 스크럼에서 공유할 내용을 찾던 도중에 작지만 재밌는 메서드가 이번에 새로 추가되어 간단하게 조사를 진행해보았습니다.
이 메서드는 주로 Promise 를 Resolve 시키거나 Reject 하는 로직을 Promise 내부가 아닌 외부에서 결정할 수 있도록 해줍니다.
즉, Promise 의 이행 여부를 생성자 내부의 Callback 에서 결정짓지 않고 외부의 컨텍스트에서 결정지을 수 있도록 하는 메서드립니다.
Promise 를 다루기 위해서 사용자는 반드시 Resolve 함수와 Reject 함수를 인자로 받는 Callback 을 Promise 생성자에 넘겨야 했습니다.
이러한 설계는 보통 Callback 이 내부에서 비동기 함수를 호출한 후, 그 결과에 따라 Promise 를 Resolve 하거나 Reject 시키는 경우 쓰였습니다.
const promise = new Promise((resolve, reject) => {
asyncRequest(config, response => {
const buffer = [];
response.on('data', data => buffer.push(data));
// Callback 내부에서 resolve 혹은 reject 함수 호출
response.on('end', () => resolve(buffer));
response.on('error', reason => reject(reason));
});
});
promise
.then((res) => console.log('promise is resolved'));
.catch((err) => console.error('promise is rejected'));
그런데 만약 사용자가 Promise 를 생성했지만 이행 결과를 초기화 이후에 결정짓고 싶을 때는 어떨까요?
보통은 Callback 의 외부 컨텍스트에서 식별자를 정의하고, Callback 내부에서 식별자에 인자로 받은 resolve
, reject
함수를 넘기는 편입니다.
class CancelToken {
constructor(executor) {
if (typeof executor !== 'function') {
throw new TypeError('executor must be a function.');
}
// 1. resolve 함수를 받을 식별자를 선언한다.
let resolvePromise;
// 2. Promise 생성자 내부 함수에서 resolvePromise 에 resolve 함수를 넘긴다.
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});
const token = this;
//... 중략
executor(function cancel(message, config, request) {
if (token.reason) return;
token.reason = new CanceledError(message, config, request);
// 3. resolvePromise 함수를 호출하여 Promise 를 resolve 시킨다.
resolvePromise(token.reason);
});
}
// @gate experimental
it('emits all HTML as one unit', async () => {
let hasLoaded = false;
// 1. resolve 함수를 받을 식별자를 선언한다.
let resolve;
// 2. Promise 생성자 내부 함수에서 resolvePromise 에 resolve 함수를 넘긴다.
const promise = new Promise(r => (resolve = r));
// 3. hasLoaded 가 false 이므로 아직 이행되지 않은 Promise 를 던진다.
function Wait() {
if (!hasLoaded) {
throw promise;
}
return 'Done';
}
const resultPromise = ReactDOMFizzStatic.prerender(
<div>
<Suspense fallback="Loading">
<Wait />
</Suspense>
</div>,
);
await jest.runAllTimers();
// 4. 이후 Resolve 함수를 실행하여 Promise 를 이행시킨다.
hasLoaded = true;
await resolve();
const result = await resultPromise;
const prelude = await readContent(result.prelude);
expect(prelude).toMatchInlineSnapshot(
`"<div><!--$-->Done<!-- --><!--/$--></div>"`,
);
});
이렇듯 Resolve, Reject 함수를 받기 위한 식별자를 사전에 생성하는 코드는 프로덕션 레벨의 라이브러리에서 심심찮게 보이는 패턴입니다.
Promise 가 생성되는 레벨에서 이행 여부가 결정되지 않고, 이후의 단계에서 결정되기 때문에 이러한 패턴을 Deferred Promise 라고 불리며, 실제 jQuery 에서도 이러한 패턴을 돕는 유틸 함수가 존재합니다.
var _deferred = function(param) {
// Deffered 객체 생성
var dfd = $.Deferred();
setTimeout(function() {
// 비동기 처리가 종료되면 resolve 혹은 reject 함수 호출로 Deffered 객체의 state 변경
param ? dfd.resolve("resolved!") : dfd.reject(new Error("Error occurred!"));
}, 2000);
// Deffered 객체가 보유한 Promise 반환.
return dfd.promise();
};
_deferred(false)
.done(function(data) {
// resolve가 실행된 경우 (성공), resolve 함수에 전달된 값이 data에 저장된다
console.log(data);
})
.fail(function(error) {
// reject가 실행된 경우 (실패), reject 함수에 전달된 값이 error에 저장된다
console.error(error);
})
.always(function() {
// Promise 의 resolve / reject 여부와 관계 없이 실행되는 함수.
console.log('always');
});
Promise.withResolver 함수는 promise
, resolve
, reject
속성이 담긴 객체를 반환합니다.
const { promise, resolve, reject } = Promise.withResolver()
위의 코드는 정확히 아래의 형식과 동일합니다.
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
// AS - IS : Promise 생성자 내부에서 처리.
const promise = new Promise((resolve, reject) => {
fs.unlink(`${imageFile.path}`, (error) => {
return error ? reject(error) : resolve();
});
});
// TO - BE : Promise 생성자 외부에서 이행 여부를 결정
const { promise, resolve, reject } = Promise.withResolver();
fs.unlink(`${imageFile.path}`, (error) => {
return error ? reject(error) : resolve();
})
async init() {
const { promise, resolve, reject } = Promise.withResolver();
if (typeof window === 'undefined') {
reject('T Map 은 Server Side 에서 사용할 수 없습니다.');
}
const mapElement = document.getElementById(mapId);
if (!mapElement) {
reject(
'T Map 을 렌더링하기 위해 필요한 HTMLDivElement 가 없습니다.',
);
}
this.#mapInstance = new Tmapv3.Map(mapId, {
center: new Tmapv3.LatLng(lat, lng),
width: typeof width === 'number' ? `${width}px` : width,
height: typeof height === 'number' ? `${height}px` : height,
zoom,
});
// Promise 의 이행 여부를 이벤트 핸들러에게 위임한다.
this.#mapInstance.on("ConfigLoad", resolve);
return promise;
}
뭐… 새로운 메서드가 추가된 건 좋은데 언제 이걸 어떻게 잘 써먹어야 할지는 잘 모르겠네요