Implementing Promises In JavaScript - Lee-hyuna/33-js-concepts-kr GitHub Wiki

์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ์—์„œ Promises ๊ตฌํ˜„

ํ”„๋กœ๊ทธ๋ž˜๋ฐ์— ๋Œ€ํ•ด ๋‚ด๊ฐ€ ๊ฐ€์žฅ ์ข‹์•„ํ•˜๋Š” ๊ฒƒ์€ aha(์•„~)์ž…๋‹ˆ๋‹ค! ๊ฐœ๋…์„ ์™„์ „ํžˆ ์ดํ•ดํ•˜๊ธฐ ์‹œ์ž‘ํ•˜๋Š” ์ˆœ๊ฐ„์ž…๋‹ˆ๋‹ค.
๋น„๋ก ๊ฑฐ๊ธฐ๊นŒ์ง€ ๊ฐ€๋Š”๋ฐ ์˜ค๋žœ ์‹œ๊ฐ„์ด ๊ฑธ๋ฆฌ๊ณ  ์ž‘์€ ๋…ธ๋ ฅ๋„ ์—†์„์ง€๋ผ๋„, ๊ทธ๊ฒƒ์€ ํ™•์‹คํžˆ ๊ฐ€์น˜๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

์ฃผ์–ด์ง„ ์ฃผ์ œ์— ๋Œ€ํ•œ ์šฐ๋ฆฌ์˜ ์ดํ•ด ์ •๋„๋ฅผ ํ‰๊ฐ€ํ•˜๊ณ  ๊ฐœ์„ ํ•˜๋Š”๋ฐ ๊ฐ€์žฅ ํšจ๊ณผ์ ์ธ ๋ฐฉ๋ฒ•์€ ์ง€์‹์„ ํ˜„์‹ค ์„ธ๊ณ„์— ์ ์šฉํ•˜๋ ค๊ณ  ์‹œ๋„ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ์•ฝ์ ์„ ํŒŒ์•…ํ•˜๊ณ  ๊ถ๊ทน์ ์œผ๋กœ ํ•ด๊ฒฐํ•  ์ˆ˜์žˆ์„๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ž‘๋™ ๋ฐฉ์‹์— ๋Œ€ํ•œ ์ •๋ณด๋„ ์–ป์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฐ„๋‹จํ•œ ์‹œํ–‰ ์ฐฉ์˜ค ์ ‘๊ทผ๋ฒ•์€ ์ข…์ข… ์ด์ „์— ์• ๋งคํ–ˆ๋˜ ์„ธ๋ถ€ ์‚ฌํ•ญ์„ ๋“œ๋Ÿฌ๋ƒ…๋‹ˆ๋‹ค.

์ด๋ฅผ ์—ผ๋‘์— ๋‘๊ณ  Promises๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ฐฐ์šฐ๋Š” ๊ฒƒ์ด ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๊ณผ์ •์—์„œ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ์ˆœ๊ฐ„ ์ค‘ ํ•˜๋‚˜๋ผ๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค. ๋น„๋™๊ธฐ ์ฝ”๋“œ์˜ ์ž‘๋™ ๋ฐฉ์‹์— ๋Œ€ํ•œ ๊ท€์ค‘ํ•œ ํ†ต์ฐฐ๋ ฅ์„ ์–ป์—ˆ๊ณ  ์ „๋ฐ˜์ ์ธ ํ”„๋กœ๊ทธ๋ž˜๋จธ๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

์ด ๊ธ€์ด ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ์—์„œ Promises๋ฅผ ๊ตฌํ˜„ํ•˜๋Š”๋ฐ ๋„์›€์ด๋˜๊ธฐ๋ฅผ ๋ฐ”๋ž๋‹ˆ๋‹ค.

Bluebird API์˜ ๋ช‡๊ฐ€์ง€ ๋ฐฉ๋ฒ•์œผ๋กœ Promises / A+ ์‚ฌ์–‘์— ๋”ฐ๋ผ Promise core๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์— ์ค‘์ ์„ ๋‘˜ ๊ฒƒ์ž…๋‹ˆ๋‹ค.
๋˜ํ•œ Jest์™€ ํ•จ๊ป˜ TDD๋ฐฉ์‹์„ ์‚ฌ์šฉํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

TypeScript๋„ ์œ ์šฉ ํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

์—ฌ๊ธฐ์„œ ์šฐ๋ฆฌ๊ฐ€ ๊ตฌํ˜„ ๊ธฐ์ˆ ์„ ์—ฐ๊ตฌํ•˜๊ณ  ์žˆ๋‹ค๋Š” ์ ์„ ๊ฐ์•ˆํ•  ๋•Œ, Promises๊ฐ€ ๋ฌด์—‡์ธ์ง€์— ๋Œ€ํ•œ ๊ธฐ๋ณธ์ ์ธ ์ดํ•ด์™€ ์ž‘๋™ ๋ฐฉ์‹์— ๋Œ€ํ•œ ๋ง‰์—ฐํ•œ ์ƒ๊ฐ์„ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๊ทธ๋ ‡์ง€ ์•Š๋‹ค๋ฉด, ์—ฌ๊ธฐ ์‹œ์ž‘ํ•˜๊ธฐ์— ์ข‹์€ ๊ณณ์ด ์žˆ์Šต๋‹ˆ๋‹ค.
์ด์ œ ๊ทธ ๋ฌธ์ œ๋Š” ์ ‘์–ด๋‘๊ณ , repository๋ฅผ ๋ณต์ œํ•˜๊ณ  ์‹œ์ž‘ํ•ฉ์‹œ๋‹ค.

