최근에 업무를 하다가 아래와 같이 두 줄로 끝나는 함수가 있어서 한 줄로 그냥 리턴하면 어떨까 하고 생각해보게 됬는데요.
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이 더 생기게 되는 셈이므로 필요 없다는 것입니다.
- 끝내는 용도로 return 을 쓸 경우 Promise를 리턴하지 않음
- 그러나 짧게 쓰겠다고 return await를 할 경우 Promise까지 같이 리턴됨.
- 이에 따라 싱글스레드 call stack을 돌고 있는 javascript 런타임에서 Promise를 처리하느라 그만큼 로스가 생김
하지만 엄밀히 놓고 보면 return await
를 바로 부르는 것은 우려할만큼 높은 cpu 로스를 일으키진 않으리라고 생각합니다.
그러나 가장 큰 문제는 역시 디버깅의 문제로 호출하는 곳에서 오류가 생길 경우 해당 try catch 블록에서 잡아낼 수 있는 것을 return 시켜버리므로 안 그래도 짜증나는 빨간 오류 화면에서 더 찾기 어려워지는 경우가 발생할 수 있습니다.
결론 : 우리 모두 더 이상 불필요하게
return await
를 피하도록 합시다.