[JS] Promise 직접 구현된 줄 알았지만, 실패한 이야기 // then 에 대하여

준리·2022년 9월 29일
3

비동기 프로그래밍

목록 보기
9/10
post-thumbnail

[JS] promise 직접 만들어 본 사람?

then // catch // finally 추가구현

new Promise 만들어 놓은걸 잘 쓰고 싶다.
수 만가지 핑계를 뒤로 하고 마음을 다 잡았다.

  1. 무엇을 하고자 하는지 인지한다.
  2. 전략을 세운다.
  3. 그 방법으로 접근해본다.
  4. 왜 안되는지 확인하고 다른 시도를 해본다.

위 과정을 겪으며, 다시 반복하며promise.then을 구현해본다.

Promise 함수에 then,catch,finally를 구현하시오(여러개 가능)

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을 구현하겠다.
  1. then의 콜백함수를 어떻게 관리할 것 인지
  2. return 값을 어떻게 보관하고 활용할 것인지

두 가지 문제를 코드를 짜며 접근해보겠다.

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!!!"));

직접 구현한 Promise code

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";
    }
}

위 코드를 그냥 실행해보면 아래와 같은 결과가 나온다.

#1 then(callback) 관리하기

//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>>> 가 출력되었다. 무언가 에러가 다시 발생한 것이다.

#2 then.chaining // then이 흘러내린다.

내가 알던 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 메서드를 손봐야겠다.

#3 thenCallBackArr.forEach((cb) => {

여기서부터 약간 머리가 골치아파진다. 이게 함수형 프로그래밍의 매력일까... 하나하나 씹어보자

고이 담아둔 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.

#3-1 사라진 p.then.res11>>> ??? 찾기

    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 값을 그대로 사용해서 그런 것 같았다.

#3-2 사라진 첫번째 then의 return 값 찾기

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 값을 타입을 파악해볼 필요가 있다. 그리고 새로운 조치를 취해야 한다.

#3-3 value 값이 promise 객체라면...

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) 여기에 걸린다면 valuepromise 객체일 것이고 then 메서드를 가지고 있을 것이다. 그걸 console.log로 찍어보겠다.

오오오 우리가 원하는 return 값을 뽑아냈다. 1!

이 값을 담아 두번째 then이 실행될 때 출력해주면 되겠다.

#3-4 최최최최최종 resolve 메서드.js 👨🏽‍💻

    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);
            }
        });
    };

#3-5 추가적으로 시도한 resolve

   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전반을 깊이 있게 배우고 실무에 사용되는 프로젝트를 다룬다. 앞으로 그 과정 중의 내용을 블로그에 다루고자 한다.

profile
트렌디 풀스택 개발자

1개의 댓글

comment-user-thumbnail
2022년 9월 29일

잘 읽고 갑니다. ^-^

답글 달기