The core of a promise

์•„์‹œ๋‹ค์‹œํ”ผ Promise๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์†์„ฑ์„ ๊ฐ€์ง„ ๊ฐœ์ฒด์ž…๋‹ˆ๋‹ค.

Then

Promise์— ํ•ธ๋“ค๋Ÿฌ๋ฅผ ์—ฐ๊ฒฐํ•˜๋Š” ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค. ํ•ธ๋“ค๋Ÿฌ ๋ฉ”์†Œ๋“œ ์ค‘ ํ•˜๋‚˜์— ์˜ํ•ด ๋งตํ•‘๋œ ์ด์ „ Promise์˜ ๊ฐ’์œผ๋กœ ์ƒˆ๋กœ์šด Promise๋ฅผ ๋ฆฌํ„ดํ•ฉ๋‹ˆ๋‹ค.

Handlers

then์— ์˜ํ•ด ์—ฐ๊ฒฐ๋œ ํ•ธ๋“ค๋Ÿฌ ๋ฐฐ์—ด์ž…๋‹ˆ๋‹ค. ํ•ธ๋“ค๋Ÿฌ๋Š” onSuccess์™€ onFail์˜ ๋‘ ๊ฐ€์ง€ ๋ฉ”์†Œ๋“œ๋ฅผ ํฌํ•จํ•˜๋Š” ๊ฐ์ฒด์ด๋ฉฐ, ๋‘˜ ๋‹คthen (onSuccess, onFail)์— ์ธ์ˆ˜๋กœ ์ „๋‹ฌ๋ฉ๋‹ˆ๋‹ค.

type HandlerOnSuccess<T, U = any> = (value: T) => U | Thenable<U>;
type HandlerOnFail<U = any> = (reason: any) => U | Thenable<U>;

interface Handler<T, U> {
  onSuccess: HandlerOnSuccess<T, U>;
  onFail: HandlerOnFail<U>;
}

State

Promise๋Š” resolved, rejected, ํ˜น์€ pending ์„ธ๊ฐ€์ง€ state์ค‘ ํ•˜๋‚˜์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Resolved๋Š” ๋ชจ๋“  ๊ฒƒ์ด ์ˆœ์กฐ๋กญ๊ฒŒ ์ง„ํ–‰๋˜์–ด ๊ฐ’์„ ์–ป๊ฑฐ๋‚˜ ์˜ค๋ฅ˜๋ฅผ ํŒŒ์•…ํ•˜๊ณ  ์ฒ˜๋ฆฌํ–ˆ์Œ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.

Rejected๋Š” Promise๋ฅผ ๊ฑฐ๋ถ€ํ–ˆ๊ฑฐ๋‚˜ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜์—ฌ ์žก์ง€ ์•Š์•˜์Œ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.

Pending์€ resolve ๋˜๋Š” reject ๋ฉ”์†Œ๋“œ๊ฐ€ ์•„์ง ํ˜ธ์ถœ๋˜์ง€ ์•Š์•˜์œผ๋ฉฐ ์—ฌ์ „ํžˆ ๊ฐ’์„ ๊ธฐ๋‹ค๋ฆฌ๊ณ  ์žˆ์Œ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.

"Promise๊ฐ€ ํ™•์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค(the promise is settled)"๋ผ๋Š” ์šฉ์–ด๋Š” Promise๊ฐ€ resolved์ด๊ฑฐ๋‚˜ rejected๋˜์—ˆ์Œ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.

Value

resolved ํ˜น์€ rejected ๊ฐ’์ž…๋‹ˆ๋‹ค.
๊ฐ’์ด ์„ค์ •๋˜๋ฉด ๋ณ€๊ฒฝํ•  ๋ฐฉ๋ฒ•์ด ์—†์Šต๋‹ˆ๋‹ค.

Testing

TDD ์ ‘๊ทผ๋ฒ•์— ๋”ฐ๋ฅด๋ฉด ์‹ค์ œ ์ฝ”๋“œ๊ฐ€ ๋‚˜์˜ค๊ธฐ ์ „์— ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜๋ ค๊ณ ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ƒฅ ํ•ด๋ด…์‹œ๋‹ค.

ํ•ต์‹ฌ ํ…Œ์ŠคํŠธ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

