Post

JavaScript Async 함수는 Promise 객체를 반환하는가?

JavaScript Asynchronous 함수는 Promise 객체를 반환하는가? 단순하게 생각하면 정답이라 생각할 수 있지만, 조금만 깊게 들어가면 부분적으로 오답이 될 수 있다. async function 선언은 AsyncFunction 객체를 반환하는 하나의 비동기 함수를 정의한다.

async function 선언은 AsyncFunction 객체의 인스턴스를 생성한다. 즉, async function을 정의하면 그 함수는 AsyncFunction 객체의 인스턴스가 되며, 이 인스턴스는 Promise를 반환한다. 아래 코드처럼 AsyncFunction 객체는 별도로 생성되는 것이 아니라 async function의 생성자 형태를 나타낸다.

1
const AsyncFunction = async function () {}.constructor;

이처럼 async 함수가 직접적으로 Promise 객체를 반환하지 않는다. AsyncFunction 객체의 인스턴스가 함수의 결과 값을 평가하여 Promise 객체를 감싸는 작업을 수행한다. 즉, 결과에 따라 Promise.resolve 또는 Promise.reject를 호출하여 최종으로 반환되는 값이 Promise 객체가 되는 것이다.

또한, 예를 들어서 async 함수 내에서 return 42;와 같이 단순한 값을 반환하면, 이 값은 자동으로 Promise.resolve(42)로 감싸져 반환된다.

1
2
3
async function foo() {
    return 42; // Promise.resolve(42)
}

우리에게 잘 알려져 있고 익숙한 async/await 사용법과는 다른 형태로, 아래 코드처럼 AsyncFunction 생성자로 비동기 함수를 만들 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function resolveAfter2Seconds(x) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(x);
    }, 2000);
  });
}

let AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;

let a = new AsyncFunction(
  "a",
  "b",
  "return await resolveAfter2Seconds(a) + await resolveAfter2Seconds(b);",
);

a(10, 20).then((v) => {
  console.log(v); // 4초 후에 30을 출력
});

이 코드에서는 a(10, 20)이 호출되는 시점에, AsyncFunction 생성자를 사용하여 정의된 비동기 함수 a는 이미 실행되어 Promise 객체를 반환한다. 이 함수는 resolveAfter2Seconds 함수를 두 번 비동기적으로 호출하여, 각각의 결과를 기다린 후에 덧셈 연산을 수행한다. 이 작업은 총 4초가 소요되며, 결과적으로 30을 출력한다.

AsyncFunction 생성자는 함수의 인스턴스를 생성할 때 사용되며, 이를 통해 비동기 함수의 동작을 runtime에 동적으로 수정할 수 있다. 즉, AsyncFunction 생성자에 의해 생성된 비동기 함수는 runtime에 생성된다. 이는 함수가 코드 내에서 미리 정의된 형태(함수 선언식이나 함수 표현식)로 존재하는 것이 아니라, runtime에 함수의 내용이 결정된다는 것을 의미한다. 예를 들어, 함수의 본문이 문자열 형태로 AsyncFunction 생성자에 전달되며, 이 문자열은 실행 중에 함수로 변환되고 실행된다. 이 방식은 코드를 작성하는 시점이 아닌, 프로그램이 실제로 실행되는 시점에 함수를 생성하고 정의할 수 있다.

그러면 우리는 왜 그동안 AsyncFunction 생성자 방식을 주로 사용하지 않은 것인가? AsyncFunction 생성자로 생성된 함수는 runtime에 평가되고 컴파일되어야 하며, 이는 추가적인 처리를 요구한다. 반면에 우리가 잘 아는 ‘async function expression’으로 이미 정의된 함수는 더 효율적으로 실행된다. 이는 컴파일 시에 코드를 처리하며, runtime에는 추가적인 처리가 필요하지 않다. 따라서, 대부분의 개발 환경에서는 더 간결하고 효율적인 우리가 잘 아는 방법으로 async 함수를 정의한다.

아래 코드는 위에서 언급한 코드와 동일한 동작을 수행하지만, async function expression을 사용하여 정의되었다.

1
2
3
async function a(a, b) {
  return await resolveAfter2Seconds(a) + await resolveAfter2Seconds(b);
}

결론적으로, async 함수는 Promise 객체를 반환한다고 볼 수 있다. 하지만 더 정확히 말하면, async 함수를 정의하는 것 자체는 AsyncFunction 객체의 인스턴스를 생성하는 것이다. 이 객체는 비동기 함수를 정의하고, 실행되는 시점에 비동기 함수의 결과 값을 평가하여 Promise 객체를 반환한다. 근래 주니어 개발자 면접 질문에서 빈번하게 등장하는 질문에 대해 단순히 ‘async 함수는 Promise 객체를 반환한다’라고 답하는 것보다는, async 함수가 반환하는 값이 Promise 객체가 되는 과정을 이해하는 것이 더 중요하다.

Reference

  1. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncFunction
  2. https://tc39.es/ecma262/multipage/control-abstraction-objects.html#sec-async-function-objects
This post is licensed under CC BY 4.0 by the author.