[1] Promise
Promise에 대해 공부하기 전까지 Promise를 그냥 사용했었다. 비동기 함수들의 실행 순서 보장할 수 있고 콜 스택에서 우선순위가 높다. 이정도만 알고 api 호출하면 반환 값으로 인지하며 사용하면서 익혔다. 그러나 최근 일은 아니지만, 리액트의 Suspense를 처음 접하면서 Suspense에 대해 알아보다가 Suspense의 fallback이 나오는 조건으로 `throw new Promise...` 이런걸 봤는데 당시 되게 충격이었다. 기존에 알고 있던 Promise에 관련된 지식으로는 도저히 이해를 못하겠던 코드였기 때문이다.
그래서 그때부터 Promise에 대해 공부하기 시작했고, Promise 동작을 조금씩 따라 구현해봤다. 처음엔 되게 간단한 코드였지만, 좀 오래 만지다보니 코드도 많이 길어졌다. Promise 표준을 정의한 곳도 찾았다. https://promisesaplus.com/
이거 말고도 표준은 더 있다.
Promises/A+
Here “platform code” means engine, environment, and promise implementation code. In practice, this requirement ensures that onFulfilled and onRejected execute asynchronously, after the event loop turn in which then is called, and with a fresh stack. Th
promisesaplus.com
Promise를 구현하면서, 단순 Promise 로직 이해뿐만이 아니라, try-catch문에 대한 이해가 달라졌다. `throw new Promise...`가 임팩트가 진짜 컸다. 사실 해당 코드를 이해하기 위해서 Promise, try-catch 모두 다시 공부했다. 아무튼, 이제는 catch문이 최후의 보루로 보이지 않고 그냥 코드의 흐름중 하나로 보인다. 원래는 try-catch문을 최대한 안쓰려고 했는데, 이제는 catch문을 의도해서 쓸 때도 있다.
예를 들어 websocket을 통해 엄청나게 많은 데이터를 빠르게 받을 때, 코드를 안전하게 짜려면 if 문을 통해 불안전한 데이터를 걸러낸 후 로직을 수행해야 한다. 그런데 if문 없이 바로 로직을 수행하고 데이터 이상하면 catch에서 잡는것을 의도하는 것이다. 왜냐하면, 어차피 try-catch는 같은 컨텍스트라서 메세지 처리 순서가 꼬이지 않고 처리 속도는 충분히 빨랐기 때문에 그냥 catch에서 잡아서 해결 했었다.
정리하자면, catch문을 정상적인 초기화 패턴의 일부로 사용했다. 메세지를 받아서 처리할 때 특정 상태의 초기화가 늦어 에러가 발생했다. 그냥 catch문에서 초기화하고 수행하게 했다. 말이 조금 이상한데 결과적으로 onmessage에 if문 몇 개 줄어서 내심 뿌듯했다. 물론 주석을 통해 의도를 꼼꼼히 적었다. (주석은 빌드 파일에 안들어가게 해놨다.) 이게 올바른 방법이라고는 생각하지 않는다 ㅎㅎ...
서론이 길었다. 아무튼 이렇게 블로그 정리를 마지막으로 Promise에 관련된 공부는 마무리 하려 한다. 결국 thenable이 핵심이고 나름 킥이라고 생각되는건 /** */ 을 사용했다.
const PENDING = 0;
const FULFILLED = 1;
const REJECTED = 2;
// 체이닝과 thenable 객체 처리
const resolvePromise = (promise, x, resolve, reject) => {
// 순환 참조 감지 (Promise/A+ 2.3.1)
if (promise === x) {
return reject(new TypeError('순환 참조 발생'));
}
// x가 Promise 또는 thenable 객체인지 확인 (Promise/A+ 2.3.3)
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
// 여러 번 호출x (Promise/A+ 2.3.3.3.3)
let isCalled = false;
try {
// x.then 속성에 접근 (Promise/A+ 2.3.3.1)
const then = x.then;
// x가 thenable이 아니면 그냥 resolve (Promise/A+ 2.3.3.4)
if (typeof then !== 'function') {
resolve(x);
return;
}
then.call(
/**
* 객체를 this로 호출되어야 한다고 명시됨 (Promise/A+ 2.3.3.3)
*/
x,
// resolve 정의 (Promise/A+ 2.3.3.3.1)
(y) => {
if (isCalled) {
return;
}
isCalled = true;
resolvePromise(promise, y, resolve, reject);
},
// reject 정의 (Promise/A+ 2.3.3.3.2)
(r) => {
if (isCalled) {
return;
}
isCalled = true;
reject(r);
}
);
} catch (e) {
// x.then 접근이나 호출 중 예외 발생 시 (Promise/A+ 2.3.3.2, 2.3.3.3.4)
if (isCalled) {
return;
}
isCalled = true;
reject(e);
}
} else {
// x가 객체나 함수가 아니면 resolve (Promise/A+ 2.3.4)
resolve(x);
}
};
class PPromise {
constructor(executor) {
this.state = PENDING;
this.value = undefined;
this.callbacks = null;
const resolve = (value) => {
// 값이 Promise인 경우 Promise 체이닝
if (value instanceof PPromise) {
value.then(resolve, reject);
return;
}
// Promise는 상태 변경이 한 번만 일어남
if (this.state !== PENDING) {
return;
}
this.state = FULFILLED;
/**
* 값
*/
this.value = value;
this._executeCallbacks();
};
const reject = (reason) => {
// Promise는 상태 변경이 한 번만 일어남
if (this.state !== PENDING) {
return;
}
this.state = REJECTED;
/**
* 에러
*/
this.value = reason;
this._executeCallbacks();
};
// 받은 함수 실행
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
_executeCallbacks() {
if (!this.callbacks) {
return;
}
// 단일 콜백
if (!Array.isArray(this.callbacks)) {
this._executeCallback(this.callbacks);
this.callbacks = null;
return;
}
const callbacks = this.callbacks;
/**
* 재귀적 콜백 실행 중 새 콜백 추가 방지
*/
this.callbacks = null;
callbacks.forEach((callback) => this._executeCallback(callback));
}
_executeCallback(callback) {
/**
* 마이크로태스크 큐에 콜백 실행 예약 + Promise의 비동기 실행 보장
*/
queueMicrotask(() => {
const { promise, onFulfilled, onRejected, resolve, reject } = callback;
try {
if (this.state === FULFILLED) {
if (typeof onFulfilled !== 'function') {
// 함수 아니면 그대로 전달 (Promise/A+ 2.2.7.3)
resolve(this.value);
} else {
const result = onFulfilled(this.value);
resolvePromise(promise, result, resolve, reject);
}
return;
}
if (this.state === REJECTED) {
if (typeof onRejected !== 'function') {
// 함수 아니면 그대로 전달 (Promise/A+ 2.2.7.4)
reject(this.value);
} else {
const result = onRejected(this.value);
resolvePromise(promise, result, resolve, reject);
}
}
} catch (e) {
reject(e);
}
});
}
then(onFulfilled, onRejected) {
// 새 Promise 생성 (Promise/A+ 2.2.7)
const nextPromise = new PPromise((resolve, reject) => {
const callback = {
/**
* resolvePromise에서 x임
*/
promise: nextPromise,
onFulfilled,
onRejected,
resolve,
reject,
};
if (this.state === PENDING) {
// 콜백 저장
if (!this.callbacks) {
this.callbacks = callback;
} else if (Array.isArray(this.callbacks)) {
this.callbacks.push(callback);
} else {
// 두번째 콜백
this.callbacks = [this.callbacks, callback];
}
} else {
this._executeCallback(callback);
}
});
return nextPromise;
}
catch(onRejected) {
// then(undefined, onRejected)와 동일
return this.then(undefined, onRejected);
}
finally(callback) {
return this.then(
(value) => PPromise.resolve(callback()).then(() => value),
(reason) =>
PPromise.resolve(callback()).then(() => {
throw reason;
})
);
}
static resolve(value) {
if (value instanceof PPromise) {
return value;
}
const promise = new PPromise(() => {});
promise.state = FULFILLED;
promise.value = value;
return promise;
}
static reject(reason) {
const promise = new PPromise(() => {});
promise.state = REJECTED;
promise.value = reason;
return promise;
}
}
'Frontend > Javascript' 카테고리의 다른 글
[Javascript] Execute Context, Lexical Environment, Closure, Promise, Async/Await 요약 (1) | 2024.08.08 |
---|---|
[Javascript] 자바스크립트 애니메이션, requestAnimationFrame (0) | 2024.05.08 |
[Javascript] DOM, HTML DOM API, document (0) | 2024.05.04 |
[Javascript] 자바스크립트 특징 (0) | 2024.03.17 |
[Javascript] Ajax 개념, 사용 (회원가입시 ID 중복체크) (0) | 2022.07.05 |