# Promise
Promise는 자바스크립트를 제대로 다루기 위한 필수 개념 중 하나로서 반드시 이해하고 넘어가야 하는 개념이다. 자바스크립트는 single thread로 동작하는데 브라우저를 사용하다 보면 마치 multi thread처럼 보인다. 클릭 이벤트를 처리하면서 동시에 API를 요청하고 rendering을 하는 등 여러가지의 요구사항을 queue에 쌓아서 처리하는 것이 아니라 한번에 처리하는 것 같은데 이는 사실 자바스크립트가 비동기로 동작하기 때문이다. 비동기란 처리하는 thread는 하나이지만 자바스크립트 엔진에서 수행하는 Event loop로 인해 비동기 요청들을 메인 stack에서 분리하여 별도로 수행한 후 다시 메인 stack으로 보내서 작업을 수행함으로서 API를 요청하면서 동시에 다른 Event들을 다루는 방식이 가능하다. 비동기로 수행하는 것들은 `fetch`, `setTimeout`, `setInterval` 등이 있으며 `Promise` 도 이 중 대표적인 비동기 처리 방식으로 `callback hell`문제를 해결하기 위한 방식 중 하나로 사용된다.
`callback hell`이란 사전의 작업이 끝난 후 다음 작업을 수행하기 위해 함수 안에 callback 함수를 계속해서 정의해 나가면서 생기는 문제이다
``` javascript
// Callback hell
function task1() {
function task2() {
function task3() {
// ...
}
}
}
```
`Promise`는 이러한 문제를 `method chaining` 방식으로 해결한다
``` javascript
new Promise((res, rej) => {
// Task
})
.then()
.then()
.then()
.catch()
```
물론 `method chaining`에도 한계가 있기 때문에 자바스크립트는 `async await` 도입으로 좀 더 깔끔하게 비동기 작업들을 처리할 수 있다
```javascript
const callback = async () => {
await task1();
await task2();
await task3();
}
```
`async await`에 대한 더 자세한 정보는 [여기](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function)에 나와있다.
`Promise`는 4가지 Status 정보를 가지고 있는데 한번 Status가 정의 된 경우 다시 작업을 수행하지 않는다.
1. Pending : 작업 수행 전 기본 상태
2. Fulfilled : 성공
3. Rejected : 실패
4. Settled : 수행 완료
`Promise`를 생성하면 기본적으로 Status는 `pending`이 되고 전달된 수행 함수의 결과에 따라 성공 또는 실패를 결정한다. Status값이 업데이트 되고 나면 `Settled`상태로 변경된다.
`Promise` 기본 Method들은 아래와 같다
1. `resolve` : 작업 성공 시 실행
2. `reject` : 작업 실패 시 실행
3. `then` : Settled 후 다음에 수행 할 callback 함수
4. `catch` : Error 발생 시 호출
5. `all` : `Promise` 배열을 파라미터로 전달하여 모든 작업 완료 시 결과 값 return
6. `race` : 전달된 파라미터 중 먼저 완료된 값 return
이 외에도 `any`, `allSettled` 등 `Promise`에 내장된 다양한 Method들이 있다.
`then`과 `catch`를 제외하곤 전부 `static` method로서 `Promise`를 생성하지 않고도 사용할 수 있으며 기본적인 `Promise` 생성시에는 실행 함수를 파라미터로 전달해 주어야 한다. 생성 함수는 2가지 callback method를 전달 받는데 첫번째 파라미터로 성공 시에 호출하는 `resolve` 두번째 파라미터로는 실패시 호출 하는 `reject`를 받는다.
```typescript
type Promise = new (executor: (resolve: (value: any) => void, reject: (reason?: any) => void) => void) => Promise
```
```javascript
// Promise 생성
const myExecutor = function (resolve /* 성공시 호출 */, reject /* 실패시 호출 */) {
// 비동기 작업이 성공하면
resolve(result);
// 비동기 작업이 실패하면
reject(error);
}
new Promise(myExecutor);
```
`static`으로 method를 아래와 같이 바로 실행할 수도 있다.
``` javascript
Promise.resolve().then();
Promise.all([/* Promise */]).then();
```
이 두가지 핵심만 기억하면 `Promise` 구현은 쉽게 이해할 수 있다.
## Promise 구현하기
`Promise`는 생성하여 사용해야 하므로 ES6 이후 도입된 `class`를 사용하면 좀 더 깔끔하게 정의할 수 있다. 우선은 생성자에 `executor`를 전달해 주어야 한다. 전체적인 틀은 아래와 같다.
```typescript
class MyPromise {
constructor : (executor) => void
private resolve : (value: any) => void;
private reject : (error: any) => void;
static resolve
static reject
then
catch
}
```
### constructor
`Promise`는 생성하자마자 실행함수를 호출 하고 파라미터로 `resolve`, `reject`를 전달 해 주는데, 이때 `static` `resolve`, `reject`를 구분하기 위해 `private` `resolve`, `reject` 전달 시켜 준다.
``` javascript
constructor(executor) {
// state값 pending으로 초기화
this.state = 'pending';
try {
// 생성하자마자 executor 호출, private resolve, reject 전달
executor(this._resolve.bind(this), this._reject.bind(this));
} catch (error) {
this._reject(error);
}
}
```
`_resolve`, `_reject`는 아래에서 정의하기로 한다. 이제 그 다음 `then` 메서드를 정의해 주어야 하는데, `resolve` 된 이후 어떤 함수를 호출해야하는지 `then` 메서드에서 정의해주므로 먼저 구현하는것이 이해하기 수월하다
### then
`then` 메서드는 두가지 파라미터를 전달 받는데, `fulfilled` 됐을때, `rejected` 됐을때 수행할 함수를 전달 받으며 모두 optional로 전달하지 않아도 에러가 발생하지 않는다. 따라서 구현시 파라미터 값이 있는지, 함수가 맞는지 확인하는 과정이 필요하다. 만약 타입이 다르거나 값이 없다면 default 함수를 정의해서 `resolve` 또는 `reject` 메서드 실행시 에러가 나지 않도록 방지해야 한다. 또한, 앞에서 보았듯이 `Promise`는 메서드 체이닝이 가능해야 하므로 `Promise`를 새로 생성하여 전달해야 한다.
``` javascript
then(onFulfilled, onRejected) {
// fulfilled callback 함수가 제대로 넘어왔는지 확인 후 등록
const isOnFulfilledFunction = typeof onFulfilled === 'function';
this.onFulfilled = isOnFulfilledFunction ? onFulfilled : (value) => value;
// rejected callback 함수가 제대로 넘어왔는지 확인 후 등록
const isOnRejectedFunction = typeof onRejected === 'function';
this.onRejected = isOnRejectedFunction
? onRejected
: (error) => {
throw error;
};
return new MyPromise((resolve, reject) => {
// resolve, reject 함수를 then 수행 후 실행 함수로 등록
// 이렇게 함으로서 _resolve, _reject 함수 내에서
// 그 다음 resolve, reject 함수를 실행할 수 있다.
this.thenPromiseResolve = resolve;
this.thenPromiseReject = reject;
});
}
```
### private resolve
`private resolve`는 executor 함수가 비동기 작업을 성공적으로 마쳤을 경우 호출해주는 작업으로서 `Promise` status를 업데이트 해주고 `then`으로 전달받은 callback 함수가 있다면 실행 시켜 준다. 이때 callback 함수를 바로 호출 하는 것이 아니라 Event loop를 통해 callback queue에 보내준 다음 call stack 작업이 모두 끝난 다음 실행시키도록 해야하는데 event loop를 활용하는 가장 쉬운 방법은 `setTimeout` 같은 timer web API를 사용하는 방법이다. Timer API를 사용해서 비동기 방식으로 동작하게끔 처리해도 되지만, `Promise`는 기존 API들이랑 다른 점이 있는데 callback queue에서 우선순위를 가진다는 점이다.
``` javascript
console.log('console 1');
setTimeout(() => {
console.log('console 2');
}, 0);
console.log('console 3');
new Promise((res) => {
res();
}).then(() => {
console.log('console 4');
});
console.log('console 5');
```
위와 같이 실행할 경우
1. `console.log` 가 call stack에서 실행
2. `setTimeout` 은 event loop로 처리
3. `console.log` 가 call stack에서 실행
4. `Promise` 는 event loop로 처리
5. `console.log`가 call stack에서 실행
6. `Promise` callback 실행
7. `setTimeout` 실행
따라서 console은 아래와 같이 찍힌다
```javascript
console 1 // 첫번째 console.log
console 3 // 두번째 console.log
console 5 // 세번째 console.log
console 4 // Promise then callback
console 2 // setTimeout
```
따라서 `Promise`에 우선순위를 주고 싶다면 `setTimeout`으로 비동기를 구현할 수 없다. 그렇다면 어떻게 구현해야 될까? 브라우저는 Web API로 [queueMicrotask](https://developer.mozilla.org/en-US/docs/Web/API/queueMicrotask)를 제공하고 있다. 해당 API를 사용할 경우 다른 비동기 함수들보다 우선적으로 처리되어 실제 `Promise`가 동작하는 것과 비슷하게 구현할 수 있다.
``` javascript
_resolve(value) {
// state가 pending이 아닐 경우
if (this.state !== 'pending') return;
this.state = 'fulfilled';
this.result = value;
queueMicrotask(() => {
// then으로 호출하지 않을 경우 더이상 호출할 필요가 없음
if (this.onFulfilled === undefined) return;
try {
const returnValue = this.onFulfilled(this.result);
const isReturnValuePromise = returnValue instanceof MyPromise;
if (!isReturnValuePromise) {
this.thenPromiseResolve(returnValue);
} else {
returnValue.then(this.thenPromiseResolve, this.thenPromiseReject);
}
} catch (error) {
this.thenPromiseReject(error);
}
});
}
```
자바스크립트에는 `private`을 구현하기 위해선 `#`을 붙여주거나 이전 부터 자주 사용되던 `_`를 통해서 간접적으로 `private`임을 알려주는 방식이 있다. `queueMicrotask`에서는 이후에 수행되어야할 `then` fulfilled callback 함수가 있을 경우 호출해 주고 아니면 작업을 마친다.
### private reject
`reject`도 status 값을 제외하곤 `resolve`와 동일하게 수행된다.
```javascript
_reject(error) {
if (this.state !== 'pending') return;
this.state = 'rejected';
this.result = error;
queueMicrotask(() => {
if (this.onRejected === undefined) return;
try {
const returnValue = this.onRejected(this.result);
const isReturnValuePromise = returnValue instanceof MyPromise;
if (!isReturnValuePromise) {
this.thenPromiseResolve(returnValue);
} else {
returnValue.then(this.thenPromiseResolve, this.thenPromiseReject);
}
} catch (error) {
this.thenPromiseReject(error);
}
});
}
```
`private resolve`와 `reject`를 구현하고 나면 `static resolve`와 `reject`구현은 수월하다.
### static resolve, reject
```javascript
static resolve(value) {
const isValuePromise = value instanceof MyPromise;
if (isValuePromise) {
return value;
}
return new MyPromise((resolve) => {
resolve(value);
});
}
static reject(value) {
return new MyPromise((_, reject) => {
reject(value);
});
}
```
`resolve`의 경우 파라미터 값이 `Promise`라면 그대로 return 해주며 아닐 경우 새로운 `Promise` 생성 후 값을 바로 넘겨준다. `reject`의 경우 곧바로 `reject`를 호출하면 된다.
### catch
`catch`의 경우 `then`을 통해 `onRejected` 함수를 등록해줘서 혹시나 `catch` 함수가 이전에 정의되어 있지 않을 경우, 바로 수행할 수 있도록 해준다.
```javascript
catch(onRejected) {
return this.then(undefined, onRejected);
}
```
이로서 기본적인 `Promise`구현은 끝이 났으며 전체 코드는 아래와 같다.
``` javascript
class MyPromise {
constructor(executor) {
this.state = 'pending';
try {
executor(this._resolve.bind(this), this._reject.bind(this));
} catch (error) {
this._reject(error);
}
}
_resolve(value) {
if (this.state !== 'pending') return;
this.state = 'fulfilled';
this.result = value;
queueMicrotask(() => {
if (this.onFulfilled === undefined) return;
try {
const returnValue = this.onFulfilled(this.result);
const isReturnValuePromise = returnValue instanceof MyPromise;
if (!isReturnValuePromise) {
this.thenPromiseResolve(returnValue);
} else {
returnValue.then(this.thenPromiseResolve, this.thenPromiseReject);
}
} catch (error) {
this.thenPromiseReject(error);
}
});
}
_reject(error) {
if (this.state !== 'pending') return;
this.state = 'rejected';
this.result = error;
queueMicrotask(() => {
if (this.onRejected === undefined) return;
try {
const returnValue = this.onRejected(this.result);
const isReturnValuePromise = returnValue instanceof MyPromise;
if (!isReturnValuePromise) {
this.thenPromiseResolve(returnValue);
} else {
returnValue.then(this.thenPromiseResolve, this.thenPromiseReject);
}
} catch (error) {
this.thenPromiseReject(error);
}
});
}
then(onFulfilled, onRejected) {
const isOnFulfilledFunction = typeof onFulfilled === 'function';
this.onFulfilled = isOnFulfilledFunction ? onFulfilled : (value) => value;
const isOnRejectedFunction = typeof onRejected === 'function';
this.onRejected = isOnRejectedFunction
? onRejected
: (error) => {
throw error;
};
return new MyPromise((resolve, reject) => {
this.thenPromiseResolve = resolve;
this.thenPromiseReject = reject;
});
}
catch(onRejected) {
return this.then(undefined, onRejected);
}
static resolve(value) {
const isValuePromise = value instanceof MyPromise;
if (isValuePromise) {
return value;
}
return new MyPromise((resolve) => {
resolve(value);
});
}
static reject(value) {
return new MyPromise((_, reject) => {
reject(value);
});
}
}
```