미주알고주알

[Javascript] Promise 객체 직접 구현하기 본문

Javascript

[Javascript] Promise 객체 직접 구현하기

미주알고주알 2023. 3. 24. 16:40

비동기 처리라 하면 가장 먼저 떠오르는 `Promise`.

생성자 함수로써 프로미스 객체를 만들기 때문에

이 생성자란 개념을 이용해 프로미스를 직접 구현해보고자 한다.

 

프로미스 객체는 `then`, `catch`란 메소드를 갖고 있고, 인스턴스 객체라면 모두 공유되는 메소드이기 때문에, 상속의 개념인 `prototype`이란 아이디어를 같이 사용해보면 좋을 것 같다.

 

또한 프로미스의 인자로 들어오는 함수를 실행할 때 그 함수의 매개변수에 `resolve`, `reject`의 메소드가 들어와야 하며,

`resolve`, `reject`에 담겨져 오는 성공 및 에러 값은 `then`, `catch`메소드가 실행 후  넘겨져 나온다. 

 

이런 프로미스의 구조 및 원리를 생각하면서

다음과 같은 코드를 짜봤다.

function Promise(cb) {
	Promise.prototype.then = tcb => {
		Promise.prototype.thenFn = tcb;
		return this;
	};

	Promise.prototype.catch = ccb => {
		Promise.prototype.catchFn = ccb;
		return this;
	};

	const resolve = succ => {
		this.state = 'resolve';
		this.thenFn(succ);
	};

	const reject = err => {
		this.state = 'reject';
		if (this.catchFn) this.catchFn(err);
	};

	cb(resolve, reject);

	if (new.target) this.state = 'pending';
}

 

여기서 사용된 코드를 이해하기 위해선,

1. 생성자 함수

2. prototype 상속

3. thisBinding

과 같은 개념을 다시 한 번 살펴봐야 한다.

 

그런데 위 코드는 뭔가 아쉽다. 다음과 같은 문제가 해결되지 않았기 때문이다.

1. `finally` 메소드 없음

2. 여러개의 then, finally 메소드가 실행될 수 있고, finally는 then과 catch가 모두 실행된 후, 가장 마지막으로 실행되어야 한다.

(순서를 보장해야 함.)

3. `reject`의 인자는 `then`와 `catch`에 모두 들어올 수 있다. (실제 promise 객체의 then 메소드에는 성공 객체를 첫번째 인자로, 실패 객체를 두번째 인자로 하는 콜백이 들어온다.)

4.`catch` 발생 시 `finally`로 바로 이동한다.

 

이를 해결하기 위해, 기존의 `catch`와 `then`과 동일하게 `prototype` 상속 메소드로서 `finally`를 추가하였다.

`finally`의 콜백은 모든`then`과 `catch` 이후에 실행돼야 하기 때문에 순.서.를 보장하기 위해 각각 배열에 담았다.

 

이후 `then` 배열이 모두 끝났을 때 `finally` 배열이 실행될 수 있도록 재귀 함수를 통해 `then` 배열에 남은 콜백가 없을 때까지 현재 콜백에 성공 객체를 전달하였다. 만약에 이 성공 객체가 또 하나의 프로미스 객체라면 `resolve` 상태 객체로 변환시켜 반환한다. 왜냐하면 `then`는 프로미스를 반환하면서 연쇄적으로 `then`을 요청 할 수 있기 때문이다. 끝으로, 배열에 남은 콜백에 없다면 `finally` 배열의 요소들을 하나씩 실행하였다.

 

또한 에러 객체와 관련해서, `catch`메소드가 이미 실행되었다면 그 메소드로 실패 객체를 반환하고 그렇지 않으면 기존의 `then` 메소드 안으로 전달하는 방식으로 수정하였다.

 

function Promise(cb) {
	const thenFns = [];
	const finalFns = [];

	Promise.prototype.then = tcb => {
		if (typeof tcb === 'function') thenFns.push(tcb);
		return this;
	};

	Promise.prototype.catch = ccb => {
		if (!Promise.prototype.catchFn) Promise.prototype.catchFn = ccb;
		return this;
	};

	Promise.prototype.finally = fcb => {
		if (typeof fcb === 'function') finalFns.push(fcb);
		return this;
	};


	const finalRunner = () => {
		for (const ffn of finalFns) ffn();
	};

	const resolve = succ => {
		const recur = preRet => {
			const fn = thenFns.shift();
			if (!fn) {
				this.state = 'resolve';
				return finalRunner();
			}

			if (preRet instanceof Promise) {
				preRet.then(fn).then(res => {
					recur(res);
				});
			} else {
				recur(fn(preRet));
			}
		};

		recur(succ);
	};

	const reject = err => {
		this.state = 'reject';
		if (this.catchFn) this.catchFn(err);
		finalRunner();
	};

	cb(resolve, reject);

	if (new.target) this.state = 'pending';
}

 

이 코드는 콘솔 상에서 어떻게 나오는지 확인해보자.

이 테스트 코드를 활용해서! (간단히 확인하기 위해, `jest` 말고 커스텀으로 만든 코드를 사용했다.)

const randTime = val => {
	return new Promise(resolve => setTimeout(resolve, Math.random() * 1000, val));
};

const p = new Promise((resolve, reject) => {
	setTimeout(() => {
		const now = Date.now();
		if (now % 2 === 0) resolve(now);
		else reject(new Error('에러 발생!!!!'));
	}, 1000);
});

p.then(res => {
	console.log('p.then.res1>>>>', res);
	return randTime(1);
})
	.then(res => {
		console.log('p.then.res4>>>', res);
		return randTime(2);
	})
	.then(res => {
		console.log('p.then.res2>>>', res);
		return 'finally';
	})
	.then(console.log('p.then.res3!!!'))
	.then(res => res || 'TTT')
	.catch(err => console.error('err-1>>', err))
	.catch(err => console.error('err-2>>', err))
	.finally(() => console.log('finally-1'))
	.finally(() => console.log('finally-2'));

프로미스, 자바스크립트에서 손에 꼽히게 중요한 개념인데, 

이렇게 직접 만들어보지 않는 이상 그 원리가 어떠한지를 제대로 알 길이 없다.

 

이렇게 코드를 짜면서 몇번이고 머리를 움켜 쥐었지만 너무나 귀한 시간이었다. 언제 한 객체를 이렇게 물고 늘어져 보겠낭..

 

추후에 수정 사항이 있으면 계속적으로 고쳐나갈 예정이다.