describe('PQ <constructor>', () => {
  test('resolves like a promise', () => {
    return new PQ<number>((resolve) => {
      setTimeout(() => {
        resolve(1);
      }, 30);
    }).then((val) => {
      expect(val).toBe(1);
    });
  });

  test('is always asynchronous', () => {
    const p = new PQ((resolve) => resolve(5));

    expect((p as any).value).not.toBe(5);
  });

  test('resolves with the expected value', () => {
    return new PQ<number>((resolve) => resolve(30)).then((val) => {
      expect(val).toBe(30);
    });
  });

  test('resolves a thenable before calling then', () => {
    return new PQ<number>((resolve) =>
      resolve(new PQ((resolve) => resolve(30))),
    ).then((val) => expect(val).toBe(30));
  });

  test('catches errors (reject)', () => {
    const error = new Error('Hello there');

    return new PQ((resolve, reject) => {
      return reject(error);
    }).catch((err: Error) => {
      expect(err).toBe(error);
    });
  });

  test('catches errors (throw)', () => {
    const error = new Error('General Kenobi!');

    return new PQ(() => {
      throw error;
    }).catch((err) => {
      expect(err).toBe(error);
    });
  });

  test('is not mutable - then returns a new promise', () => {
    const start = new PQ<number>((resolve) => resolve(20));

    return PQ.all([
      start
        .then((val) => {
          expect(val).toBe(20);
          return 30;
        })
        .then((val) => expect(val).toBe(30)),
      start.then((val) => expect(val).toBe(20)),
    ]);
  });
});

ํ…Œ์ŠคํŠธ ์‹คํ–‰

Visual Studio Code์šฉ Jest ํ™•์žฅ์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค. ๊ทธ๊ฒƒ์€ ์šฐ๋ฆฌ๋ฅผ ์œ„ํ•ด ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•˜๊ณ  ์ฝ”๋“œ ์ค„ ์‚ฌ์ด์˜ ๊ฒฐ๊ณผ๋ฅผ ๊ฐ๊ฐ ํ†ต๊ณผ ๋ฐ ์‹คํŒจ ํ…Œ์ŠคํŠธ์— ๋Œ€ํ•œ ๋…น์ƒ‰ ๋ฐ ๋นจ๊ฐ„์ƒ‰ ์ ์œผ๋กœ ํ‘œ์‹œํ•ด์ค๋‹ˆ๋‹ค.

๊ฒฐ๊ณผ๋ฅผ ๋ณด๋ ค๋ฉด "Output"์ฝ˜์†”์„ ์—ด๊ณ  "Jest"ํƒญ์„ ์„ ํƒํ•˜์‹ญ์‹œ์˜ค.

์ด๋ฏธ์ง€ ์ฃผ์†Œ : //cdn-media-1.freecodecamp.org/images/0*dr7riPl5ZRkUF8lo

๋‹ค์Œ ๋ช…๋ น์„ ์‹คํ–‰ํ•˜์—ฌ ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

npm run test

ํ…Œ์ŠคํŠธ๋ฅผ ์ˆ˜ํ–‰ํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๊ด€๊ณ„์—†์ด ๋ชจ๋“  ํ…Œ์ŠคํŠธ๊ฐ€ ๋ถ€์ •์ ์œผ๋กœ ๋Œ์•„ ์˜ค๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
๋ณ€๊ฒฝํ•ด ๋ด…์‹œ๋‹ค.

Promise ํ•ต์‹ฌ ๊ตฌํ˜„

constructor

class PQ<T> {
  private state: States = States.PENDING;
  private handlers: Handler<T, any>[] = [];
  private value: T | any;
  public static errors = errors;

  public constructor(callback: (resolve: Resolve<T>, reject: Reject) => void) {
    try {
      callback(this.resolve, this.reject);
    } catch (e) {
      this.reject(e);
    }
  }
}

constructor๋Š” ๋งค๊ฐœ ๋ณ€์ˆ˜๋กœ callback์„ ๋ฐ›์Šต๋‹ˆ๋‹ค.
this.resolve ๋ฐ this.reject๋ฅผ ์ธ์ˆ˜๋กœ ์‚ฌ์šฉํ•˜์—ฌ ์ด callback์„ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.
์ผ๋ฐ˜์ ์œผ๋กœ this.resolve์™€ this.reject๋ฅผ this์— ๋ฐ”์ธ๋”ฉํ–ˆ์ง€๋งŒ ์—ฌ๊ธฐ์„œ๋Š” class arrow ๋ฉ”์†Œ๋“œ๋ฅผ ๋Œ€์‹  ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

setResult

์ด์ œ ๊ฒฐ๊ณผ๋ฅผ ์„ค์ •ํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค. ๊ฒฐ๊ณผ๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ฒ˜๋ฆฌํ•ด์•ผํ•œ๋‹ค๋Š” ๊ฒƒ์„ ๊ธฐ์–ตํ•˜์‹ญ์‹œ์˜ค.
์ฆ‰ Promise์„ ๋ฐ˜ํ™˜ํ•˜๋ฉด ๋จผ์ € ๊ฒฐ๊ณผ๋ฅผ ํ•ด๊ฒฐํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.

class PQ<T> {

  // ...
  
  private setResult = (value: T | any, state: States) => {
    const set = () => {
      if (this.state !== States.PENDING) {
        return null;
      }

      if (isThenable(value)) {
        return (value as Thenable<T>).then(this.resolve, this.reject);
      }

      this.value = value;
      this.state = state;

      return this.executeHandlers();
    };

    setTimeout(set, 0);
  };
}

๋จผ์ € state๊ฐ€ pending์ด ์•„๋‹Œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
ํ•ด๋‹น state์ธ ๊ฒฝ์šฐ Promise๊ฐ€ ์ด๋ฏธ ์ •ํ•ด์ง€๊ณ  ์ƒˆ๋กœ์šด ๊ฐ’์„ ํ• ๋‹น ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

