회사에서 동료의 코드를 리뷰 진행하다가 DB 조회하는 비동기 함수를 await 없이 return 하는 부분이 있어, 동료와 해당 부분에 대해 이야기를 했습니다.
기존에 return await와 no return await 둘 다 사용이 가능하지만, 각각의 장점에 대해 정확히 알지 못했다고 생각이 들어 따로 파악한 내용을 정리한 글 입니다.
return await와 no return await의 예시입니다.
// 비동기 함수
const simulateAsyncOperation = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve("결과값");
}, 1000);
});
};
// return await 사용
const withReturnAwait = async () => {
return await simulateAsyncOperation();
};
// no return await 사용
// 비동기 함수이지만, await 없이 return 하여도 정상적으로 호출됨
const withReturnOnly = async () => {
return simulateAsyncOperation();
};
학습한 자료에 의하면 각각의 장점은 아래와 같습니다.
return await
를 사용하는 경우, 함수가 Promise가 해결되기를 기다린 후 결과값을 반환 합니다. 그 에 반해 return만 사용하는 no return await
같은 경우 즉시 Promise를 반환하고, 호출자가 결과를 기다립니다.return await
를 생략하면 코드가 더 간결해지고 읽기 쉬워집니다.return await
를 사용하지 않으면 오류 발생 시 stack trace가 더 간단해집니다. 위 장점 중 간결성은 공감을 하지만, 명확성 같은 경우는 조직, 서비스, 구성원 별로 달라질 수 있을 것 같습니다.
그 외 공감은 되지만, 정확한 결과를 봤으면 하는 부분이 있어 테스트를 진행해봤습니다.
위 예시 코드를 통해 테스트를 진행한 결과 값입니다.
테스트를 진행한 코드는 아래와 같습니다.
const simulateAsyncOperation = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve("결과값");
}, 1000);
});
};
// return await 사용
const withReturnAwait = async () => {
return await simulateAsyncOperation();
};
// return만 사용
const withReturnOnly = async () => {
return simulateAsyncOperation();
};
const measurePerformance = async () => {
const startWithReturnAwait = performance.now();
await withReturnAwait();
const endWithReturnAwait = performance.now();
console.log(`Return await: ${endWithReturnAwait - startWithReturnAwait} ms`);
const startWithReturnOnly = performance.now();
await withReturnOnly();
const endWithReturnOnly = performance.now();
console.log(`Return only: ${endWithReturnOnly - startWithReturnOnly} ms`);
};
measurePerformance();
테스트 결과 0.4ms 정도 성능 최적화가 된 것을 확인 할 수 있습니다.
에러 추척이 어려운 이유를 위 예제 코드를 좀 변경해서 테스트 해보겠습니다.
우선 1초 기다리던 함수를 에러를 발생하게 변경하겠습니다.
const simulateAsyncError = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error("시뮬레이션된 에러"));
}, 1000);
});
};
no return await
가 catch문에 안잡히는 지 확인하기 위해 try catch를 추가하겠습니다.
// return await 사용
const withReturnAwait = async () => {
try {
return await simulateAsyncError();
} catch (error) {
console.error("withReturnAwait 에러:", error);
throw error;
}
};
// return만 사용
const withReturnOnly = async () => {
try {
return simulateAsyncError();
} catch (error) {
console.error("withReturnOnly 에러:", error);
throw error;
}
};
위 함수를 각각 호출하겠습니다.
const testStackTrace = async () => {
try {
await withReturnAwait();
} catch (error) {
console.error("withReturnAwait 호출 시 에러:", error);
}
try {
await withReturnOnly();
} catch (error) {
console.error("withReturnOnly 호출 시 에러:", error);
}
};
testStackTrace();
결과 값은 아래 이미지와 같이 withReturnOnly함수 같은 경우 withReturnAwait와 다르게 해당 함수에서 catch에 걸리지 않는 것을 확인 할 수 있습니다.
저는 성능이 엄청나게 중요한 프로젝트가 아니면, return await
를 사용할 것 같습니다.
그렇게 생각한 이유는 성능 최적화 부분이 크지 않다고 생각하며, 또한 에러 추적 부분이 더 중요하다고 생각합니다.
또한, return await
같은 경우 순차적 비동기 처리를 위해 필수적으로 필요한 상황이 발생합니다. 그렇기에, 프로젝트에 return await
와 no return await
가 같이 있기 때문에 휴먼 에러가 발생할 가능성이 있다고 생각합니다.
withReturnOnly 쪽을 예시로 비교하는것 자체가 이상하네요
"에러 추적" 쪽과 같은 예시처럼 무분별한 트라이캐치는 설계 오류, 지뢰 심기라 잘못된 예시로 보입니다.
return이 없다고 생각하면 await을 뺀 비동기함수의 에러캐치가 안되는건 당연한 일이고 아래에서 발생한 오류를 상위에서 잡아서 한번만 처리 하는게 좋은데 아래에서 안잡힌다고 안쓴다는건 조금 이상합니다
굳이 await을 써야하고 리턴해야된다 싶으면 아래처럼 사용하는게 낫지요
return await f() 나 return f() 이나 해당 함수를 호출하는 곳에서는 어차피 Promise객체로 떨어지기때문에 의미가 없습니다.
불필요한 행사코드를 적는 일에 불과하고 위에 말씀하신대로 성능까지 떨어지면 굳이 쓸 필요가 있을까요?
(원문 보시는게 좋을 것 같습니다. https://perade.github.io/blog/await-vs-return-vs-return-await/)