탄탄한 기본기!

Promise 내부 구현 따라해보기 본문

개인 공부/JS (자바스크립트)

Promise 내부 구현 따라해보기

두영두영 2021. 7. 2. 11:28

 

자바스크립트에는 Promise라는 아주 중요한 객체가 있다. 내부적으로 비동기적인 로직을 처리할 때 이 Promise객체를 통해서 callback hell이나 에러 처리 등 여러 가지 문제점들을 해결해주고 있다.

// callback hell
get('/step1', a => {
  get(`/step2/${a}`, b => {
    get(`/step3/${b}`, c => {
      get(`/step4/${c}`, d => {
        console.log(d);
      });
    });
  });
});

이러한 코드를 Promise를 사용해 바꾸면 다음과 같다.

promiseGet(`/step/1`)
  .then(a => promiseGet(`/step/${a}`))
  .then(b => promiseGet(`/step/${b}`))
  .then(c => promiseGet(`/step/${c}`))
  .catch(err => console.error(err));

코드도 훨씬 간결하며 보기도 좋다. 그럼 이러한 Promise 객체를 사용하는 법은 어떻게 될까? 간단한 예시로 setTimeout을 통해서 2초 후 숫자를 resolve해주는 프로미스 객체를 만들어 보도록 하겠다.

const numberPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(10);
  }, 2000);
});

console.log(numberPromise); // {state: "PENDING", ...}

 

즉 프로미스는 콜백함수를 인수로 넘겨주고, 콜백 함수에 주입받는 첫 번째와 두 번째 인수로 각각 (비동기) 처리를 성공했을 경우와 실패했을 경우에 실행할 콜백 함수를 넘겨받는다. 그 후 후, 조건에 맞도록 resolve(첫 번째 매개변수) 또는 reject(두 번째 매개변수)를 실행하는 것이다.

 

본인의 경우에는 callback함수가 callback함수를 받아 사용하고 인수를 넘겨주고 하는 것이 매우 복잡해보여서 처음에 이해하는데 꽤 오래 걸렸다. 하지만 Promise가 어떤 방식으로 동작하는지 알아보기 위해서 직접 (전혀 안 똑같겠지만) 한번 비슷(?)하게라도 동작하도록 직접 구현해보았다.

function myPromise(callback) {
  this.state = 'PENDING';
  this.value;
}

일단 이렇게 상태와 resolve할 값 두 가지 상태를 가진다고 생각하였다.

 

그리고 전달받은 callback함수에 인수를 두 개 넘겨주어야 하는데, 바로 reslover와 rejector 함수이다.

function myPromise(callback) {
  this.state = 'PENDING';
  this.value;

  const resolver = value => {
    if (this.state !== 'PENDING') return;
    this.state = 'fullfilled';
    this.value = value;
  };

  const rejector = value => {
    if (this.state !== 'PENDING') return;
    this.state = 'rejected';
    this.value = value;
  };
}

이 함수는 Promise를 생성할 때 넘겨주는 콜백 함수 내부에서 호출되는데, 이때 값을 하나 넘겨받기 때문에 value라는 매개변수를 통해서 이를 지정해주었다.

 

그리고 이 함수들을 화살표 함수로 작성한 이유는, 나중에 전달받은 callback을 실행시킬 때 this 바인딩을 자신의 상위 스코프의 this로 바인딩해주기 위해서이다. 만약 함수 표현식이나 선언문으로 작성했다면 Function.prototype.bind 메서드로 this를 바인딩해주어야 하는 번거로움이 있었을 것이다.

 

그리고 이 후, 넘겨받은 callback에 resolver와 rejector를 넘겨주며 실행한다.

function myPromise(callback) {
  this.state = 'PENDING';
  this.value;

  const resolver = value => {
    if (this.state !== 'PENDING') return;
    this.state = 'fullfilled';
    this.value = value;
  };

  const rejector = value => {
    if (this.state !== 'PENDING') return;
    this.state = 'rejected';
    this.value = value;
  };

  callback(resolver, rejector);
}