๊ทธ๋Ÿฐ ๋‹ค์Œ ๊ฐ’์ด thenable์ธ์ง€ ํ™•์ธํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค. ๊ฐ„๋‹จํžˆ ๋งํ•ด thenable์€then์„ ๋ฉ”์†Œ๋“œ๋กœ ์‚ฌ์šฉํ•˜๋Š” ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค.

์ผ๋ฐ˜์ ์œผ๋กœ thenable์€ promise์ฒ˜๋Ÿผ ํ–‰๋™ํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค. ๊ฒฐ๊ณผ๋ฅผ ์–ป๊ธฐ ์œ„ํ•ด ํ˜ธ์ถœ ํ•œ ๋‹ค์Œ this.resolve ๋ฐ this.reject ์ธ์ˆ˜๋กœ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.

thenable์ด ์ •ํ•ด์ง€๋ฉด, ๋ฉ”์†Œ๋“œ ์ค‘ ํ•˜๋‚˜๋ฅผ ํ˜ธ์ถœํ•˜๊ณ  ์˜ˆ์ƒ ๋œ non-promise๊ฐ’์„ ์ œ๊ณต ํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

์ด์ œ ๊ฐ์ฒด๊ฐ€ thenable์ธ์ง€ ํ™•์ธํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.

describe('isThenable', () => {
  test('detects objects with a then method', () => {
    expect(isThenable({ then: () => null })).toBe(true);
    expect(isThenable(null)).toBe(false);
    expect(isThenable({})).toBe(false);
  });
});
const isFunction = (func: any) => typeof func === 'function';

const isObject = (supposedObject: any) =>
  typeof supposedObject === 'object' &&
  supposedObject !== null &&
  !Array.isArray(supposedObject);

const isThenable = (obj: any) => isObject(obj) && isFunction(obj.then);

callback ๋‚ด๋ถ€์˜ ์ฝ”๋“œ๊ฐ€ ์žˆ๋”๋ผ๋„ promise๊ฐ€ ๋™๊ธฐ๊ฐ€๋˜์ง€ ์•Š์„ ๊ฒƒ์ด๋ผ๋Š” ์ ์„ ์ธ์‹ํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค.
setTimeout์„ ์‚ฌ์šฉํ•˜์—ฌ ๋‹ค์Œ ์ด๋ฒคํŠธ ๋ฃจํ”„ ๋ฐ˜๋ณต๊นŒ์ง€ ์‹คํ–‰์„ ์ง€์—ฐ์‹œํ‚ต๋‹ˆ๋‹ค.
์ด์ œ ๋‚จ์€ ๊ฒƒ์€ ๊ฐ’๊ณผ ์ƒํƒœ๋ฅผ ์„ค์ • ํ•œ ๋‹ค์Œ ๋“ฑ๋ก ๋œ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ์‹คํ–‰ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

executeHandlers

class PQ<T> {

  // ...
  
  private executeHandlers = () => {
    if (this.state === States.PENDING) {
      return null;
    }

    this.handlers.forEach((handler) => {
      if (this.state === States.REJECTED) {
        return handler.onFail(this.value);
      }

      return handler.onSuccess(this.value);
    });

    this.handlers = [];
  };
}

๋‹ค์‹œ, ์ƒํƒœ๊ฐ€ pending์ด ์•„๋‹Œ์ง€ ํ™•์ธํ•˜์‹ญ์‹œ์˜ค.

promise์˜ ์ƒํƒœ๋Š” ์šฐ๋ฆฌ๊ฐ€ ์‚ฌ์šฉํ•  ํ•จ์ˆ˜๋ฅผ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค.
๋ฌธ์ œ๊ฐ€ ํ•ด๊ฒฐ๋˜๋ฉด onSuccess๋ฅผ ์‹คํ–‰ํ•˜๊ณ  ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด onFail์„ ์‹คํ–‰ํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.

์ด์ œ ์•ˆ์ „ํ•˜๊ณ  ๋‚˜์ค‘์— ์šฐ์—ฐํžˆ ์‹คํ–‰๋˜์ง€ ์•Š๋„๋ก ํ•ธ๋“ค๋Ÿฌ ๋ฐฐ์—ด์„ ๋ถ„๋ช…ํžˆ ํ•ด๋‘ก์‹œ๋‹ค. ํ•ธ๋“ค๋Ÿฌ๋Š” ๋‚˜์ค‘์—๋„ ๋ถ€์ฐฉํ•˜๊ณ  ์‹คํ–‰ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ทธ ๋‹ค์Œ์— ๋‹ค๋ฃจ์–ด์•ผ ํ•  ๋‚ด์šฉ์€ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ์—ฐ๊ฒฐํ•˜๋Š” ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.

attachHandler

class PQ<T> {

  // ...
  
  private attachHandler = (handler: Handler<T, any>) => {
    this.handlers = [...this.handlers, handler];

    this.executeHandlers();
  };
}

