인프런 영문 브랜드 로고
인프런 영문 브랜드 로고

인프런 커뮤니티 질문&답변

ki mu님의 프로필 이미지
ki mu

작성한 질문수

함수형 프로그래밍과 JavaScript ES6+

go, pipe, reduce에서 비동기 제어

질문있습니다...

작성

·

379

1

reduce 함수에서 acc인자로 비동기적인 값이 들어올 때 처리를  위해 유명함수 라는거를 사용하셨는데...

그런데 if (acc instanceof Promise) return acc.then(recur)

이 부분에서 왜 return 이 들어가야 되는건지 모르겠어요...

그냥 acc.then(recur) 하고 실행만 해주면 함수 실행되는거 아닌가요 ??? 그런데 ... return 없이 실행 하니깐 값이 제대로 안나오더라구요

답변 4

2

유인동님의 프로필 이미지
유인동
지식공유자

promise를 리턴해줘야 하기 때문입니다 :)

이 부분 때문에 미치겠군요 3일째 이해가 안되서 넘어가질 못하겠습니다. ㅠㅠ

promise를 누구에게 리턴하고 있는건가요...?

유인동님의 프로필 이미지
유인동
지식공유자

recur 함수에서 리턴하고 있습니다. 그 결과는 결국 맨 바깥에까지 전달되어 go1의 실행결과값으로 리턴됩니다. 

리턴되어 맨 바깥쪽까지 실행되는 흐름은 알겠습니다.

제일 헷갈리는는 부분은 acc.then(recur) -> 여기안에 들어가는 인자가 return쓰냐 안쓰냐에 따라 달라지는게 이해가 안됬습니다.

return function recur(acc) {
  let cur;
  while(!(cur = iter.next()).done) {
    const a = cur.value;
    console.log('acc', acc)
    console.log('fucntoin', a)
    acc = f(acc, a);
    if(acc instanceof Promise) return acc.then((result) => { // 이 부분의 리턴을 빼보고 한번 넣고 한번 실행
      console.log('result',result)
      recur(result)
    })
  }
  return acc;
} (acc)

실행결과로서 이하의 이미지에서 왼쪽이 리턴을 뺀쪽입니다.

왼쪽 이미지의 다섯번째 줄을 보면 인자로서 "이행된 Promise"가 들어가고 있군요

하지만 제일 밑줄을 보시면 result는 111이 들어가고 있네요.. result와 다섯번째 줄의 acc는

코드상에서는 똑같은 인자를 받고있지만요..

그래서 그냥 이렇게 생각했습니다 return을 시켜주지 않으면 해당시점에서 Promise가 종료된 상태가 아니라 계속 이어진다고..............라고 억지로 자기를 설득하는중입니다 ;)

1

흥미로운 질문이어서 저도 고민하고 코드로 돌려보다가 어느정도 이해한 것 같아 정리해서 올려봅니다.
이 질문으로 고민하시는 분들께 도움이 되길 바랍니다.

-----------------------------------------------

먼저 중요한 개념은 프로미스를 객체로 바라보아야 하는 것 입니다. 또한 프로미스 객체를 생성할 때, 콜백함수는 즉시 실행되지만, then() 으로 생성되는 프로미스 객체의 콜백은 콜스택이 비워진 이후에 실행된다는 점을 기억해야 합니다.

실행할 go :

go(1,
    a => a + 10,
    a => Promise.resolve(a + 100),
    a => a + 1000,
    a => a + 10000,
    console.log
)

 

1. return을 붙이지 않았을 경우

const reduce = curry((f, acc, iter) => {
    if (!iter) {
        iter = acc[Symbol.iterator]()
        acc = iter.next().value;
    } else {
        iter = iter[Symbol.iterator]()
    }

    return go1(acc, function recur(acc) {
        let cur
        while (!(cur = iter.next()).done) {
            const a = cur.value
            console.log(acc)
            acc = f(acc, a)
            if (acc instanceof Promise) 
                acc.then(recur) // return 여부에 따른 변화
        }
        return acc;
    }
    )
})

 

1
11
Promise { 111 }
[object Promise]1000
[object Promise]100010000

[object Promise]100010000