그리고 여기까지 잘 되었는지 테스트를 해보겠다. 테스트는 알아보기 쉽도록 브라우저 콘솔을 활용했다.

...

const myProm = new myPromise((resolve, reject) => {
  setTimeout(() => {
    resolve('RESOLVED');
  }, 2000);
});

직접 구현한 Promise 테스트

그럼 위 그림과 같이 여기까지는 의도한 대로 잘 동작하는 것을 확인할 수 있다. 그럼 이제 Promise의 then 메서드까지 한번 비슷(?)하게 동작하도록 간단하게 정도만 구현해보겠다.

 

먼저, then이라는 함수는 Promise 생성자 함수가 평가될 때 실행되는 것이 아니라 비동기적으로 resolver가 동작한 이후에, 즉 상태가 "fullfilled"로 변환된 이후에 호출되는 것이기 때문에 변수를 따로 두어 함수 값을 가질 수 있도록 하였다.

function myPromise(callback) {
  this.state = 'PENDING';
  this.value;
  this.thenFunc;

  this.then = thenCallback => {
    this.thenFunc = thenCallback;
  };

  const resolver = value => {
    if (this.state !== 'PENDING') return;
    this.state = 'fullfilled';
    this.value = value;
  };

  const rejector = value => {
    if (this.state !== 'PENDING') return;
    this.state = 'rejected';
    this.value = value;
  };

  callback(resolver, rejector);
}

그리고, 상태가 "fullfilled"로 변화했을 때, 즉 resolver가 호출이 되었을 때 then 메서드가 동작하도록 비동적인 로직을 구현해야 했으므로 여기에서는 resolver가 호출이 되었을 때 thenFunc를 호출할 수 있도록 해주었다.

 

then에 넘겨주는 콜백 함수는 매개변수로 reslove된 값을 하나 넘겨받기 때문에(then 함수의 첫 번째 인수로 relove 되었을 때 실행할 함수, 두 번째 인수로 reject 되었을 때 실행할 함수 총 2개의 인수를 넘겨준다.) 넘겨받은 콜백 함수의 인수에 this.value를 넘겨줄 수 있도록 하였다.

function myPromise(callback) {
  this.state = 'PENDING';
  this.value;
  this.thenFunc;
  this.catchFunc;

  this.then = (resolveCallback, rejectCallback) => {
    this.thenFunc = resolveCallback;
    this.catchFunc = rejectCallback;
  };

  const resolver = value => {
    if (this.state !== 'PENDING') return;
    this.state = 'fullfilled';
    this.value = value;
    this.thenFunc(this.value);
  };

  const rejector = value => {
    if (this.state !== 'PENDING') return;
    this.state = 'rejected';
    this.value = value; // reason
    this.catchFunc(this.value); // reason
  };

  callback(resolver, rejector);
}

그리고 이제 아까 했던 테스트에 then까지 함께 테스트하면 다음과 같다.

myPromise의 then까지 테스트

Promise를 조금 더 잘 이해하기 위해서 이런 방식으로 실제 Promise와 비슷하게 동작하는 나만의 Promise 객체를 한 번 만들어 보았다.

 

하지만 myPromise의 경우에는 문제점이 있는데, new myPromise로 생성한 객체에 then 메서드를 호출하지 않을 경우에는 오류 메세지가 발생하게 된다. 왜냐하면 resolve가 될 때 함께 thenFunc를 호출하는데, then 메서드가 호출되지 않았다면 thenFunc가 undefined이기 때문이다.

 

이러한 문제를 바로잡기 위해서는 thenFunc를 resolve가 호출할 때 상태를 체크해서 예외 처리를 해주거나, thenFunc의 기본 함수를 지정해 예외를 처리하는 방식을 사용하면 될 것이다.

 

이 부분에 대해서는 Promise implementation을 키워드로 검색을 통하여 내부적으로 어떻게 동작하는지에 대해 더 자세하게 학습한 후 다시 구현해볼 예정이다.

Comments