웹사이트 검색

재귀 - 자바스크립트에서 재귀 비동기 함수가 ?를 초래하는 이유


다음 코드 조각을 고려해보세요:

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