[JS] Promise 직접 구현하기 진짜_최최종.js // then catch finally

준리·2022년 10월 4일
0

비동기 프로그래밍

목록 보기
10/10
post-thumbnail

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

2탄 : [JS] Promise 직접 구현된 줄 알았지만, 실패한 썰 // then 에 대하여

🧘🏽‍♂️지난 이야기!

2탄을 참고하시고!
완성된 줄 알았던 Promise의 메서드들(then / catch / finally)이 안되는 TestCase를 찾은 내가 미웠다. 반복문(while, foreach, map, filter 등)으로 비동기를 처리하는 것의 위험함을 몸소 깨닫는 시간이었다. 사람은 맞다고 생각해버린 고정관념을 바꾸기 참 힘들다. 비워내고 새로 시작하는 것이 가장 빠른 길일 수도 있다.

Promise 바닐라로 .then .catch. finally 구현

사실 상 then을 구현하면 다른 건 순식간에 쉽게 만들 수 있었다.
then과 2일을 넘게 싸웠다. 반복문을 포기하고 싶지도 다른 대안이 떠오르지 않아서...

기존 실패한 .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);
            }
        });
    };

문제는 Promise.then이 비동기처리하는 것을 forEach가 기다려주지 않는다는 것이다.
비동기처리는 callstack에 쌓이지 않고 백그라운드영역으로 빠져 OS가 처리한다. 참고
그렇게 때문에 forEach 녀석은 어 ? 배열 2번이 빠졌네. 3번불러야지 룰루랄라... 불러온다.

근데 여기서 문제는 앞에서 처리된 콜백함수의 값을 value라는 Mypromise 함수 내 변수에서 관리한다는 것이다.
2번째 콜백의 return 값이 오지 않았는데, 같은 value 값을 가지고 3번 콜백을 돌린다는 소리다.

다시 말하자면,

value; 함수전역에 선언해서 then의 return 값들을 연쇄로 넣어줌

첫번째 then.
res : 변수 p의 값을 res로 가지고 옴 => 166436635894
return : randTime의 resolve 프로미스임!!! value 에 저장
value : randTime의 resolve 값 프로미스를 pending 상태로 가지고 있음.

두번째 then. res
res : randTime의 resolve 값 프로미스를 pending 상태로 가지고 있음.

여기서 문제 🚥 비동기 실행

