diff --git a/assignment/index.html b/assignment/index.html index 2bb0ce8..bf06f79 100644 --- a/assignment/index.html +++ b/assignment/index.html @@ -8,6 +8,6 @@
- + diff --git a/assignment/src/promise.test.ts b/assignment/src/promise.test.ts new file mode 100644 index 0000000..036e3cc --- /dev/null +++ b/assignment/src/promise.test.ts @@ -0,0 +1,63 @@ +import MyPromise from "./promise" + +type Callback = (resolve: (value: T) => void, reject: (reason?: any) => void) => void + +function testPromise(callback: Callback) { + return new MyPromise((resolve, reject) => { + setTimeout(() => { + callback(resolve, reject) + }, 300) + }) +} + +describe("프로미스 테스트", () => { + test("then 메서드 테스트", () => { + return testPromise((resolve) => resolve(1)).then((value) => expect(value).toEqual(1)) + }) + + test("then 메서드 체이닝", () => { + return testPromise((resolve) => resolve(1)) + .then((value) => value + 1) + .then((value) => value + 1) + .then((value) => expect(value).toEqual(3)) + }) + + test("catch 메서드 테스트 베이직", () => { + return testPromise((_, reject) => reject("에러 발생")).catch((error) => { + expect(error).toBe("에러 발생") + }) + }) + + test("catch 메서드 테스트 스탠다드", () => { + return testPromise((resolve) => resolve(1)) + .then((value) => value + 1) + .then((value) => { + if (value < 5) { + throw new Error("value가 5보다 작아요") + } + + return value + }) + .catch((error) => { + expect(error).toBeInstanceOf(Error) + }) + }) + + test("finally 메서드 테스트", () => { + const finallyMockFn = vi.fn(() => "비동기 성공, 실패 여부와 상관 없이 실행") + + return testPromise((resolve) => resolve(1)) + .then((value) => value + 1) + .then((value) => { + expect(value).toEqual(2) + throw new Error("에러 발생") + }) + .catch((error) => { + expect(error).toBeInstanceOf(Error) + }) + .finally(finallyMockFn) + .then(() => { + expect(finallyMockFn).toHaveBeenCalledTimes(1) + }) + }) +}) diff --git a/assignment/src/promise.ts b/assignment/src/promise.ts new file mode 100644 index 0000000..2cc7780 --- /dev/null +++ b/assignment/src/promise.ts @@ -0,0 +1,105 @@ +type Executor = (resolve: Resolve, reject: Reject) => void +type Resolve = (value: T) => void | T +type Reject = (reason?: any) => void + +export default class MyPromise { + private promiseState: "pending" | "fulfilled" | "rejected" = "pending" + private promiseResult: T | null | undefined = null + private resolveQueue: any[] = [] + private rejectQueue: any[] = [] + + constructor(executor: Executor) { + const bindingResolve = this._resolve.bind(this) + const bindingReject = this._reject.bind(this) + + try { + executor(bindingResolve, bindingReject) + } catch (error) { + bindingReject(error) + } + } + + static resolve(value: T) { + return new MyPromise((resolve) => resolve(value)) + } + + static reject(value: T) { + return new MyPromise((_, reject) => reject(value)) + } + + private _resolve(value: T) { + if (this.promiseState !== "pending") { + return + } + + this.promiseState = "fulfilled" + this.promiseResult = value + + this.resolveQueue.forEach((fn) => fn(this.promiseResult)) + this.resolveQueue = [] + } + + private _reject(reason?: any) { + if (this.promiseState !== "pending") { + return + } + + this.promiseState = "rejected" + this.promiseResult = reason + this.rejectQueue.forEach((fn) => fn(this.promiseResult)) + this.rejectQueue = [] + } + + catch(onRejected: Reject) { + return this.then(undefined, onRejected) + } + + then(onFulfilled: Resolve | undefined, onRejected?: Reject) { + return new MyPromise((resolve, reject) => { + const enqueueFulfilled = (value: T) => { + queueMicrotask(() => { + if (onFulfilled) { + try { + const fulfilledValue = onFulfilled(value) + resolve(fulfilledValue as T) + } catch (error) { + reject(error) + } + } else { + resolve(value) + } + }) + } + + const enqueueRejected = (reason: any) => { + queueMicrotask(() => { + if (onRejected) { + try { + const rejectedValue = onRejected(reason) + resolve(rejectedValue as T) + } catch (error) { + reject(error) + } + } else { + reject(reason) + } + }) + } + + const lookupTable = { + fulfilled: () => enqueueFulfilled(this.promiseResult as T), + rejected: () => enqueueRejected(this.promiseResult), + pending: () => { + this.resolveQueue.push(enqueueFulfilled) + this.rejectQueue.push(enqueueRejected) + }, + } + + lookupTable[this.promiseState]() + }) + } + + finally(onFinally: () => void) { + return this.then(onFinally, onFinally) + } +}