Skip to content
2 changes: 1 addition & 1 deletion assignment/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/curring-partial.ts"></script>
<script type="module" src="/src/promise.ts"></script>
</body>
</html>
63 changes: 63 additions & 0 deletions assignment/src/promise.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import MyPromise from "./promise"

type Callback<T> = (resolve: (value: T) => void, reject: (reason?: any) => void) => void

function testPromise<T>(callback: Callback<T>) {
return new MyPromise<T>((resolve, reject) => {
setTimeout(() => {
callback(resolve, reject)
}, 300)
})
}

describe("프로미스 테스트", () => {
test("then 메서드 테스트", () => {
return testPromise<number>((resolve) => resolve(1)).then((value) => expect(value).toEqual(1))
})

test("then 메서드 체이닝", () => {
return testPromise<number>((resolve) => resolve(1))
.then((value) => value + 1)
.then((value) => value + 1)
.then((value) => expect(value).toEqual(3))
})

test("catch 메서드 테스트 베이직", () => {
return testPromise<number>((_, reject) => reject("에러 발생")).catch((error) => {
expect(error).toBe("에러 발생")
})
})

test("catch 메서드 테스트 스탠다드", () => {
return testPromise<number>((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<number>((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)
})
})
})
105 changes: 105 additions & 0 deletions assignment/src/promise.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
type Executor<T> = (resolve: Resolve<T>, reject: Reject) => void
type Resolve<T extends unknown> = (value: T) => void | T
type Reject = (reason?: any) => void

export default class MyPromise<T> {
private promiseState: "pending" | "fulfilled" | "rejected" = "pending"
private promiseResult: T | null | undefined = null
private resolveQueue: any[] = []
private rejectQueue: any[] = []

constructor(executor: Executor<T>) {
const bindingResolve = this._resolve.bind(this)
const bindingReject = this._reject.bind(this)

try {
executor(bindingResolve, bindingReject)
} catch (error) {
bindingReject(error)
}
}

static resolve<T>(value: T) {
return new MyPromise<T>((resolve) => resolve(value))
}

static reject<T>(value: T) {
return new MyPromise<T>((_, 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<T> | undefined, onRejected?: Reject) {
return new MyPromise<T>((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)
}
}