๋ณด์ด๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ๊ฐ„๋‹จํ•ฉ๋‹ˆ๋‹ค. ํ•ธ๋“ค๋Ÿฌ ๋ฐฐ์—ด์— ํ•ธ๋“ค๋Ÿฌ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ  ์‹คํ–‰ํ•˜๋ฉด๋ฉ๋‹ˆ๋‹ค. ์ด๊ฒŒ ๋‹ค์ž…๋‹ˆ๋‹ค.
์ด์ œ ๋ชจ๋“  ๊ฒƒ์„ ํ•ฉ์น˜๋ ค๋ฉด then๋ฉ”์†Œ๋“œ๋ฅผ ๊ตฌํ˜„ํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.

then

class PQ<T> {

  // ...
  
  public then<U>(
    onSuccess?: HandlerOnSuccess<T, U>,
    onFail?: HandlerOnFail<U>,
  ) {
    return new PQ<U | T>((resolve, reject) => {
      return this.attachHandler({
        onSuccess: (result) => {
          if (!onSuccess) {
            return resolve(result);
          }

          try {
            return resolve(onSuccess(result));
          } catch (e) {
            return reject(e);
          }
        },
        onFail: (reason) => {
          if (!onFail) {
            return reject(reason);
          }

          try {
            return resolve(onFail(reason));
          } catch (e) {
            return reject(e);
          }
        },
      });
    });
  }
}

then์—์„œ, ์šฐ๋ฆฌ๋Š” promise๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ณ ,callback์—์„œ ํ˜„์žฌ promise๊ฐ€ ํ™•์ •๋˜๊ธฐ๋ฅผ ๊ธฐ๋‹ค๋ฆฌ๋Š”๋ฐ ์‚ฌ์šฉ๋˜๋Š” ํ•ธ๋“ค๋Ÿฌ๋ฅผ ์—ฐ๊ฒฐํ•ฉ๋‹ˆ๋‹ค.
์ด ๊ฒฝ์šฐ ํ•ธ๋“ค๋Ÿฌ์˜ onSuccess ๋˜๋Š” onFail์ด ์‹คํ–‰๋˜๊ณ  ๊ทธ์— ๋”ฐ๋ผ ์ง„ํ–‰๋ฉ๋‹ˆ๋‹ค.

์—ฌ๊ธฐ์„œ ๊ธฐ์–ตํ•ด์•ผ ํ•  ๊ฒƒ์€ ์ „๋‹ฌ๋œ ํ•ธ๋“ค๋Ÿฌ ์ค‘ ์–ด๋Š ๊ฒƒ๋„ ํ•„์š”ํ•˜์ง€ ์•Š๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ undefined๋ฅผ ์‹คํ–‰ํ•˜๋ ค๊ณ  ์‹œ๋„ํ•˜์ง€ ์•Š๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค.

๋˜ํ•œ ํ•ธ๋“ค๋Ÿฌ๊ฐ€ ์ „๋‹ฌ ๋  ๋•Œ onFail์—์„œ ์˜ค๋ฅ˜๊ฐ€ ์ฒ˜๋ฆฌ๋˜์—ˆ์œผ๋ฏ€๋กœ ์‹ค์ œ๋กœ ๋ฆฌํ„ด๋œ promise๋ฅผ ํ•ด๊ฒฐํ•ฉ๋‹ˆ๋‹ค.

catch

Catch๋Š” ์‹ค์ œ๋กœ then๋ฉ”์†Œ๋“œ์— ๋Œ€ํ•œ ์ถ”์ƒํ™”์ž…๋‹ˆ๋‹ค.

class PQ<T> {

  // ...
  
  public catch<U>(onFail: HandlerOnFail<U>) {
    return this.then<U>(identity, onFail);
  }
}

์ด๊ฒŒ ๋‹ค์ž…๋‹ˆ๋‹ค.

Finally

Finally๋Š” ๋˜ํ•œ promise์˜ ๊ฒฐ๊ณผ์— ์‹ค์ œ๋กœ ์‹ ๊ฒฝ ์“ฐ์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— then(finallyCb, finallyCb)์— ๋Œ€ํ•œ ์ถ”์ƒํ™”์ž…๋‹ˆ๋‹ค. ์‹ค์ œ๋กœ ์ด์ „ promise์˜ ๊ฒฐ๊ณผ๋„ ๋ณด์กดํ•˜๊ณ  ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ finallyCb์— ์˜ํ•ด ๋ฐ˜ํ™˜๋˜๋Š” ๊ฒƒ์€ ์‹ค์ œ๋กœ ์ค‘์š”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

describe('PQ.prototype.finally', () => {
  test('it is called regardless of the promise state', () => {
    let counter = 0;
    return PQ.resolve(15)
      .finally(() => {
        counter += 1;
      })
      .then(() => {
        return PQ.reject(15);
      })
      .then(() => {
        // wont be called
        counter = 1000;
      })
      .finally(() => {
        counter += 1;
      })
      .catch((reason) => {
        expect(reason).toBe(15);
        expect(counter).toBe(2);
      });
  });
});
class PQ<T> {

  // ...
  

  public finally<U>(cb: Finally<U>) {
    return new PQ<U>((resolve, reject) => {
      let val: U | any;
      let isRejected: boolean;

      return this.then(
        (value) => {
          isRejected = false;
          val = value;
          return cb();
        },
        (reason) => {
          isRejected = true;
          val = reason;
          return cb();
        },
      ).then(() => {
        if (isRejected) {
          return reject(val);
        }

        return resolve(val);
      });
    });
  }
}

