All Articles

Disallows unnecessary return await (no-return-await) Eslint 오류

최근에 업무를 하다가 아래와 같이 두 줄로 끝나는 함수가 있어서 한 줄로 그냥 리턴하면 어떨까 하고 생각해보게 됬는데요.

  async fetchStoreDetailBrowser (_, { storeId }) {
    const storeDetail = await this.$axios.$get(endpoints.storeDetailBrowser(storeId))
    return storeDetail
  },

  // 아래와 같이 바꾸면 어떨까?
  async fetchStoreDetailBrowser (_, { storeId }) {
    return await this.$axios.$get(endpoints.storeDetailBrowser(storeId))
  },

아래에 더 추가적인 로직이 없다면 깔끔하게 끝나는 것 같이 보여 좋아보이긴 합니다. 하지만 eslint에서 이런 오류를 발견할 수 있습니다. eslint org에서 더 자세한 내용을 확인 할 수 있었습니다.

문서를 읽어보면 이렇게 쓰여 있습니다.

async function 내부에서 return await를 사용하는 것은 대기 중인 Promise가 해결될 때까지 호출 스택에 현재 함수가 유지됩니다.

역시나 항상 이해하기 어려운데요. 더 짧고 와닿게 얘기하면 Call stack 내부에 함수를 붙잡아 두어 CPU 낭비가 발생하기 때문입니다.

다만 try catch 블록 내에서 사용하면 eslint 룰에 걸리지는 않습니다.

이와 관련되서 더 심화된 내용을 알아보기 위해 eslint 문서에서 링크로 달아놓은 관련 예제를 살펴보도록 하겠습니다.

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!");
}

이런 함수가 있다고 합니다. 이 함수는 settimeout Promise와 함께 동전던지기를 해서 50:50의 확률로 yay를 리턴하거나 에러로 Boo를 던지는 함수입니다.

키워드 없이 호출할 경우

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

만약 여기서 이렇게 그냥 부르면 어떻게 될까요?

정답: waiting 없이 undefined만 반환함.

Promise 체이닝, async await에 익숙하신 분들이라면 바로 풀으셨겠지만 위 코드는 매우 잘못 작성한 코드라고 할 수 있습니다.

비동기 처리를 전혀 하지 않은 셈이니까 waiting하는 프로세스를 거치지 않죠.

Await 키워드 붙였을 때

위 문제는 Async에 익숙하신 분들이라면 어렵지 않았으리라고 생각합니다.

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

그렇다면 await 키워드를 붙이게 된다면 어떻게 될까요?

정답: 1초를 기다리고, undefind 혹은 “caught”를 반환

Boo는 반환하지 않습니다. 왜냐면 waitAndMaybeReject의 결과값을 받기 때문인데요. 오류가 생겼다면 try catch 블록에서 잡기 때문에 ‘caught’를 반환하게 되는 것입니다.

리턴 시켰을 때

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

바로 리턴을 시키면 어떻게 될까요?

정답: 1초를 기다리고, “yay” 혹은 Error(“boo”)를 반환

예상했다시피 바로 return을 했기 때문에 try catch에서 걸리지 않습니다.

return await를 시켰을 때

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

return과 동시에 await를 시키게 되면? 정답: 1초를 기다리고, “yay” 혹은 “caught”를 반환

waitAndMaybeReject의 await를 하기 때문에 여기서의 오류는 catch 블록으로 넘어가게 됩니다.

이 코드는 아래와 같다고 생각하면 됩니다.

async function foo() {
  try {
    // waitAndMaybeReject의 결과가 나올때까지 기다린다.
    // 그리고 해당한 result를 fulfilledValue에 저장한다.
    const fulfilledValue = await waitAndMaybeReject();
    // 만약 reject된 값이라면 catch 블록으로 넘어가고 
    // 아니라면 결과를 반환하게 된다. 
    return fulfilledValue;
  }
  catch (e) {
    return 'caught';
  }
}

결론

return을 단순히 끝내는 용도로만 쓴다면야 문제가 없겠지만 또다른 Promise를 불필요하게 넘겨준다면 call stack에서 대기를 해야하므로 이에 대한 microtask이 더 생기게 되는 셈이므로 필요 없다는 것입니다.

  1. 끝내는 용도로 return 을 쓸 경우 Promise를 리턴하지 않음
  2. 그러나 짧게 쓰겠다고 return await를 할 경우 Promise까지 같이 리턴됨.
  3. 이에 따라 싱글스레드 call stack을 돌고 있는 javascript 런타임에서 Promise를 처리하느라 그만큼 로스가 생김

하지만 엄밀히 놓고 보면 return await를 바로 부르는 것은 우려할만큼 높은 cpu 로스를 일으키진 않으리라고 생각합니다.

그러나 가장 큰 문제는 역시 디버깅의 문제로 호출하는 곳에서 오류가 생길 경우 해당 try catch 블록에서 잡아낼 수 있는 것을 return 시켜버리므로 안 그래도 짜증나는 빨간 오류 화면에서 더 찾기 어려워지는 경우가 발생할 수 있습니다.

결론 : 우리 모두 더 이상 불필요하게 return await를 피하도록 합시다.