JavaScript Promise 구현하기

41013 단어 JavaScriptJavaScript

Promise 기본 동작 살펴보기

Promise 객체는 다음과 같은 세 가지의 상태를 지닐 수 있다.

  • pending
  • fulfilled
  • rejected

최초로 Promise 객체가 생성되면 pending 상태가 된다. resolve되면 fulfilled 상태가 되고, 반대로 reject되면 rejected 상태가 된다.

조금 더 자세히, 아래와 같이 정리할 수 있다.

  • Promise는 최초로 생성되었을 시점에 pending 상태이다.
  • Promise가 v라는 값으로 resolve 되었다면, Promise는 fulfilled 상태가 되며, v를 fulfillment value라고 부른다.
  • Promise가 e라는 에러로 reject 되었다면, Promise는 rejected 상태가 되며, e를 rejection value라고 부른다.

Promise 객체의 처리 흐름은 위와 같다. 위 그림에서 주의 깊게 볼 부분은 다음과 같다.

then()catch()가 리턴될 때 Pending이라는 초기 상태를 갖는 Promise 객체를 반환한다. 이를 통해 Promise Chaining이 가능해진다는 점이다.

Promise에서 사용되는 resolve(), reject(), then(), catch() 이 네 메서드를 직접 구현함으로써 Promise 클래스와 유사한 동작을 하도록 구현할 수 있다.

직접 구현해보기

const PromiseStatus = {
  PENDING: "pending",
  FULFILLED: "fulfilled",
  REJECTED: "rejected",
};

const addToTaskQueue = callback => setTimeout(callback, 0);
const addAllToTaskQueue = fulfilledQueue => fulfilledQueue.forEach(callback => addToTaskQueue(callback));
  • PromiseStatus : Promise의 상태를 표기하기 위한 객체이다.
  • addToTaskQueue : 태스크 큐에 콜백 함수를 추가해주는 역할을 한다.
  • addAllToTaskQueue : fulfilledQueue에 위치하는 모든 콜백 함수들을 차례로 꺼내, 태스크 큐에 넣어주는 역할을 한다.
class MyPromise {
  constructor(callback) {
    this.status = PromiseStatus.PENDING;
    this.result = undefined;
    this.fulfilledQueue = [];
    this.rejectedQueue = [];

    callback(this.resolve.bind(this), this.reject.bind(this));
  }

	...
}

다음으로 MyPromise 클래스를 만든 뒤, 위와 같이 생성자를 작성한다.

  • status : Promise 객체의 초기 상태 Pending을 지닌다.
  • result : Promise 객체가 resolve 될 때의 fulfillment value 값을 의미한다.
  • fulfilledQueue : Promise가 이행될 때 호출되는 콜백 함수들이 위치하는 큐이다.
  • rejectedQueue : Promise가 거부될 때 호출되는 콜백 함수들이 위치하는 큐이다.
  • Promise 생성자 호출시 전달되는 callback 내부에서 resolve(), reject() 호출시, 프로미스 객체에 대한 정보를 넘겨주기 위해 bind를 사용한다.
class MyPromise {
	...

	then(onFulfilled) {
    switch (this.status) {
      case PromiseStatus.PENDING:
        return new MyPromise(resolve => {
          this.fulfilledQueue.push(() => resolve(onFulfilled(this.result)));
        });
      case PromiseStatus.FULFILLED:
        return new MyPromise(resolve => resolve(onFulfilled(this.result)));
    }

    return this;
  }

	...
}

then 메서드 구현부는 위와 같다. then() 호출시 파라미터(콜백 함수)를 onFulfilled로 받는다.

  • 프로미스 객체의 상태 값이 Pending이면, 새로운 MyPromise 객체를 만들며, 동시에 fulfilledQueue에 콜백 함수들을 넣어둔다.
  • 프로미스 객체의 상태 값이 Fulfilled이면, 새로운 MyPromise 객체를 만들어 리턴한다.
class MyPromise {
	...

	catch(onRejected) {
    const rejectedTask = () => {
      onRejected(this.result);
    };

    switch (this.status) {
      case PromiseStatus.PENDING:
        this.rejectedFunc = rejectedTask;
        break;
      case PromiseStatus.REJECTED:
        addToTaskQueue(rejectedTask);
    }
  }

	...
}

catch 메서드 구현부는 위와 같다. 거부될 때 실행할 콜백 함수 onRejected를 파라미터로 받으며, 프로미스 객체 상태가 Rejected일 때 태스크 큐에 콜백을 넣어준다.

class MyPromise {
	...
	