toString

describe('PQ.prototype.toString', () => {
  test('returns [object PQ]', () => {
    expect(new PQ<undefined>((resolve) => resolve()).toString()).toBe(
      '[object PQ]',
    );
  });
});
class PQ<T> {

  // ...
  
  public toString() {
    return `[object PQ]`;
  }
}

๋ฌธ์ž์—ด [object PQ]๋งŒ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
promise์˜ ํ•ต์‹ฌ์„ ๊ตฌํ˜„ ํ•œ ํ›„์—๋Š” ์•ž์„œ ์–ธ๊ธ‰ ํ•œ Bluebird ๋ฉ”์†Œ๋“œ์ค‘ ์ผ๋ถ€๋ฅผ ๊ตฌํ˜„ํ•˜์—ฌ promise๋ฅผ ๋ณด๋‹ค ์‰ฝ๊ฒŒ ์ˆ˜ํ–‰ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Additional methods

Promise.resolve

์ž‘๋™๋ฐฉ์‹

describe('PQ.resolve', () => {
  test('resolves a value', () => {
    return PQ.resolve(5).then((value) => {
      expect(value).toBe(5);
    });
  });
});
class PQ<T> {

  // ...
  
  public static resolve<U = any>(value?: U | Thenable<U>) {
    return new PQ<U>((resolve) => {
      return resolve(value);
    });
  }
}

Promise.reject

์ž‘๋™๋ฐฉ์‹

describe('PQ.reject', () => {
  test('rejects a value', () => {
    return PQ.reject(5).catch((value) => {
      expect(value).toBe(5);
    });
  });
});
class PQ<T> {

  // ...
  
  public static reject<U>(reason?: any) {
    return new PQ<U>((resolve, reject) => {
      return reject(reason);
    });
  }
}

Promise.all

์ž‘๋™๋ฐฉ์‹

describe('PQ.all', () => {
  test('resolves a collection of promises', () => {
    return PQ.all([PQ.resolve(1), PQ.resolve(2), 3]).then((collection) => {
      expect(collection).toEqual([1, 2, 3]);
    });
  });

  test('rejects if one item rejects', () => {
    return PQ.all([PQ.resolve(1), PQ.reject(2)]).catch((reason) => {
      expect(reason).toBe(2);
    });
  });
});
class PQ<T> {

  // ...
  
  public static all<U = any>(collection: (U | Thenable<U>)[]) {
    return new PQ<U[]>((resolve, reject) => {
      if (!Array.isArray(collection)) {
        return reject(new TypeError('An array must be provided.'));
      }

      let counter = collection.length;
      const resolvedCollection: U[] = [];

      const tryResolve = (value: U, index: number) => {
        counter -= 1;
        resolvedCollection[index] = value;

        if (counter !== 0) {
          return null;
        }

        return resolve(resolvedCollection);
      };

      return collection.forEach((item, index) => {
        return PQ.resolve(item)
          .then((value) => {
            return tryResolve(value, index);
          })
          .catch(reject);
      });
    });
  }
}

๊ตฌํ˜„์ด ๋งค์šฐ ๊ฐ„๋‹จํ•˜๋‹ค๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค.

collection.length์—์„œ ์‹œ์ž‘ํ•˜์—ฌ 0์— ๋„๋‹ฌ ํ•  ๋•Œ๊นŒ์ง€ ๊ฐ tryResolve'๋กœ ์นด์šดํŠธ ๋‹ค์šดํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” ์ปฌ๋ ‰์…˜์˜ ๋ชจ๋“  ํ•ญ๋ชฉ์ด ํ•ด๊ฒฐ๋˜์—ˆ์Œ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฐ ๋‹ค์Œ ์ƒˆ๋กœ ๋งŒ๋“  ์ปฌ๋ ‰์…˜์„ ํ•ด๊ฒฐํ•ฉ๋‹ˆ๋‹ค.

Promise.any

์ž‘๋™๋ฐฉ์‹

describe('PQ.any', () => {
  test('resolves the first value', () => {
    return PQ.any<number>([
      PQ.resolve(1),
      new PQ((resolve) => setTimeout(resolve, 15)),
    ]).then((val) => expect(val).toBe(1));
  });

  test('rejects if the first value rejects', () => {
    return PQ.any([
      new PQ((resolve) => setTimeout(resolve, 15)),
      PQ.reject(1),
    ]).catch((reason) => {
      expect(reason).toBe(1);
    });
  });
});
class PQ<T> {

  // ...

  public static any<U = any>(collection: (U | Thenable<U>)[]) {
    return new PQ<U>((resolve, reject) => {
      return collection.forEach((item) => {
        return PQ.resolve(item)
          .then(resolve)
          .catch(reject);
      });
    });
  }
}

์šฐ๋ฆฌ๋Š” ๋‹จ์ˆœํžˆ ์ฒซ ๋ฒˆ์งธ ๊ฐ’์ด ํ•ด๊ฒฐ๋˜์–ด Promise๋กœ ๋Œ์•„ ์˜ค๊ธฐ๋ฅผ ๊ธฐ๋‹ค๋ฆฝ๋‹ˆ๋‹ค.

Promise.props

์ž‘๋™๋ฐฉ์‹

