Promise 구현하기 (1)

Tei·2020년 11월 28일
11
post-thumbnail

Promise 는 어떻게 구현할 수 있을까?

자바스크립트의 비동기 처리는 프로미스가 있기 전과 후로 나뉠 정도로, 프로미스라는 개념은 자바스크립트 진형에 많은 영향을 주었습니다.

지금이야 async await 이라는 더 직관적이고 깔끔한 문법이 나왔지만, 아직도 promise 는 꾸준한 사랑을 받고 있습니다.

오늘은 이 Promise 를 어떻게 구현할 수 있는지 알아보겠습니다.

해당 코드는 많은 선지자분들이 작성하신 좋은 선례들을 보고 작성되었고, 실제 프로미스의 구현체와는 다른, 이해를 위한 멘탈 모델정도라고 생각하시면 좋겠습니다.

코드를 작성하기 위해 참고한 링크는 아래와 같습니다.

예제코드

const asyncJob = (f) => setTimeout(f, 3000);

new Promise((res, rej) => {
  asyncJob(() => {
    res("tei");
  });
}).then((e) => console.log("나는", e));

console.log("3초후에 나올것");

해당 코드의 실행 결과는 다음과 같을 것입니다.

3초뒤에 나올것
(...3초라는 시간의 벽)
나는 tei

오류케이스의 경우는 어떨까요?

new Promise((res, rej) => {
  asyncJob(() => {
    rej(new Error("Request is failed"));
  });
})
  .then((e) => console.log("나는", e))
  .catch((e) => console.log(e));

then 이 실행되지 않고, catch 에서 받아줘야 합니다.

> 3초 후에 나올것
(...3초라는 시간의 벽)
> Error: Request is failed

그렇다면 이 예제를 가지고, 이와 동일하게 동작하는 _Promise 클래스를 한번 작성해봅시다.

가장 간단한 형태(1단계만 받을 수 있는 프로미스, 체이닝 X)

구현되어야 할 기능
  • 비동기 작업을 resolve 하면 then 으로 건내준 함수가 실행된다
  • 비동기 작업을 reject 하면 catch 로 받을 수 있다.

해야할 작업은 명확해 졌습니다. 우리는 _Promise 라는 클래스를 만들고, 그 안에 resolve,reject,then,catch

4개의 함수를 구현하면 됩니다.

프로미스 클래스의 생성자

프로미스 클래스는 resolve 함수와 reject 함수를 인자로 하는 함수를 인자로 받습니다.

new Promise((res, rej) => {
  asyncJob(() => {
    res("tei");
  });
});

프로미스 클래스의 생성자에서 초기화해줘야 할 변수는 4개 입니다.

  • 프로미스의 현재 상태( pending,fulfilled,rejected )
  • 프로미스가 resolve 하거나 reject 했을때 건내줄 값
  • resolve 했을 때 실행되어야 할 함수
  • reject 되었을 떄 실행되어야 할 함수

먼저 프로미스의 상태값을 객체로 하나 만들어주도록 하겠습니다.

const PromiseStatus = {
	PENDING: "pending", // 비동기 로직이 완료되지 않은 상태
	FULFILLED: "fulfilled", // 성공, 프로미스의 결과값이 리턴됨
	REJECTED: "rejected", // 실패, 오류발생,
};

위의 내용을 토대로 생성자 함수를 만들어 보도록 하겠습니다.

const _Promise = class {
	constructor(callBack) {
		this.status = PromiseStatus.PENDING;
		this.promiseResult = undefined;
		this.fulfilledFunc = ()=> {};
		this.rejectedFunc = ()=> {};
		callBack(
			(v) => this.resolve(v),
			(v) => this.reject(v)
		);
	}
}

크게 설명할 것 없이, 처음 상태는 pending 이고, 처음 값은 undefined, 그리고 아직 성공 실패시 어떤 일을 할지 정해지지 않았습니다. 생성자의 인자로 받은 함수는 resolve 함수와 reject 함수를 부르는데요, 이 두 함수를 구현한 후에 한번 보도록 하겠습니다.

then 의 구현

then 의 구현에 앞서, 만들어줘야 할 함수가 있습니다.

const addToTaskQueue = (t) => setTimeout(t, 0); //콜스택이 아니고, 태스크 큐에 넣기 위해

addTaskQueue 라는 한줄짜리 간단한 함수이고, 콜스택이 아닌 태스크 큐에서 실행시키기 위한 함수입니다.

