TIL 029 프로미스 직접 구현하기

조성현·2021년 10월 9일
0

지난 글에서 비동기 api를 순차적으로 보내고자 고민했었다. 자바스크립트에서 이를 쉽게 할 수 있는 방법이 바로 프로미스 체이닝이다.

이번 글에서는 프로미스를 직접 구현해보며 프로미스 체이닝을 이해하려 한다.

1.동기 체이닝 처리

우선 동기처리부터 구현해보자. 우선 테스트 코드의 동작을 상상해보면, 프로미스의 ressolve가 즉시 실행될 것이고 각 then은 "first","second", "third"를 전달 받아 사용할 것이다.

console.time("동기처리");
new MyPromise((res, rej) => res("first"))
  .then(msg => {
    console.log(msg);
    return "second";
  })
  .then(msg => {
    console.log(msg);
    return "third";
  })
  .then(msg => {
    console.log(msg);
    console.timeEnd("동기처리");
  });

MyPromise를 선언하고, 현재 상태(pending,resolved,recjected)를 나타내는 state, 프로미스가 resolve, reject됐을때의 값을 담을 value, 체이닝으로 다음 프로미스를 담아둘 chain 을 선언한다.

class MyPromise {
  constructor(callback) {
    this.state = "pending";
    this.value = undefined;
    this.chain = undefined;
    callback(this.resolve.bind(this), this.reject.bind(this));
  }

resolve 되었을때는 value와 state를 바꿔주고 다음 프로미스 체인이 있다면 실행시킨다.
reject 되었을때도 우선은 같게 해주겠다.

  resolve(value) {
    this.value = value;
    this.state = "resolved";
    this.chain?.();
  }

  reject(value) {
    this.value = value;
    this.state = "rejected";
    this.chain?.();
  }

then 에서는 실제로도 프로미스를 반환한다. 이때 then의 함수를 resolve안에 넣어두고 이 resolve 함수를 앞의 프로미스의 chain에 넣어주는 것이다. 즉, 앞에 프로미스가 resolve되면서 this.chain을 실행시킬것이고 이때 then 프로미스의 resolve 실행되면서 넣어주었던 콜백함수를 연쇄적으로 실행시킬 것이다. 트리거를 앞에다 넣어둔다고 생각하자.

만약 현재 테스트 케이스처럼 동기적이라면 이미 모두 resolved 됬을 것이고 chain에 넣어주고 resolve를 기다릴 필요없이, 새로운 프로미스를 바로 resolve 시키면 된다.

  then(callback) {
    if (this.state === "pending") {
      return new MyPromise(resolve => (this.chain = () => resolve(callback(this.value))));
    }
    if (this.state === "resolved") {
      return new MyPromise(resolve => resolve(callback(this.value)));
    }
    return this;
  }
}

2.비동기 체이닝 처리

비동기 작업 프로미스로 이루어져 있다면 어떨까? 저번 글과 같이 api의 응답을 연쇄적으로 사용해야한다면?

console.time("비동기처리");
new MyPromise((res, rej) => setTimeout(() => res("first"), 1000))
  .then(msg => {
    console.log(msg);
    return new MyPromise((res, rej) => setTimeout(() => res("second"), 1000));
  })
  .then(msg => {
    console.log(msg);
    return new MyPromise((res, rej) => setTimeout(() => res("third"), 1000));
  })
  .then(msg => {
    console.log(msg);
    console.timeEnd("비동기처리");
  });

이제는 then이 값을 return 해주는 것이 아니라, 새 프로미스를 리턴해주고 있다. 이에 대한 예외처리가 필요하다.

resolve함수 부분을 수정하여 value가 프로미스라면 그 프로미스가 resolve 될 때 비로소 나(프로미스)를 resolve 시키도록 하였다. 즉 inner 프로미스가 응답할때까지 기다리는 것이다.

 resolve(value) {
    if (value instanceof MyPromise) {
      value.then(innerPromiseValue => this.setThisPromiseValue(innerPromiseValue));
    } else {
      this.setThisPromiseValue(value);
    }
  }

  setThisPromiseValue(value) {
    this.value = value;
    this.state = "resolved";
    this.chain?.();
  }

모든 프로미스의 응답을 기다리며 순차적으로 실행되어 3초가 소모된 모습이다.

3.catch로 예외처리 하기

비동기 작업에서의 예외처리는 쉽지는 않지만, 실제 프로미스는 .catch 메소드로 에러처리를 할 수 있다.

테스트 코드를 보면, 두번째 프로미스에서 rej로 에러를 반환해 3번째 프로미스는 실행되지 않고 catch로 갈 것이다.

console.time("에러처리");
new MyPromise((res, rej) => setTimeout(() => res("first"), 1000))
  .then(msg => {
    console.log(msg);
    return new MyPromise((res, rej) => setTimeout(() => rej("에러"), 1000));
  })
  .then(msg => {
    console.log(msg);
    return new MyPromise((res, rej) => setTimeout(() => res("third"), 1000));
  })
  .then(msg => {
    console.log(msg);
    return msg;
  })
  .catch(err => {
    console.log(err);
    console.timeEnd("에러처리");
  });