reduce 내부에 console.log를 작성하여 acc 값이 어떻게 변화하는지 확인해 보았습니다.

return을 하지 않게되면, go1 내부의 while 문이 돌면서 이전에 사용했던 acc를 그대로 가져가게 됩니다.

acc는 f(acc, a) 이므로
전 단계의 acc 가 프로미스 객체이거나,
또는 a 함수가 프로미스 객체를 반환하는 함수라면,
f(acc,a)는 프로미스 객체를 반환하게 되고,
이번 단계의 acc 는 프로미스 객체가 될 것입니다.

코드에서 acc.then(recur) 은 단순히 프로미스 객체를 생성만 하는 것입니다. 여기서는 recur 함수가 실행되지 않고, 선행된 프로미스가 완료되고 콜스택이 비워진 이후에 실행됩니다.
그러나 reduce 함수가 종료되지 않았으므로, 콜 스택이 비워지지 않습니다. 따라서 acc와 acc.then() 프로미스 객체 모두 resolve 되지 못한 채 while문이 진행됩니다.

그 말은 then()으로 생성된 promise가 resolve 되는 것을 기다리지 않고,
(콜스택이 비워져야 비로소 프로미스가 실행되어 fulfilled 된 값이 반환되므로)
다음 while 단계로 프로미스 객체를 넘겨 반복문을 이어가는 것입니다.
즉, 다음 while문에서 acc 값이 받는 값은 'fulfilled 되어 111로 resolve된 상태' 의 Promise 객체 입니다. (then()으로 생성된 프로미스가 실행된 것이 아님)

 

따라서 go 의 두번 째 함수처럼,
중간에 프로미스를 반환하는 인자가 있을경우
while문은 프로미스가 해결되는 것을 기다리지 않은 채
프로미스 객체 자체를 직접 활용하게되고,
다음함수인 a => a + 1000 는,
이 프로미스 객체를 string 처럼 인식하여
Promise { 111 } 에 다가 '1000' 이라는
string 값을 붙여버리게 된 것입니다.

 

 

2. return 을 해주었을 경우

const reduce = curry((f, acc, iter) => {
    if (!iter) {
        iter = acc[Symbol.iterator]()
        acc = iter.next().value;
    } else {
        iter = iter[Symbol.iterator]()
    }

    return go1(acc, function recur(acc) {
        let cur
        while (!(cur = iter.next()).done) {
            const a = cur.value
            console.log(acc)
            acc = f(acc, a)
            if (acc instanceof Promise) 
                return acc.then(recur) // return 여부에 따른 변화
        }
        return acc;
    }
    )
})
1
11
111
1111
11111

11111

 

반면에 return을 해주었을 경우
while문이 돌고 있던 go1 함수 자체가 종료되고,
프로미스 객체인 acc...(1) 가 resolve 되면 recur 함수를 실행하겠다는 새로운 프로미스 객체...(2) 를 생성하여 값으로 반환합니다.

이 반환된 값은 결국 reduce의 반환 값이며,
reduce가 실행되던 콜스택은 일시적으로 비워집니다.

(1) acc 프로미스 객체가 resolve 되고(애초에 resolve된 상태로 생성되었지만), 콜스택이 비워졌으므로 microQueue 에 (2) 프로미스의 콜백이 들어가고 실행됩니다. 여기서 다시 recur 함수가 실행됩니다.

recur 함수가 참조하는 값들은 모두 클로저로 유지가 되고 있기 때문에 이전과 같은 환경에서 동작이 가능합니다.

 

따라서 이후에 들어오는 함수 또는 acc 값은 일반 number 값이므로, while문 안에서 계속 재활용 하며 조건문을 만족할 때 까지 ( cur.done ) 진행됩니다.

최종적으로 reduce는 정상적으로 acc 가 11111로 계산되고, console.log 함수에 의해 그 값이 출력됩니다.

1

유인동님의 프로필 이미지
유인동
지식공유자

이 부분을 진짜 알고 싶으면 좀 더 노력해보시면 될거 같아요! 프로미스를 정확히 이해하면 알 수 있어요!

0

`return`을 해줘야 다음 `then`에서 이어져요.

ki mu님의 프로필 이미지
ki mu

작성한 질문수

질문하기