	resolve(val) {
    if (this.status !== PromiseStatus.PENDING) {
      return this;
    }

    this.status = PromiseStatus.FULFILLED;
    this.result = val;
    addAllToTaskQueue(this.fulfilledQueue);
  }	

	...
}

resolve 메서드 내부에선 다음과 같이 동작한다.

  • 프로미스 객체 상태를 Fulfilled로 변경한다.
  • 파라미터로 전달된 val을 result에 담는다.
  • 이행 시 실행할 함수들이 담긴 fulfilledQueue 의 모든 함수들을 꺼내 태스크 큐에 넣어준다.
class MyPromise {
	...
	
	reject(error) {
    if (this.status !== PromiseStatus.PENDING) {
      return this;
    }

    this.status = PromiseStatus.REJECTED;
    this.result = error;
    addToTaskQueue(this.rejectedFunc);
  }
}

reject 메서드 동작은 resolve와 유사하다.

문제점

Promise 깊이가 깊어지니 제대로 작동하지 않는다.

new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve("First");
  }, 1000);
})
  .then(res => {
    console.log(res); // First

    return new MyPromise((resolve, reject) => {
      setTimeout(() => {
        resolve("Second");
      }, 2000);
    });
  })
  .then(res => {
    console.log(res); // MyPromise { status: 'pending', result: undefined, ... }
  });

위처럼 then 안에서 새로운 Promise 객체를 리턴하게 되는 경우, 다음에 받는 then에서 객체 그 자체가 출력된다.

해결 방법

포스트를 참고해서 위의 문제점을 해결할 수 있었다.

resolve(value) {
  if (this.status !== PromiseStatus.PENDING) {
    return this;
  }

  if (value instanceof MyPromise) {
    value.then(innerPromiseValue => {
      this.status = PromiseStatus.FULFILLED;
      this.result = innerPromiseValue;
      addAllToTaskQueue(this.fulfilledQueue);
    });
  } else {
    this.status = PromiseStatus.FULFILLED;
    this.result = value;
    addAllToTaskQueue(this.fulfilledQueue);
  }
}

resolve 메서드의 파라미터가 프로미스 객체라면, 내부에서 then()을 호출시킴으로써, 실행시킨 프로미스 객체 내부의 result를 참조할 수 있게 되었다.

전체 코드

const PromiseStatus = {
  PENDING: "pending",
  FULFILLED: "fulfilled",
  REJECTED: "rejected",
};

const addToTaskQueue = callback => setTimeout(callback, 0);
const addAllToTaskQueue = fulfilledQueue => fulfilledQueue.forEach(callback => addToTaskQueue(callback));

class MyPromise {
  constructor(callback) {
    this.status = PromiseStatus.PENDING;
    this.result = undefined;
    this.fulfilledQueue = [];
    this.rejectedQueue = [];

    callback(this.resolve.bind(this), this.reject.bind(this));
  }

  then(onFulfilled) {
    switch (this.status) {
      case PromiseStatus.PENDING:
        return new MyPromise(resolve => {
          this.fulfilledQueue.push(() => resolve(onFulfilled(this.result)));
        });
      case PromiseStatus.FULFILLED:
        return new MyPromise(resolve => resolve(onFulfilled(this.result)));
    }

    return this;
  }

  catch(onRejected) {
    const rejectedTask = () => {
      onRejected(this.result);
    };

    switch (this.status) {
      case PromiseStatus.PENDING:
        this.rejectedFunc = rejectedTask;
        break;
      case PromiseStatus.REJECTED:
        addToTaskQueue(rejectedTask);
    }
  }

  resolve(value) {
    if (this.status !== PromiseStatus.PENDING) {
      return this;
    }

    if (value instanceof MyPromise) {
      value.then(innerPromiseValue => {
        this.status = PromiseStatus.FULFILLED;
        this.result = innerPromiseValue;
        addAllToTaskQueue(this.fulfilledQueue);
      });
    } else {
      this.status = PromiseStatus.FULFILLED;
      this.result = value;
      addAllToTaskQueue(this.fulfilledQueue);
    }
  }

  reject(error) {
    if (this.status !== PromiseStatus.PENDING) {
      return this;
    }

    this.status = PromiseStatus.REJECTED;
    this.result = error;
    addToTaskQueue(this.rejectedFunc);
  }
}

new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve("First");
  }, 1000);
})
  .then(res => {
    console.log(res);

    return new MyPromise((resolve, reject) => {
      setTimeout(() => {
        resolve("Second");
      }, 2000);
    });
  })
  .then(res => {
    console.log(res);
  });

Reference

좋은 웹페이지 즐겨찾기