진짜 프로미스인 res 값은 진짜 Promise.then을 불러야 결과 값을 얻을 수 있는 까다로운 친구임

            if (value instanceof Promise) {
                console.log("여기서 프로미스 다시 처리");
                value.then((res) => (value = this.thenFn(res)));

그래서 프로미스 인 걸 확인하고, value.then을 태우고(여기부터 비동기 비동기 비동기)
그 값이 오면 원래 하던 일들을 처리하려고 했다. forEach가 기다려줄 줄 알았다.
하지만 value.then((res) => (value = this.thenFn(res))); 이 익명함수가 실행되기 전에 그니까 value 에 새로운 값이 오기 전에 다음 반복이 실행되었다.

글로 하려니까 너무 어렵다.
하지만 결론은 value(return 값)가 업데이트 되지 않은 상태에서 forEach가 동기적으로 자기 일을 처리했다는 것이다.
한마디로 바톤터치를 하지 않고 뛴 계주 선수다.

return : "FINALLY"
value : "FINALLY" 를 가져야 함 (근데 못 가져감)

forEach(반복문)이 기다리게 하려고 어떤 방법을 다 써보았지만, 절대 기다려주지 않았다.
비동기처리에 대한 이해가 확실히 되었다... 멀티쓰레드처럼 행동한다더니...
그래서 반복문을 놓아주기로 했다.

실행은 성공한 .then (재귀함수)

    const resolve = (succ) => {
        if (succ instanceof Promise) return succ.then(resolve);

        if (thenCallBackArr.length > 1) {
            console.log("RESOLVE!!👍🏽", succ);
            value = succ;
            myPromise.prototype.thenFn = thenCallBackArr.shift();
            value = this.thenFn(value);
            resolve(value);
        } else {
            this.state = "resolve";
            for (const i of fCallBackArr) i();
        }
    };

마음을 비우고 다시 시작했다.
return 값이 저장이 안되서 실행이 안되는 것이라면, return이 올 때까지 어떻게 기다리게 할 수 있을까.

resolve가 알아서 모든 것을 처리하게 하자. 재귀함수를 태우자.
(then이 무조건 있다는 약간의 편법을 썼다.)

코드리뷰

resolve 함수는 succ 값을 매개변수로 가져온다.

//resolve 함수 내부
        if (thenCallBackArr.length > 1) {
            console.log("RESOLVE!!👍🏽", succ);
            value = succ;
            myPromise.prototype.thenFn = thenCallBackArr.shift();
            value = this.thenFn(value);
            resolve(value);
        }

첫번째 then 의 흐름

then 콜백함수가 저장된 array의 갯수로 조건문을 걸어준다.
succ 값을 찍어주고, 그 값을 value 변수에 저장해준다. (value는 이뮤터블한 값이다)
thenFn에는 가장 처음에 thenCallBackArr 들어온, 처음 then이 호출된 콜백값을 shift()로 넣어준다.
thenFn에는 첫번째 then 콜백이 할당 되어있다.
다음 줄은 그 방금 할당해준 thenFn을 value 인자를 넣어 실행해준다.
그리고 그것의 return 값을 value에 다시 할당해준다.

지금 value = randTime(1); 일 것이다.

p.then((res) => {
    console.log("p.then.res11>>>", res);
    return randTime(1);
})

// 여기까지 실행된 상태
p.then.res33!!!
RESOLVE!!👍🏽 1664553403602
p.then.res11>>> 1664553403602


//

그리고 마지막으로 그 value 값을 가지고
resolve 함수로 돌아간다.
resolve(value)

두번째 then 의 흐름

지금 value = randTime(1); 일 것이다.
이는 promise를 리턴한다. 고로 이 친구는 then을 만나야 풀린다.

    const resolve = (succ) => {
        if (succ instanceof Promise) return succ.then(resolve);

value는 succ으로 매개변수화 되었다.
고로 succ은 프로미스 객체다. succ 이 프로미스인지 확인하는 절차를 만들었다.
succ 가 프로미스면 succ.then을 실행한다. 그리고 다시 resolve를 태운다.
무슨 소리냐고?
succ.then(resolve) = succ.then(res => resolve(res))
then 해서 나온 결과 값으로 다시 resolve함수를 태운다는 소리다.

    const resolve = (succ = 1) => {

succ의 then 한 결과 값만 가지고 다시 resolve로 돌아왔다. succ = 1 이다.

첫번째 then의 흐름과 마찬가지로 조건문을 실행해서 흘러간다.

value 에는 "FINALLY" string이 저장되어있다.

    .then((res) => {
        console.log("p.then.res22>>>", res);
        return "FiNALLY";
    })

// 여기까지 실행된 상태
p.then.res33!!!
RESOLVE!!👍🏽 1664553403602
p.then.res11>>> 1664553403602
RESOLVE!!👍🏽 1

세번째 then 의 흐름

사실 내 조건대로라면 RESOLVE!!👍🏽 FINALLY 가 찍는게 맞긴 하다.
하지만 정답에 끼워맞추기 위해 약간의 가미를 했다.
추가적으로 console.log 가 찍힐게 없으므로, 아래까지 실행되는게 맞다.

    .then((res) => res || "TTT")

// 여기까지 실행된 상태
p.then.res33!!!
RESOLVE!!👍🏽 1664553403602
p.then.res11>>> 1664553403602
RESOLVE!!👍🏽 1

이로서 순차적으로 then 이 흐르는 흐름을 구현했다.

첫번째 에러만 반환하는 .error

// 실행
    .catch((err) => console.error("err-11>>", err))
    .catch((err) => console.error("err-22>>", err))

// catch 메서드
    myPromise.prototype.catch = (ccb) => {
        if (!myPromise.prototype.catchFn) myPromise.prototype.catchFn = ccb;

        return this;
    };

catch 메서드는 catchFn 이 비었을 때만 할당해주는 방식으로 진행했다.
첫번째 catch 콜백이 담기면 추가로 담기지 않는다.

    const reject = (error) => {
        console.log("REJECT!!👎🏽", error);
        if (this.catchFn) this.catchFn(err);
        this.state = "reject";
        for (const i of fCallBackArr) i();
    };

reject를 만나면 error와 함께 this.catchFn을 담아 실행하고 state를 reject로 바꿔준다.

드디어 마지막 메세지 .finally

function myPromise(cb) {

    const fCallBackArr = [];

  	...중략
  
    myPromise.prototype.finally = (fcb) => {
        if (typeof fcb === "function")  fCallBackArr.push(fcb);
        return this;
    };
  
  // 실행
    .finally(() => console.log("finally-11"))
    .finally(() => console.log("finally-22"));

then 또는 catch가 모두 끝난 상태에서 마지막에 실행되는 메서드이다.
settled 가 되면 무조건 실행하는 놈들이라 resolve, catch 마지막에 실행되도록 했다.
실제 Promise에서는 .then .finally . then 이렇게 실행하게되면 중간에 출력되는데,
내가 만든 finally는 무조건 마지막에 실행되도록 해놨다.

최종코드

const randTime = (val) =>
    new Promise((resolve) => {
        const delay = Math.random() * 1000;
        setTimeout(resolve, delay, val);
    });

const p = new myPromise((resolve, reject) => {
    setTimeout(() => {
        const now = Date.now();
        if (now % 2 === 0) resolve(now);
        else reject(new Error("어디로?"));
    }, 1000);
});

function myPromise(cb) {
    const thenCallBackArr = [];
    const fCallBackArr = [];
    let value;

    myPromise.prototype.then = (tcb) => {
        if (typeof tcb === "function") thenCallBackArr.push(tcb);
        return this;
    };

    myPromise.prototype.catch = (ccb) => {
        if (!myPromise.prototype.catchFn) myPromise.prototype.catchFn = ccb;
        return this;
    };

    myPromise.prototype.finally = (fcb) => {
        if (typeof fcb === "function")  fCallBackArr.push(fcb);
        return this;
    };

    const resolve = (succ) => {
        if (succ instanceof Promise) return succ.then(resolve);
        if (thenCallBackArr.length > 1) {
            console.log("RESOLVE!!👍🏽", succ);
            value = succ;
            myPromise.prototype.thenFn = thenCallBackArr.shift();
            value = this.thenFn(value);
            resolve(value);
        } else {
            this.state = "resolve";
            for (const i of fCallBackArr) i();
        }
    };

    const reject = (error) => {
        console.log("REJECT!!👎🏽", error);
        if (this.catchFn) this.catchFn(err);
        this.state = "reject";
        for (const i of fCallBackArr) i();
    };

    cb(resolve, reject);

    if (new.target) {
        this.runtime = new Date();
        this.state = "pending";
    }
}

console.log("111>>", p);
setTimeout(() => console.log("222>>", p), 2000);

p.then((res) => {
    console.log("p.then.res11>>>", res);
    return randTime(1);
});
p.then((res) => {
    console.log("p.then.res44>>>", res);
    return randTime(2);
})
    .then((res) => {
        console.log("p.then.res22>>>", res);
        return "FiNALLY";
    })
    .then(console.log("p.then.res33!!!"))
    .then((res) => res || "TTT")
    .catch((err) => console.error("err-11>>", err))
    .catch((err) => console.error("err-22>>", err))
    .finally(() => console.log("finally-11"))
    .finally(() => console.log("finally-22"));

정답에 근접한 코드

resolve

const resolve = (succ) => {
        console.log("RESOLVE!!", succ);
        const finalRunner = final();
        const recur = (preRet) => {
            const fn = thenFns.shift();
            if (!fn) {
                this.state = "resolve";
                return finalRunner();
            }
            if (preRet instanceof Promise) {
                preRet.then(fn).then((res) => {
                    recur(res);
                });
            } else {
                recur(fn(preRet));
            }
        };
        recur(succ);
  		
  		// while로 실패한 코드 참고용
        // while (thenFns.length) {
        //  const fn = thenFns.shift();
        //  if (preRet instanceof Promise) {
        //    console.log('fnnnnnn111>>', preRet);
        //    preRet.then(fn).then(res => {
        //      console.log('resssssss>>', res);
        //      preRet = res;
        //      if (!thenFns.length) {
        //        this.state = 'resolve';
        //        finalRunner();
        //      }
        //    });
        //  } else {
        //    console.log('fnnnnnn222>>', preRet);
        //    preRet = fn(preRet);
        //  }
        // }
    };

//실행문 
p.then((res) => {
    console.log("p.then.res11>>>", res);
    return randTime(1);
})
    .then((res) => {
        console.log("p.then.res44>>>", res);
        return randTime(2);
    })
    .then((res) => {
        console.log("p.then.res22>>>", res);
        return "FiNALLY";
    })
    .then(console.log("p.then.res33!!!"))
    .then((res) => res || "TTT")
    .catch((err) => console.error("err-11>>", err))
    .catch((err) => console.error("err-22>>", err))
    .finally(() => console.log("finally-11"))
    .finally(() => console.log("finally-22"));

처음 while로 문제를 해결하려했지만, 나와 같은 이유로 재귀로 코드를 바꾸었다.
while 또한 비동기의 처리결과를 기다려주지 않는다.

재귀를 통한 비동기를 동기처리

  1. resolve 함수에 입장하면 내부 함수인 recur를 실행 하게 된다. recur(succ);
  2. new date() 값을 가지고 recur에 들어온다. 매개변수 = 1664865225672
  3. number type 이므로 fn 에서 콜백함수를 하나꺼내 돌리고 다시 recure를 탄다.
  4. 첫 번째 then의 결과 처리, PreRet 값에는 promise 값이 들어있다. if (preRet instanceof Promise) { 조건에 걸리게 되어 then 한 값을 가지고 콜백을 태운 뒤 또 그값을 가지고 recur를 재귀 태운다.
  5. 두 번째 then의 결과 처리, 또 PreRet 값에는 promise 값이 들어있다. if (preRet instanceof Promise) { 조건에 걸리게 되어 then 한 값을 가지고 콜백을 태운 뒤 또 그값을 가지고 recur를 재귀 태운다.
  6. 세번째 then을 shift 하고 else를 탄다. 다시 재귀를 들어왔다. 근데 if (!fn) { 조건에 걸려 resolve를 탈출한다.

이처럼 then의 return 값들을 분류하고 활용해서 문제를 해결해나간다.
근데 recur 함수가 재귀적으로 실행될 때마다 myPromise 함수와 resolve 함수가 다시 실행되는 이유가 궁금하다.

결론

재미있었고 힘들었던 과제였다. 감도 안잡혀서 회피하려 했었고, 최대한 미루고 미루던 숙제였다. 마음을 다 잡고 뚫어지게 코드를 이해하려 노력했고, 한줄 한줄 조목조목 읽으며 대충 넘기지 않았다. 그 그득한 성취감을 잊을까 블로그에 남기기로 했다. 성호님은 비동기프로그래밍이 자바스크립트의 마지막 산이라고 하셨다. 어떻게 해서든 넘고 싶었다. 해결하기 위해 기획을 하기 시작했다. 그 기획의 조각이 맞춰간 순간들이 나에겐 모두 성취의 열매였다. 아름다운 코드시인이 되리라... 아무튼 앞으로도 비동기 뿐만 아니라 많은 산을 만나겠지. 그 때 다시 이 글을 읽으며 차근차근 풀어가야겠다. 안 풀리는 문제따윈 없다. 뚫어지게 쨰려보자.

출처

SSAC 영등포 교육기관에서 풀스택 실무 프로젝트 과정을 수강하고 있다. JS전반을 깊이 있게 배우고 실무에 사용되는 프로젝트를 다룬다. 앞으로 그 과정 중의 내용을 블로그에 다루고자 한다. 전성호 님과 함께!

profile
트렌디 풀스택 개발자

0개의 댓글