재귀 - 자바스크립트에서 재귀 비동기 함수가 ?를 초래하는 이유
다음 코드 조각을 고려해보세요:
function f() {
return new Promise((resolve, reject) => {
f().then(() => {
resolve();
});
});
}
f();
이렇게도 쓸 수 있습니다:
async function f() {
return await f();
}
f();
주어진 두 코드 중 어느 것을 실행하더라도, 다음과 같은 오류가 발생합니다:
(node:23197) UnhandledPromiseRejectionWarning: RangeError: Maximum call stack size exceeded
제 질문은 왜 그런가입니다. 제 질문에 답하기 전에, 제 주장을 고려해 주세요:
저는 재귀의 개념과 멈춤 조건이 없으면 스택 오버플로가 발생한다는 것을 이해하고 있습니다. 하지만 제가 주장하는 바는, 첫 번째 f();가 실행되면 Promise를 반환하고 스택을 벗어나므로 이 재귀는 스택 오버플로를 겪지 않아야 한다는 것입니다. 제게 이것은 다음과 동일하게 동작해야 합니다:
while (1) {}
물론, 이렇게 쓰면 고정될 거예요:
function f() {
return new Promise((resolve, reject) => {
setTimeout(() => {
f().then(() => {
resolve();
});
}, 0);
});
}
f();
그것은 다른 이야기이고 나는 그것에 대해 아무 문제도 없다.
[업데이트]
내 잘못이야, 서버 측에서 node v8.10.0로 테스트하고 있었다는 것을 언급하는 것을 잊었어.
답변 #1
왜 그것이 무한 재귀를 일으키지 않을 것이라고 기대하지 않나요? 프로미스의 생성자가 f를 재귀적으로 호출하고 있기 때문에, 프로미스가 생성되기 전에 무한 재귀 루프가 발생하여 프로미스는 결코 생성되지 않습니다.
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise
위의 링크에서
실행자 함수는 Promise 구현에 의해 즉시 실행되며, resolve와 reject 함수를 전달받습니다(실행자는 Promise 생성자가 생성된 객체를 반환하기 전에도 호출됩니다).
답변 #2
@Adrian 덕분에 스택 오버플로우를 맞지 않을 방법을 찾을 수 있었습니다. 하지만 그 전에 그는 맞았고, 그 형태의 재귀는 스택 오버플로우를 일으켰어야 했습니다. 그리고 질문이 '왜'인지이기 때문에 그의 답변이 채택된 답변입니다. 이것은 스택 오버플로우를 맞지 않는 '어떻게'에 대한 저의 시도입니다.
테스트 1
function f() {
return new Promise((resolve) => {
resolve();
}).then(f);
}
그리고 await를 사용하여:
테스트 2
async function f() {
return await Promise.resolve()
.then(f);
}
이 경우에 Promise를 제거할 수 있을지 잘 모르겠어요!
그리고 나는 setTimeout은 안 된다고 말했지만, 이것도 흥미로운 사례입니다:
테스트 3
async function f() {
await new Promise(resolve => setTimeout(resolve, 0));
return f();
}
이것도 스택 오버플로를 겪지 않을 것입니다.
결국, 제가 이것에 관심을 가지게 된 이유에 대한 맥락을 드리자면; 예를 들어 AWS의 DynamoDb에서 모든 레코드를 가져오는 함수를 코딩하고 있다고 합시다. 한 번의 요청으로 DynamoDb에서 추출할 수 있는 레코드 수에 제한이 있기 때문에, 모든 레코드를 가져오기 위해 필요한 만큼 ( ExclusiveStartKey 를 사용하여) 요청을 보내야 합니다:
테스트 4
async function getAllRecords(records = [], ExclusiveStartKey = undefined) {
let params = {
TableName: 'SomeTable',
ExclusiveStartKey,
};
const data = await docClient.scan(params).promise();
if (typeof data.LastEvaluatedKey !== "undefined") {
return getAllRecords(records.concat(data.Items), data.LastEvaluatedKey);
}
else {
return records.concat(data.Items);
}
}
나는 이것이 절대로 스택 오버플로우를 겪지 않도록 확실히 하고 싶었다. 실제로 이렇게 거대한 DynamoDb 테이블을 만들어 테스트하는 것은 현실적이지 않았다. 그래서 나는 그것을 확인하기 위해 몇 가지 예제를 생각해냈다.
처음에는 테스트 #4가 실제로 스택 오버플로우에 직면할 수 있을 것처럼 보였지만, 제 테스트 #3에서는 그러한 가능성이 없음을 보여줍니다 (await docClient.scan(params).promise() 때문에).
[업데이트]
@Bergi 덕분에, Promise 없이 await를 위한 코드가 있습니다:
테스트 5
async function f() {
await undefined;
return f();
}