describe('PQ.props', () => {
  test('resolves object correctly', () => {
    return PQ.props<{ test: number; test2: number }>({
      test: PQ.resolve(1),
      test2: PQ.resolve(2),
    }).then((obj) => {
      return expect(obj).toEqual({ test: 1, test2: 2 });
    });
  });

  test('rejects non objects', () => {
    return PQ.props([]).catch((reason) => {
      expect(reason).toBeInstanceOf(TypeError);
    });
  });
});
class PQ<T> {

  // ...
  
  public static props<U = any>(obj: object) {
    return new PQ<U>((resolve, reject) => {
      if (!isObject(obj)) {
        return reject(new TypeError('An object must be provided.'));
      }

      const resolvedObject = {};

      const keys = Object.keys(obj);
      const resolvedValues = PQ.all<string>(keys.map((key) => obj[key]));

      return resolvedValues
        .then((collection) => {
          return collection.map((value, index) => {
            resolvedObject[keys[index]] = value;
          });
        })
        .then(() => resolve(resolvedObject as U))
        .catch(reject);
    });
  }
}

์ „๋‹ฌ ๋œ ๊ฐ์ฒด์˜ ํ‚ค๋ฅผ ๋ฐ˜๋ณตํ•˜์—ฌ ๋ชจ๋“  ๊ฐ’์„ ํ•ด๊ฒฐํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฐ ๋‹ค์Œ ์ƒˆ ๊ฐ์ฒด์— ๊ฐ’์„ ํ• ๋‹นํ•˜๊ณ  Promise๋ฅผ ํ•ด๊ฒฐํ•ฉ๋‹ˆ๋‹ค.

Promise.prototype.spread

์ž‘๋™๋ฐฉ์‹

describe('PQ.protoype.spread', () => {
  test('spreads arguments', () => {
    return PQ.all<number>([1, 2, 3]).spread((...args) => {
      expect(args).toEqual([1, 2, 3]);
      return 5;
    });
  });

  test('accepts normal value (non collection)', () => {
    return PQ.resolve(1).spread((one) => {
      expect(one).toBe(1);
    });
  });
});
class PQ<T> {

  // ...
  
  public spread<U>(handler: (...args: any[]) => U) {
    return this.then<U>((collection) => {
      if (Array.isArray(collection)) {
        return handler(...collection);
      }

      return handler(collection);
    });
  }
}

Promise.delay

์ž‘๋™๋ฐฉ์‹

describe('PQ.delay', () => {
  test('waits for the given amount of miliseconds before resolving', () => {
    return new PQ<string>((resolve) => {
      setTimeout(() => {
        resolve('timeout');
      }, 50);

      return PQ.delay(40).then(() => resolve('delay'));
    }).then((val) => {
      expect(val).toBe('delay');
    });
  });

  test('waits for the given amount of miliseconds before resolving 2', () => {
    return new PQ<string>((resolve) => {
      setTimeout(() => {
        resolve('timeout');
      }, 50);

      return PQ.delay(60).then(() => resolve('delay'));
    }).then((val) => {
      expect(val).toBe('timeout');
    });
  });
});
class PQ<T> {

  // ...
  
  public static delay(timeInMs: number) {
    return new PQ((resolve) => {
      return setTimeout(resolve, timeInMs);
    });
  }
}

setTimeout์„ ์‚ฌ์šฉํ•จ์œผ๋กœ์จ, ์ฃผ์–ด์ง„ ๋ฐ€๋ฆฌ ์ดˆ๋งŒํผ resolve ํ•จ์ˆ˜์˜ ์‹คํ–‰์„ ์ง€์—ฐ์‹œํ‚ต๋‹ˆ๋‹ค.

Promise.prototype.timeout

์ž‘๋™๋ฐฉ์‹

describe('PQ.prototype.timeout', () => {
  test('rejects after given timeout', () => {
    return new PQ<number>((resolve) => {
      setTimeout(resolve, 50);
    })
      .timeout(40)
      .catch((reason) => {
        expect(reason).toBeInstanceOf(PQ.errors.TimeoutError);
      });
  });

  test('resolves before given timeout', () => {
    return new PQ<number>((resolve) => {
      setTimeout(() => resolve(500), 500);
    })
      .timeout(600)
      .then((value) => {
        expect(value).toBe(500);
      });
  });
});
class PQ<T> {

  // ...
  
  public timeout(timeInMs: number) {
    return new PQ<T>((resolve, reject) => {
      const timeoutCb = () => {
        return reject(new PQ.errors.TimeoutError());
      };

      setTimeout(timeoutCb, timeInMs);

      return this.then(resolve);
    });
  }
}

์ด๊ฒƒ์€ ์กฐ๊ธˆ ๊นŒ๋‹ค ๋กญ์Šต๋‹ˆ๋‹ค.

setTimeout์ด promise์—์„œ๋ณด๋‹ค ๋” ๋น ๋ฅด๊ฒŒ ์‹คํ–‰๋˜๋ฉด ํŠน๋ณ„ํ•œ ์˜ค๋ฅ˜๋กœ promise๋ฅผ ๊ฑฐ์ ˆํ•ฉ๋‹ˆ๋‹ค.

Promise.promisify

์ž‘๋™๋ฐฉ์‹

