1. 클래스 초기화 및 실행 컨텍스트 설정
Polyfill 개발의 첫 단계는 Promise/A+ 명세서에 정의된 상태 모델과 실행기를 구성하는 것입니다. 내부적으로 상태 값, 최종 결과물, 그리고 성공/실패 콜백 큐 배열을 프로퍼티로 선언합니다. 실행기는 즉시 호출되며, 이때 발생할 수 있는 동시성 충돌을 방지하기 위해 this 바인딩과 예외 포착 로직을 적용합니다.
class CustomPromise {
constructor(executor) {
this.state = 'pending';
this.value = undefined;
this.successQueue = [];
this.failureQueue = [];
const resolve = (val) => {
if (this.state !== 'pending') return;
this.state = 'fulfilled';
this.value = val;
this.successQueue.forEach(task => task(val));
};
const reject = (err) => {
if (this.state !== 'pending') return;
this.state = 'rejected';
this.value = err;
this.failureQueue.forEach(task => task(err));
};
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
}
2. then 메서드의 동기적 실행과 비동기 전환
원격 API나 타이머 없이 즉시 해결된 경우, then은 콜백을 바로 실행합니다. 그러나 비동기 환경에서는 상태가 아직 'pending'일 수 있으므로, 콜백 함수를 해당 큐에 밀어넣어야 합니다. Promise가 항상 마이크로태스크처럼 동작하도록 보장하기 위해 setTimeout(..., 0) 패턴을 적용하여 호출 순서를 조정합니다.
then(onSuccess, onFailure) {
return new CustomPromise((resolve, reject) => {
const handleSuccess = (val) => {
setTimeout(() => {
try {
if (!onSuccess) return resolve(val);
const result = onSuccess(val);
this.handleChain(result, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
};
const handleFailure = (err) => {
setTimeout(() => {
try {
if (!onFailure) return reject(err);
const result = onFailure(err);
this.handleChain(result, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
};
if (this.state === 'fulfilled') handleSuccess(this.value);
else if (this.state === 'rejected') handleFailure(this.value);
else {
this.successQueue.push(handleSuccess);
this.failureQueue.push(handleFailure);
}
});
}
3. 체이닝(Chaining)과 반환값 처리 로직
Promise를 연결할 때마다 새로운 인스턴스가 반환되는 특성이 있습니다. 이는 이전 상태의 결괏값이 다음 프로미스에 전달되기 위함이며, 원본 Promise의 상태는 이미 고정되었으므로 this를 리턴하면 안 됩니다. handleChain 내부 메서드에서는 반환값이 일반 값인지, 아니면 또 다른 Promise 객체인지를 판별하여 재귀적으로 처리하거나 즉시 해결해야 합니다.
handleChain(returnedVal, resFn, rejFn) {
if (returnedVal instanceof CustomPromise) {
returnedVal.then(resFn, rejFn);
} else {
resFn(returnedVal);
}
}
4. catch 연산자와 예외 버블링
에러 핸들링은 then(undefined, onError)를 축약한 형태입니다. 만약 onError 콜백이 누락되거나 정의되지 않은 에러가 발생하지 않았다면, 에러는 아래 체이닝 단계로 자연스럽게 전달됩니다(Bubbling). 이는 Promise가 에러를 자동으로 정지시키지 않고, 명시적인 catch 블록이나 마지막 인스턴스에서 처리될 때까지 이어지는 거버넌스 규칙을 반영합니다.
catch(onError) {
return this.then(null, onError);
}
5. 정적 팩토리 메서드 구현
정적 메서드는 기존 Promise 인스턴스를 래핑하거나 즉시 해결/거부된 상태를 생성할 때 사용됩니다. 특히 resolve 메서드에는 외부 값이 이미 Promise라면 그것을 그대로 전달하는(Unwrapping) 논리가 반드시 포함되어야 합니다.
static resolve(val) {
if (val instanceof CustomPromise) return val;
return new CustomPromise(res => res(val));
}
static reject(reason) {
return new CustomPromise((_, rej) => rej(reason));
}
6. 병렬 제어 정적 메서드(all, race)
다중 비동기 작업을 통합하는 정적 메서드는 입력된 이터러블(iterable) 내각의 Promise들이 언제 완료되는지에 따라 결정邏輯을 가집니다. all은 모든 작업이 성공해야 결과를 집계하고, 하나라도 실패하면 즉시 거부됩니다. 반면 race는 가장 먼저 settled 상태가 되는 결과만 채택합니다.
static all(promisesArr) {
return new CustomPromise((resolve, reject) => {
let completedCount = 0;
const results = new Array(promisesArr.length);
promisesArr.forEach((p, idx) => {
CustomPromise.resolve(p).then(
val => {
results[idx] = val;
if (++completedCount === promisesArr.length) resolve(results);
},
err => reject(err)
);
});
});
}
static race(promisesArr) {
return new CustomPromise((resolve, reject) => {
for (const p of promisesArr) {
CustomPromise.resolve(p).then(resolve, reject);
}
});
}