catch안의 콜백 함수는 chain이 아닌 errorHandler에서 보관할 것이다.
실제는 promise가 resolve될지 reject될지 알 수 없기 때문에 모든 체인을 연결해줘야한다.그리고 비동기처리시 에러는 try catch로 감지해 reject를 실행시킨다.

class MyPromise {
  constructor(callback) {
    this.state = "pending";
    this.value = undefined;
    this.chain = undefined;
    this.errorHandler = undefined;
    try {
      callback(this.resolve.bind(this), this.reject.bind(this));
    } catch (error) {
      this.reject(error);
    }
  }

만약 value가 프로미스라면 이 inner 프로미스의 에러도 catch하여 나(프로미스)의 rejcet 시켜야한다.

  resolve(value) {
    if (value instanceof MyPromise) {
      value
        .then(innerPromiseValue => this.setThisPromiseValue(innerPromiseValue))
        .catch(innerPromiseValue => this.reject(innerPromiseValue));
    } else {
      this.setThisPromiseValue(value);
    }
  }

  setThisPromiseValue(value) {
    this.value = value;
    this.state = "resolved";
    this.chain?.();
  }

reject시에는 이제 errorhandler를 실행시킨다.

  reject(value) {
    this.value = value;
    this.state = "rejected";
    this.errorHandler?.();
  }

then 실행시 앞 프로미스의 chain에 내 프로미스의 resolve안에 콜백을 넣어두었었다. 만약 앞의 프로미스에 reject이 발생해 chain이 아닌 errorHandler를 실행시킨다면, then 은 콜백을 실행시키지 않고 reject만 전달해주면 된다.

그래서 erroHandler에는 then의 콜백은 실행시키지 않고 reject만 시켜 값만 전달한다.

  then(callback) {
    if (this.state === "pending") {
      return new MyPromise((resolve, reject) => {
        this.chain = () => resolve(callback(this.value));
        this.errorHandler = () => reject(this.value);
      });
    }
    if (this.state === "resolved") {
      return new MyPromise(resolve => resolve(callback(this.value)));
    }
    return this;
  }

.catch가 실행된다면 then의 반대다. chain에는 resolve만 실행시켜 값만 전달하고( 앞에 reject가 없기 때문에 값만 전달한다.) errorHandler에는 catch 콜백도 같이 실행시킨다. 이로서 앞에서 전달된 reject가 있으면 catch가 실행될 것이다.

  catch(callback) {
    if (this.state === "pending") {
      return new MyPromise((resolve, reject) => {
        this.chain = () => resolve(this.value);
        this.errorHandler = () => reject(callback(this.value));
      });
    }
    if (this.state === "reject") {
      return new MyPromise((resolve, reject) => reject(callback(this.value)));
    }
    return this;
  }
}

세 번째 프로미스는 실행되지 않고 건너뛴 모습이다.

전체코드

이로서, 나만의 프로미스가 어느 정도 역할을 할 수 있게 되었다. then, catch 끼리 프로미스 의 resolve로 연결되어 있고, value에도 프로미스를 활용할 수 있다는 점이 핵심이라고 생각한다.

class MyPromise {
  constructor(callback) {
    this.state = "pending";
    this.value = undefined;
    this.chain = undefined;
    this.errorHandler = undefined;
    try {
      callback(this.resolve.bind(this), this.reject.bind(this));
    } catch (error) {
      this.reject(error);
    }
  }

  resolve(value) {
    if (value instanceof MyPromise) {
      value
        .then(innerPromiseValue => this.setThisPromiseValue(innerPromiseValue))
        .catch(innerPromiseValue => this.reject(innerPromiseValue));
    } else {
      this.setThisPromiseValue(value);
    }
  }

  setThisPromiseValue(value) {
    this.value = value;
    this.state = "resolved";
    this.chain?.();
  }

  reject(value) {
    this.value = value;
    this.state = "rejected";
    this.errorHandler?.();
  }

  then(callback) {
    if (this.state === "pending") {
      return new MyPromise((resolve, reject) => {
        this.chain = () => resolve(callback(this.value));
        this.errorHandler = () => reject(this.value);
      });
    }
    if (this.state === "resolved") {
      return new MyPromise(resolve => resolve(callback(this.value)));
    }
    return this;
  }

  catch(callback) {
    if (this.state === "pending") {
      return new MyPromise((resolve, reject) => {
        this.chain = () => resolve(this.value);
        this.errorHandler = () => reject(callback(this.value));
      });
    }
    if (this.state === "reject") {
      return new MyPromise((resolve, reject) => reject(callback(this.value)));
    }
    return this;
  }
}

더 추가될 부분

finally 메소드도 추가해야한다. constructor에 finally의 resolve와 콜백을 담도록 추가해주고 상황에 따라(마지막 프로미스일때) 이를 실행시키도록 하면 될 것 같다.

또한, then에서 에러가 일어나서 reject 되었는데 이를 처리하는 catch 가 없다면 이에 대해 에러를 일으키도록 제외처리를 추가해야될 것 같다.

기회가 되면 더 구현해보도록하겠다.

profile
Jazzing👨‍💻

0개의 댓글

관련 채용 정보