describe('PQ.promisify', () => {
  test('works', () => {
    const getName = (firstName, lastName, callback) => {
      return callback(null, `${firstName} ${lastName}`);
    };

    const fn = PQ.promisify<string>(getName);
    const firstName = 'Maciej';
    const lastName = 'Cieslar';

    return fn(firstName, lastName).then((value) => {
      return expect(value).toBe(`${firstName} ${lastName}`);
    });
  });
});
class PQ<T> {

  // ...
  
  public static promisify<U = any>(
    fn: (...args: any[]) => void,
    context = null,
  ) {
    return (...args: any[]) => {
      return new PQ<U>((resolve, reject) => {
        return fn.apply(context, [
          ...args,
          (err: any, result: U) => {
            if (err) {
              return reject(err);
            }

            return resolve(result);
          },
        ]);
      });
    };
  }
}

์ „๋‹ฌ ๋œ ๋ชจ๋“  ์ธ์ˆ˜์™€ ๋งˆ์ง€๋ง‰ ์ธ์ˆ˜๋กœ ํ•จ์ˆ˜์— ์ ์šฉํ•˜์—ฌ ์˜ค๋ฅ˜ ์šฐ์„  callback์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

Promise.promisifyAll

์ž‘๋™๋ฐฉ์‹

describe('PQ.promisifyAll', () => {
  test('promisifies a object', () => {
    const person = {
      name: 'Maciej Cieslar',
      getName(callback) {
        return callback(null, this.name);
      },
    };

    const promisifiedPerson = PQ.promisifyAll<{
      getNameAsync: () => PQ<string>;
    }>(person);

    return promisifiedPerson.getNameAsync().then((name) => {
      expect(name).toBe('Maciej Cieslar');
    });
  });
});
class PQ<T> {

  // ...
  
  public static promisifyAll<U>(obj: any): U {
    return Object.keys(obj).reduce((result, key) => {
      let prop = obj[key];

      if (isFunction(prop)) {
        prop = PQ.promisify(prop, obj);
      }

      result[`${key}Async`] = prop;

      return result;
    }, {}) as U;
  }
}

์šฐ๋ฆฌ๋Š” ๊ฐ์ฒด์™€ promisify์˜ ํ‚ค๋ฅผ ๋ฐ˜๋ณตํ•˜์˜€๊ณ  ์ด๊ฒƒ์€ ๊ฐ๊ฐ Async๋ผ๋Š” ๋ฉ”์†Œ๋“œ์˜ ์ด๋ฆ„์œผ๋กœ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

๋งˆ๋ฌด๋ฆฌ

์—ฌ๊ธฐ์—๋Š” ๋ชจ๋“  Bluebird API ๋ฉ”์†Œ๋“œ ์ค‘ ๋ช‡ ๊ฐ€์ง€๋งŒ ํฌํ•จ๋˜์–ด ์žˆ์œผ๋ฏ€๋กœ ๋‚˜๋จธ์ง€๋ฅผ ํƒ์ƒ‰ํ•˜๊ณ  ํ•ด๊ฒฐํ•˜๊ณ  ๊ตฌํ˜„ํ•ด ๋ณด์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค.

์ฒ˜์Œ์—๋Š” ์–ด๋ ค์›Œ ๋ณด์ผ ์ˆ˜ ์žˆ์ง€๋งŒ ๋‚™๋‹ดํ•˜์ง€๋Š” ๋งˆ์‹ญ์‹œ์˜ค. ์‰ฌ์šฐ๋ฉด ๊ฐ€์น˜๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.

์ฝ์–ด ์ฃผ์…”์„œ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค! ์ด ๊ธฐ์‚ฌ๊ฐ€ ์œ ์ตํ•˜๊ณ  Promise์˜ ๊ฐœ๋…์„ ์ดํ•ดํ•˜๋Š” ๋ฐ ๋„์›€์ด ๋˜์—ˆ๊ธฐ๋ฅผ ๋ฐ”๋ž๋‹ˆ๋‹ค. ์•ž์œผ๋กœ๋Š” Promise์„ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜ ๋‹จ์ˆœํžˆ ๋น„๋™๊ธฐ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ์ด ๋” ํŽธ์•ˆํ•ด์งˆ๊ฒƒ์ž…๋‹ˆ๋‹ค.

์งˆ๋ฌธ์ด๋‚˜ ์˜๊ฒฌ์ด ์žˆ์œผ์‹œ๋ฉด ์•„๋ž˜์˜ ์˜๊ฒฌ ์„น์…˜์— ์ž์œ ๋กญ๊ฒŒ ์˜๊ฒฌ์„ ๋ณด๋‚ด๊ฑฐ๋‚˜ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋‚ด์ฃผ์‹ญ์‹œ์˜ค.

๋‚ด ์†Œ์…œ ๋ฏธ๋””์–ด๋ฅผ ํ™•์ธํ•˜์‹ญ์‹œ์˜ค!

๋‚ด ๋‰ด์Šค ๋ ˆํ„ฐ์— ๊ฐ€์ž…ํ•˜์‹ญ์‹œ์˜ค!

Originally published at www.mcieslar.com on August 4, 2018.

โš ๏ธ **GitHub.com Fallback** โš ๏ธ