Server/Node.js (Express)

[Node.js] await vs return vs return await: 비동기 이해하기

ooeunz 2019. 11. 11. 00:28
반응형

비동기 함수를 작성할 때 awaitreturn을 하는 것 그리고 return await을 사용하는 것에는 실행에 차이가 있다. 이것을 올바르게 사용하지 않으면 우리는 예상치 못한 값을 반환받을 수 있다. 이 포스팅은 그러한 버그를 미리 방지하고 비동기에 대한 이해를 확장하고자 작성한다.


아래는 async함수이다.

async function waitAndMaybeReject() {
  // Wait one second
  await new Promise(r => setTimeout(r, 1000));
  // Toss a coin
  const isHeads = Boolean(Math.round(Math.random()));

  if (isHeads) return 'yay';
  throw Error('Boo!');
}

이 함수는 setTimout으로 1초간 대기하는 promise를 비동기적으로 실행하고, 50/50의 확률로 'yay'를 return 하거나 에러를 발생시켜서 'Boo!'를 return 한다. 해당 함수를 이용해서 밑에서 비동기적으로 어떻게 실행결과가 다른지 확인해보도록 하겠다.


Just calling

async function foo() {
  try {
    waitAndMaybeReject();
  }
  catch (e) {
    return 'caught';
  }
}

만약 foo() 함수를 호출하게 된다면 어떤 값을 반환하게 될까?
만약 'yay' 또는 'Boo!' 값이 return 되길 기대했다면 아직 비동기에 대한 이해가 부족한 상태라고 할 수 있다. 이 함수를 실행하게 되면 Promise 값을 return 하게 된다.
왜냐하면 waitAndMaybeReject() 함수는 async를 이용해 비동기적으로 작성되었다. 그러므로 foo() 함수에서도waitAndMaybeReject() 함수는 비동기적으로 호출되는데, 그 결과 함수의 반환 값을 기다리지 않고(promise가 resolve되기 전에) 바로 다음 코드를 실행하기 때문에 catch블록에 잡힐 값이 없으므로 promise만이 return 된다.


Awaiting

async function foo() {
  try {
    await waitAndMaybeReject();
  }
  catch (e) {
    return 'caught';
  }
}

그렇다면 await를 붙여서 waitAndMaybeReject() 함수의 반환 값을 기다리게 한다면 어떻게 될까?
foo() 함수를 호출하게 되면 waitAndMaybeReject()가 비동기적으로 호출되며, promise는 setTimout으로 인해 1초를 기다리게 된다. 그리고 50/50 확률로 undefined가 실행되거나 'caught'가 실행될 것이다.
왜냐하면 만약 waitAndMaybeReject() 함수가 reject 된다면 catch 블록이 에러를 잡아서 'caught'를 return 할 것이고, 만약 문제없이 실행된다면 아무런 값도 반환되지 않을 것이기 때문이다.


Returning

async function foo() {
  try {
    return waitAndMaybeReject();
  }
  catch (e) {
    return 'caught';
  }
}

이번엔 await를 없애고 바로 return 시키는 형태의 함수이다. foo() 함수를 실행시키면 waitAndMaybeReject() 함수의 반환 값을 foo() 함수에서 반환하기 위해 함수가 실행 완료 될때까지 기다리게 된다.
그 결과 waitAndMaybeReject() 함수 내부에서는 1초를 기다리게 되고 return 값이 'yay'가 return 되거나, 50/50의 확률로 Error('Boo!')가 return 된다.
waitAndMaybeReject()을 return 하게 되면 결과 값이 바로 return 되기 때문에 catch 블록에서 에러가 잡히지 않는다.


Return-awaiting

아마 이 형태의 함수가 우리가 본질적으로 가장 원하는 형태의 함수일 것이다.

async function foo() {
  try {
    return await waitAndMaybeReject();
  }
  catch (e) {
    return 'caught';
  }
}

foo() 함수를 실행시키면 promise는 1초를 기다린다. 그리고 'yay'또는 catch블록의 'caught'를 return 하게 된다.
왜냐하면 우리는 await으로 인해서 waitAndMaybeReject() 함수를 기다리게 될 것이다. 그리고 만약 err가 throw 된다면 catch 블록에서 잡혀서 'caught'를 실행하게 될 것이다. 또한 에러 없이 실행이 된다면 'yay'라는 결과 값을 return 하게 될 것이다.


만약 위의 코드들이 헷갈린다면 아래의 두 가지 단계로 생각하면 이해하기 편리하다.
1. waitAndMaybeReject()의 결과를 기다리고, fullfilledValue에 그 값을 초기화한다.
2. 만약 결과 값이 reject라면 catch블록에 잡혀서 'caught'가 return 되고, resolve라면 fullfilledValue의 값이 return 된다.

async function foo() {
  try {
    // Wait for the result of waitAndMaybeReject() to settle,
    // and assign the fulfilled value to fulfilledValue:
    const fulfilledValue = await waitAndMaybeReject();
    // If the result of waitAndMaybeReject() rejects, our code
    // throws, and we jump to the catch block.
    // Otherwise, this block continues to run:
    return fulfilledValue;
  }
  catch (e) {
    return 'caught';
  }
}


비동기는 결코 쉬운 개념이 아니다. 시간을 들여 이해해야 하는 영역이고, 필자 역시 비동기에 관한 100% 이해도를 가졌다고 말할 수 없다. 하지만 분명 비동기는 컴퓨팅 자원을 효율적으로 사용할 수 있고, 많은 부분에서 이득을 볼 수 있다. 그러므로 앞으로 동기적인 프로그래밍을 하게 될지라도 비동기적인 프로그래밍과 이에 대한 이해는 분명 좋은 개발자가 되는 데에 인사이트를 줄 것이다.

ref
https://jakearchibald.com/2017/await-vs-return-vs-return-await/

반응형