태스크 큐와 이벤트 루프에 대한 내용은(https://www.youtube.com/watch?v=8aGhZQkoFbQ&ab_channel=JSConf) 라는 명 영상이 있으니, 혹시 잘 알고계시지 못한다면 해당 영상을 참고해주세요.

then 을 구현한 코드를 살펴보면

...여기는 Promise 클래스 내부입니다.
then(onFulfilled) {
  const fulfilledTask = () => {
    onFulfilled(this.promiseResult);
  };
  switch (this.status) {
    case PromiseStatus.PENDING: {
      this.fulfilledFunc = fulfilledTask;
      break;
    }
    case PromiseStatus.FULFILLED: {
      addToTaskQueue(fulfilledTask);
      break;
    }
    case PromiseStatus.REJECTED: {
      break;
    }
  }
  return this;
}

fulfilledTask 변수에 onFulfilled(this.promiseResult) 를 넣어주고 있습니다.

그리고 현재 status 가 pending 이면 성공시 실행할 함수인 fulfilledFuncfulfilledTask 를 넣어줍니다.

그리고 만일 FULFILLED되었다면 바로 태스크 큐에 넣어줍니다. (여기서는 사실 체이닝 되지 않고 1단계만 받을 수 있는 프로미스기에 해당 상태로 오는 경우는 없을것이긴 합니다..)

그리고 REJECTED 일 경우, then 에서 처리하지 않으니 아무 일도 하지 않습니다.

catch 의 구현

catch 는 then 과 결을 같이합니다. 다만, reject 상황이라는점만 다릅니다.

대부분이 중복 코드이지만, 이해를 돕기 위해 중복되게 작성해 보았습니다.

...여기는 Promise 클래스 내부입니다.	
catch(onRejected) {
  const rejectedTask = () => {
    onRejected(this.promiseResult);
  };
  switch (this.status) {
    case PromiseStatus.PENDING: {
      this.rejectedFunc = rejectedTask;
      break;
    }
    case PromiseStatus.FULFILLED: {
      break;
    }
    case PromiseStatus.REJECTED: {
      addToTaskQueue(this.rejectedFunc);
    }
  }
  return this;
}

모든것이 반대로, 같은 로직이라는 것을 확인할 수 있습니다.

resolve 의 구현

resolve 함수는 프로미스의 상태를 성공으로 바꾸고, promiseResult 의 값을 결정합니다. 그리고 resolve 되었을 때 실행해야 할 함수를 실행합니다.

...여기는 Promise 클래스 내부입니다.	

_doTask(t){
  addToTaskQueue(t)
}

resolve(v) {
  if (this.status !== PromiseStatus.PENDING) {
    return this;
  }
  this.status = PromiseStatus.FULFILLED;
  this.promiseResult = v;
  this._doTask(this.fulfilledFunc)
}
...

함수의 동작을 이해하기 위해 실행 순서를 생각해 볼 필요가 있습니다.

다시 아까의 예제를 한번 볼까요?

new Promise((res, rej) => {
 asyncJob(() => {
   res("tei");
 });
}).then((e) => console.log("나는", e));

  1. 먼저 Promise 가 생성됩니다. then 을 만나기 전이니 생성자만 불린 상태입니다.

  2. then 이 실행되었습니다. 현재 상태는 pending 상태이니,

const fulfilledTask = () => {
  onFulfilled(this.promiseResult);
};

case PromiseStatus.PENDING: {
  this.fulfilledFunc = fulfilledTask;
  break;
}

이 케이스문을 타게 되어 fulfilledFunc 의 값이 변경됩니다. 여기서는 그 값이 (e) => console.log("나는", e) 입니다.

  1. 그리고 3초의 시간이 흐른 후, resolve 함수가 실행됩니다. resolve 함수는 this.primiseResult 를 resolve 에게 준 인자로 변경합니다. 그리고 fulfilledFunc 를 실행합니다.

  1. primiseResult 는 "tei" 로 변경된 채로 fulfilledFunc 가 실행됩니다.
reject 의 구현

reject 함수는 catch 에서 잡는다는점만 제외하면 resolve 함수와 완전히 같습니다.

reject(err) {
  if (this.status !== PromiseStatus.PENDING) {
    return this;
  }
  this.status = PromiseStatus.REJECTED;
  this.promiseResult = err;
  this._doTask(this.rejectedFunc)
}

잠시 예제를 다시 가져와보면

new _Promise((res,rej) => {
  asyncJob(() => {
    rej(new Error("Request is failed"))
  });
})
  .then((e) => console.log("나는", e))
  .catch((e)=>console.log(e))

console.log("3초후에 나올것")

실행결과는 다음과 같습니다.

reject 되었기때문에, 나는 ~ 이 출력되지 않는것을 확인할 수 있습니다.

나머지

primoise 의 기본적인 형태를 구현해 보았는데, 사실 Promise 의 구현은 이렇게 간단하지는 않습니다.
가령 Promise.then 을 통해서 프로미스간 체이닝이 가능해야 하고,
Promise.all 이나 race 등의 메소드도 지원해야 합니다. 해당 구현의 경우 나중에 시간이 생기면 한번 정리해 보도록 하겠습니다.

profile
Being a service developer

3개의 댓글

comment-user-thumbnail
2020년 11월 29일

다음 글도 기대되네요. 🤭

답글 달기
comment-user-thumbnail
2020년 12월 2일

그림 기다리겠습니다:) ㅎㅎㅎ

답글 달기
comment-user-thumbnail
2020년 12월 14일

2탄 왜 안나오나요?

답글 달기