new Promise
만들어 놓은걸 잘 쓰고 싶다.
수 만가지 핑계를 뒤로 하고 마음을 다 잡았다.
위 과정을 겪으며, 다시 반복하며promise.then
을 구현해본다.
const p = new Promise((resolve, reject) => {
setTimeout(() => {
const now = Date.now();
if (now % 2 === 0)
resolve(console.log('fulfill', now));
else reject(new Error('어디로?'));
}, 1000);
});
p.then(res => {
console.log('p.then.res11>>>', res);
return randTime(1);
})
.catch(err => console.error('err-11>>', err))
.catch(err => console.error('err-22>>', err))
.then(res => {
console.log('p.then.res22>>>', res);
return 'FiNALLY';
})
.then(console.log('p.then.res33!!!'))
.then(res => res || 'TTT')
.finally(() => console.log('finally-11'))
.finally(() => console.log('finally-22'));
Promise.then
을 구현하겠다.두 가지 문제를 코드를 짜며 접근해보겠다.
p 는 resolve만 반환하게 하고, then만 먼저 처리해보고자 한다.
console.log("111>>", p);
setTimeout(() => console.log("222>>", p), 2000);
p.then((res) => {
console.log("p.then.res11>>>", res);
return randTime(1);
})
.then((res) => {
console.log("p.then.res22>>>", res);
return "FiNALLY";
})
.then(console.log("p.then.res33!!!"));
function myPromise(cb) {
// prototype에 저장하는 이유는 console.log에 안나오게하려고
myPromise.prototype.then = (tcb) => {
myPromise.prototype.thenFn = tcb;
// this를 return하는 이유는 chaining을 위해서
return this;
};
myPromise.prototype.catch = (ccb) => {
myPromise.prototype.catchFn = ccb;
return this;
};
const resolve = (succ) => {
console.log("RESOLVE!!👍🏽", succ);
this.state = "resolve";
this.thenFn(succ);
};
const reject = (error) => {
console.log("REJECT!!👎🏽", error);
this.state = "reject";
this.catchFn(error);
};
cb(resolve, reject);
if (new.target) {
this.runtime = new Date();
this.state = "pending";
}
}
위 코드를 그냥 실행해보면 아래와 같은 결과가 나온다.
//error
111>> myPromise { state: 'pending' }
p.then.res33!!!
RESOLVE!!👍🏽 1664372108396
...
this.thenFn(succ);
^
TypeError: this.thenFn is not a function
에러에 집중해보자.
한글로 쉽게 풀면 thenFn이 함수가 아니기 때문에 (succ) 인자를 받아서 실행할 수 없다는 것이다.
왜 그런지 찾아보자. 위의 에러에서 p.then.res33!!!
이 실행된 것이 힌트다.
이는 콜백함수를 가지고 있지 않았기때문에 먼저 실행되었다. 여기서 prototype에 저장한 then 함수가 실행되었다는 소린데, tcb(then.callback) 가 새로만들어진 thenFn에 할당된다.
myPromise.prototype.then = (tcb) => {
myPromise.prototype.thenFn = tcb;
return this;
};
무슨 소린가하면 .then(console.log("p.then.res33!!!"));
가 먼저 실행되면서 위에 위의 then 함수를 만나게 된다. .then 의 인자 console.log("p.then.res33!!!") = tcb
가 된다.
myPromise.prototype.then = (console.log("p.then.res33!!!")) => {
myPromise.prototype.thenFn = console.log("p.then.res33!!!");
return this;
};
이렇게 된다는 소리다. 그러면 thenFn에는 함수가아닌 콘솔 obj가 들어가게 된다.
그래서 error를 출력하게 된다. 이제 error를 잡아냈으니 수정해보자.
then 매개변수 tcb가 function 일 때만 특정기능을 수행하도록 수정한다.
myPromise.prototype.then = (tcb) => {
if (typeof tcb === "function") {
myPromise.prototype.thenFn = tcb;
}
return this;
};
예상되는 then의 내부
복잡하지만 tbc 매개변수 자리에 들어올 진짜 콜백 값
myPromise.prototype.then = ((res) => {
console.log("p.then.res11>>>", res);
return randTime(1);
}) => {
if (typeof tcb === "function") {
myPromise.prototype.thenFn = (res) => {
console.log("p.then.res11>>>", res);
return randTime(1);
};
}
return this;
};
오오 뭔가 더 나은 결과가 출력되었다. resolve
메서드가 온전히 실행된 것을 볼 수 있다.
const resolve = (succ) => {
console.log("RESOLVE!!👍🏽", succ);
this.state = "resolve";
this.thenFn(succ);
};
근데 뭔가 이상하긴하다. p.then.res11>>>
이 출력되어야하는데 p.then.res22>>>
가 출력되었다. 무언가 에러가 다시 발생한 것이다.
내가 알던 promise 객체처럼 then이 순차적으로 처리될 순 없나보다. 모조리 들어와서 흘러내리듯 마지막 then 이 실행되었다. 이 부분은 수업에서 힌트를 얻었다.
"함수를 array에 푸쉬해둔다. then 1 return 값이 then 1을 먼저 실행한다." 이렇게 적어놨다...
이 힌트로 추론해보자.
1. 마지막 then의 callback만 저장되어서 마지막만 실행되었다. 고로 나머지는 씹혔다.
2. then이 연쇄적으로 실행될 때 어떻게 들어오는지 파악해보자
const thenCallBackArr = []; 👈🏽
myPromise.prototype.then = (tcb) => {
if (typeof tcb === "function") {
thenCallBackArr.push(tcb); 👈🏽
myPromise.prototype.thenFn = tcb;
}
console.log("콜백컴온 :>> ", thenCallBackArr); // array 담기는 것 확인
return this;
};
then의 callbackFn을 담을 멋진 array(thenCallBackArr)를 만들었다.
if 조건을 타고 tcb를 이 안에 push해주었다. array는 모든 것을 담을 수 있다. 물론 함수도.
재미있게도 callbackFn 이 담기는것을 내 눈으로 확인할 수 있었다.
이 콜백들을 어떻게 resolve가 실행될 때 하나씩 실행할 수 있는지를 고민해봐야 할 것이다.
resolve
메서드를 손봐야겠다.
여기서부터 약간 머리가 골치아파진다. 이게 함수형 프로그래밍의 매력일까... 하나하나 씹어보자
고이 담아둔 thenCallBackArr
의 값들을 어떻게 활용할 수 있을까? 나는 forEach
를 활용해 하나씩 꺼내서 이용해보려고 한다. 여기 담긴 값들은 callback 함수라는 것을 반드시 인지하자.
array.forEach
forEach() 메서드는 주어진 함수를 배열 요소 각각에 대해 실행합니다.
The forEach() method calls a function for each element in an array. The forEach() method is not executed for empty elements.
const resolve = (succ) => {
console.log("RESOLVE!!👍🏽", succ);
this.state = "resolve";
thenCallBackArr.forEach((cb) => {👈🏽
myPromise.prototype.thenFn = cb;
this.thenFn(succ);
});
};
thenCallBackArr.forEach
를 만들어주고 그 인자로 cb 이라는 친구를 불러준다.
그 값을 myPromise.prototype.thenFn = cb;
에 할당해준다. 이 함수는 원래 than을 실행했을때 자동으로 만들어주던 함수였다. 나는 여기로 옮겨서 사용해주기로 했다.
그리고 this.thenFn(succ);
을 실행하여 결과를 얻는다.
상식적으로 생각해보면 array에 담긴 첫번째 콜백이 실행되고, 두번째 콜백이 순차적으로 실행되는 것을 상상해볼 수 있다.
찾았다. p.then.res11>>>
!!!
숨겨져 있던 첫번째 콜백함수를 찾은 감격의 순간이다.
근데,
p.then.res22>>>
에는 첫번째 콜백의 return 값이 들어있어야 한다.
this.thenFn(succ);
여기서 succ 값을 그대로 사용해서 그런 것 같았다.
function myPromise(cb) {
const thenCallBackArr = [];
let value; 👈🏽(1)
...중략
const resolve = (succ) => {
console.log("RESOLVE!!👍🏽", succ);
this.state = "resolve";
value = succ; 👈🏽(2)
thenCallBackArr.forEach((cb) => {
myPromise.prototype.thenFn = cb;
value = this.thenFn(value); 👈🏽(3)
});
};
(1) 상위 함수 스코프 안에 value
변수를 선언해준다.
(2) resolve
메서드가 가지고 오는 매개변수를 value 값에 할당해준다.
(3) 그 값을 this.thenFn(value)
인자로 활용해주고 그 return 값을 value에 다시 할당한다.
첫번째 then.callbackFn
값이 value 변수에 저장되고 그 값을 다시 다음 then.callbackFn
에 넘겨주는 형태로 구현된다.
오오! p.then.res22>>> Promise { <pending> }
아무튼 다른 값이 찍혔다.
첫번째 then.callbackFn
이 무엇을 return 하는지 살펴보자
p.then((res) => {
console.log("p.then.res11>>>", res);
return randTime(1);
})
프로미스 값을 return 하고있다. 그래서 pending
상태인 것이다.
value
값을 타입을 파악해볼 필요가 있다. 그리고 새로운 조치를 취해야 한다.
then을 써야 풀릴텐데? 여기서 약간 헷갈린 부분은 이 자체가 프로미스라는 것을 인지하지 못하고
myPromise 를 다시 실행시켜 resolve를 태운다고 오판했다. 그래서 엄청 삽질했다.
myPromise는 유사 프로미스 이기 때문에 당연히 promise.then 할 수 없었다. 진짜 프로미스를 풀 방법이 myPromise 안에는 없었다. 답은 그 안에 있었다.
우선 value 값이 어떤 type인지를 확인해보자.
const resolve = (succ) => {
console.log("RESOLVE!!👍🏽", succ);
this.state = "resolve";
value = succ;
thenCallBackArr.forEach((cb) => {
if (value instanceof Promise) { 👈🏽(1)
console.log("여기서 프로미스 다시 처리");
value.then(console.log); 👈🏽(2)
}
myPromise.prototype.thenFn = cb;
value = this.thenFn(value);
});
};
instanceof 연산자는 생성자의 prototype 속성이 객체의 프로토타입 체인 어딘가 존재하는지 판별합니다.
(1) instanceof 로 value 가 Promise 인지를 확인한다. 해당 조건문이 동작하는지 console.log로 찍어보았다.
(2) 여기에 걸린다면 value
는 promise 객체
일 것이고 then
메서드를 가지고 있을 것이다. 그걸 console.log
로 찍어보겠다.
오오오 우리가 원하는 return 값을 뽑아냈다. 1!
이 값을 담아 두번째 then이 실행될 때 출력해주면 되겠다.
const resolve = (succ) => {
console.log("RESOLVE!!👍🏽", succ);
this.state = "resolve";
value = succ;
thenCallBackArr.forEach((cb) => {
myPromise.prototype.thenFn = cb;
if (value instanceof Promise) {
console.log("여기서 프로미스 다시 처리");
value.then((res) => (value = this.thenFn(res)));
} else {
value = this.thenFn(value);
}
});
};
const resolve = (succ) => {
if (succ instanceof Promise) {
succ.then(resolve);
return;
}
console.log("RESOLVE!!👍🏽", succ);
this.state = "resolve";
value = succ;
if (thenCallBackArr.length === 0) {
value = this.thenFn(succ);
// console.log("value :>> ", value);
}
thenCallBackArr.forEach((cb) => {
myPromise.prototype.thenFn = cb;
//TODO Promise.then을 써서 randTime(1)의 결과값을 가져와야하는데,
//비동기 처리 중 다음 forEach의 항목이 실행되면서 value값이 업데이트
//되지 않을 때 실행되어 중복이 발생함.
if (value instanceof Promise) {
console.log("여기서 프로미스 다시 처리");
new myPromise((resolve) => {
resolve(value);
});
// FIXME 프로미스 then 하는 중에 다음 콜백이 돌아서 흘러내림
// value
// .then((res) => (value = this.thenFn(res)))
// .then((res) => {
// console.log("밸류값", value);
// });
// FIXME myPromose를 한 번 더 실행시키고 구현하는 방법
// value.then(
// (res) =>
// new myPromise((resolve) => {
// resolve(res);
// })
// );
} else {
value = this.thenFn(value);
}
// 비동기처리로 인해 finally가 마지막에 찍히지 않음
// for (const i of fCallBackArr) i();
});
};
하지만 forEach를 활용해 value를 관리하면서 비동기처리에서 큰 문제가 발생했다.
이 문제를 해결하고 추가적으로 작성하도록 하겠다.![]
SSAC 영등포 교육기관에서 풀스택 실무 프로젝트 과정을 수강하고 있다. JS전반을 깊이 있게 배우고 실무에 사용되는 프로젝트를 다룬다. 앞으로 그 과정 중의 내용을 블로그에 다루고자 한다.
잘 읽고 갑니다. ^-^