101 시리즈: 프라미스 #2: 현재 프라미스 상태를 얻고 자신만의 프라미스 큐를 만드는 방법?

첫 번째 기사에서는 약속에 대한 기본 정보를 설명했습니다.
더 복잡한 주제에 도달하기 전에 Promise 위에 만들 수 있는 도구와 유틸리티 기능에 대해 생각해 볼 것을 제안합니다. 이 기사에서는 현재 약속 상태를 가져오고 약속 대기열을 만드는 방법에 대해 생각해 보겠습니다.

TL&DR



약속 상태 확인


const pending = {
  state: 'pending',
};

function getPromiseState(promise) {
  // We put `pending` promise after the promise to test, 
  // which forces .race to test `promise` first
  return Promise.race([promise, pending]).then(
    (value) => {
      if (value === pending) {
        return value;
      }
      return {
        state: 'resolved',
        value
      };
    },
    (reason) => ({ state: 'rejected', reason })
  );
}



약속 대기열


class Queue {
  _queue = Promise.resolve();

  enqueue(fn) {
    const result = this._queue.then(fn);
    this._queue = result.then(() => {}, () => {});

    // we changed return result to return result.then() 
    // to re-arm the promise
    return result.then();
  }

  wait() {
    return this._queue;
  }
}



현재 약속 상태를 얻는 방법



약속을 생성한 후에는 약속이 여전히 pending , fulfilled 또는 rejected 인지 런타임에 정보를 얻을 수 없습니다.

디버거에서 현재 약속 상태를 볼 수 있습니다.


디버거에서 Symbol[[PromiseState]]이 이를 담당하고 있음을 알 수 있습니다. 그러나 PromiseStatewell-known symbol 이 아니므로 이 상태를 "당장"가져올 수 없는 비공개 필드로 취급할 수 있습니다.

그러나 Promise.race를 사용하여 현재 약속 상태를 테스트할 수 있습니다. 이를 위해 Promise.race의 기능을 사용할 수 있습니다.

📝 Promise.race는 순서대로 약속을 확인합니다. 예를 들어:

const a = Promise.resolve(1);
const b = Promise.resolve(2);
Promise.race([a, b]).then(console.log); // 1


당시:

const a = Promise.resolve(1);
const b = Promise.resolve(2);
Promise.race([b, a]).then(console.log); // 2


📝 이 코드는 약속 상태를 테스트합니다.

const pending = {
  state: 'pending',
};

function getPromiseState(promise) {
  // We put `pending` promise after the promise to test, 
  // which forces .race to test `promise` first
  return Promise.race([promise, pending]).then(
    (value) => {
      if (value === pending) {
        return value;
      }
      return {
        state: 'resolved',
        value
      };
    },
    (reason) => ({ state: 'rejected', reason })
  );
}


사용법https://codesandbox.io/s/restless-sun-njun1?file=/src/index.js

(async function () {
  let result = await getPromiseState(Promise.resolve("resolved hello world"));
  console.log(result);

  result = await getPromiseState(Promise.reject("rejected hello world"));
  console.log(result);

  result = await getPromiseState(new Promise(() => {}));
  console.log(result);

  result = await getPromiseState("Hello world");
  console.log(result);
})();


확인 외에도 Promise.race의 이 기능은 시간 초과가 있는 일부 코드를 실행하는 데 유용할 수 있습니다. 예:

const TIMEOUT = 5000;
const timeout = new Promise((_, reject) => setTimeout(() => reject('timeout'), TIMEOUT));

// We may want to test check if timeout is rejected 
// before time consuming operations in the async code
// to cancel execution
async function someAsyncCode() {/*...*/}

const result = Promise.race([someAsyncCode(), timeout]);


자신만의 Promise Queue를 만드는 방법



때로는 서로 다른 코드 블록을 어떤 순서로 차례로 실행해야 합니다. 순차적으로 실행하려는 무거운 비동기 블록이 많을 때 유용합니다. 우리는 다음을 기억해야 합니다.
📝 JS는 단일 스레드이므로 동시 실행은 가능하지만 병렬은 아닙니다.

이를 위해 Promise Queue를 구현할 수 있습니다. 이 대기열은 이전에 추가된 모든 비동기 함수 뒤에 모든 함수를 넣습니다.

이 코드 블록을 확인해 보겠습니다.

class Queue {
  // By default the queue is empty
  _queue = Promise.resolve();

  enqueue(fn) {
    // Plan new operation in the queue
    const result = this._queue.then(fn);

    // avoid side effects. 
    // We can also provide an error handler as an improvement
    this._queue = result.then(() => {}, () => {});

    // To preserve promise approach, let's return the `fn` result
    return result;
  }

  // If we want just to understand when the queue is over
  wait() {
    return this._queue;
  }
}


테스트해 봅시다:

// Work emulator
const emulateWork = (name, time) => () => {
    console.log(`Start ${name}`);
    return new Promise((resolve) => {setTimeout(() => console.log(`End ${name}`) || resolve(), time)})
}

// Let's check if the queue works correctly if the promise fails
const failTest = () => () => {
    console.log(`Start fail`);
    return Promise.reject();
}
const queue = new Queue();
queue.enqueue(emulateWork('A', 500));
queue.enqueue(emulateWork('B', 500));
queue.enqueue(emulateWork('C', 900));
queue.enqueue(emulateWork('D', 1200));
queue.enqueue(emulateWork('E', 200));
queue.enqueue(failTest());
queue.enqueue(emulateWork('F', 900));


결과는 다음과 같습니다.


그러나 Promise가 거부되면 오류를 완전히 삼켰습니다.
즉, 실제 코드가 실패하면 우리는 그것에 대해 결코 알 수 없습니다.

📝 프라미스로 작업하는 경우 프라미스 체인의 끝에 .catch를 사용해야 합니다. 그렇지 않으면 애플리케이션의 실패를 놓칠 수 있습니다!

상황을 해결하기 위해 생성자에 대한 콜백을 제공할 수 있습니다.

class Queue {
  _queue = Promise.resolve();

  // By default onError is empty
  _onError = () => {};

  constructor(onError) {
    this._onError = onError;
  }

  enqueue(fn) {
    const result = this._queue.then(fn);

    this._queue = result.then(() => {}, this._onError);

    return result;
  }

  wait() {
    return this._queue;
  }
}


아니면 우리는 약속을 다시 무장시킬 수 있습니다!

class Queue {
  _queue = Promise.resolve();

  enqueue(fn) {
    const result = this._queue.then(fn);
    this._queue = result.then(() => {}, () => {});

    // we changed return result to return result.then() 
    // to re-arm the promise
    return result.then();
  }

  wait() {
    return this._queue;
  }
}


동일한 테스트에서 오류가 보고됩니다!


📝 모든 약속 체인은 unhandledRejection 오류를 일으킬 수 있습니다.

우리가 만든 코드를 실험해보고 싶다면: https://codesandbox.io/s/adoring-platform-cfygr5?file=/src/index.js

마무리



Promise는 복잡한 비동기 상호 작용을 해결해야 할 때 매우 유용합니다.

다음 기사에서는 JS Promise가 때때로 thenable 객체로 불리는 이유에 대해 설명하고 실험을 계속할 것입니다.

좋은 웹페이지 즐겨찾기