[비동기] return vs return await
🔥 결론
await 키워드로 인한 성능 이슈 해결 (Chrome 72+)
기존에는 await
키워드로 인해 불필요한 마이크로태스크가 생성되어 성능 문제가 있었습니다.
이로 인해 eslint에도 no-return-await
린트 규칙이 있었습니다. (8.46.0 deprecated)
하지만 V8 엔진에서 Fast Async 최적화를 통해 성능 문제가 완전히 해결하였습니다.
해결된 V8 엔진이 적용된 Chrome 72 이상
부터는 성능 문제가 발생하지 않으며, 오히려 Promise를 생성하는 것보다 async/await을 권장한다고 합니다.
해당 부분은 return await으로 인한 문제가 아니라, await 키워드로 인한 문제로 성능 이슈지만 해결되어 return await이 성능 문제로 사용하면 안되는 문제는 사라졌습니다.
return과 return await의 차이는?
- try/catch 내부에서 에러 처리 가능
return await
을 쓰면 호출한 함수가 에러 스택에 남아, await한 Promise가 reject될 때 그 에러를 try-catch에서 잡을 수 있음
- 스택 트레이스 유지 (디버깅 용이)
await
를 사용하면 비동기 호출 스택 트레이스를 완전하게 보존하여 에러가 발생한 함수를 추적하기 쉬움
⚙️ V8 엔진
V8 엔진 내부적으로 성능 이슈가 발생한 이유에 대해 간략하게 사려보겠습니다.
문제 상황
await 하나만 호출해도 다음과 같은 과정이 엔진에서 발생하고 있었습니다.
- V8은 값을 감싸는 wrapper Promise 생성
- await 키워드 이후로 이어서 실행하기 위해 내부적으로 throwaway Promise를 추가 생성하여 resume 핸들러 부착
- async 함수 실행을 멈췄다가 resolve된 후 결과값 반환
최소 3회의 마이크로태스크 틱을 순차 실행하도록 스케줄하여, 불필요한 Promise 생성과 이벤트 루프 대기 지연 발생하였습니다.
해결
V8에서는 ECMAScript 사양에 promiseResolve
를 도입하여, 이미 프로미스인 값에 대해선 wrapper를 생략하는 로직을 추가하였습니다.
또한, ECMAScript 사양 변경으로 인해 API 제약이 풀려 불필요한 내부 throwaway Promise도 제거하였습니다.
- 불필요한 wrapper Promise 제거
- 2개의 추가적인 마이크로태스크 틱 수 제거 (3 → 1)
- throwaway Promise 제거
🧪 실험 결과
비동기 함수를 return만 했을 때는 중간에 호출된 함수들이 에러 스택 트레이스에 잡히지 않습니다.
반면, return await을 할 경우 모든 함수를 추적할 수 있습니다.
return | return await |
---|---|
import { useState } from "react";
async function PromiseFunc(): Promise<string> {
await new Promise((r) => setTimeout(r, 100));
throw new Error("PromiseFunc 에러 발생!");
}
// A. 단순 return 체인 (await 없이 두 번만 리턴)
async function level1Return(): Promise<string> {
return PromiseFunc();
}
async function level2Return(): Promise<string> {
return level1Return();
}
// B. return await 체인 (두 단계 모두 await)
async function level1ReturnAwait(): Promise<string> {
return await PromiseFunc();
}
async function level2ReturnAwait(): Promise<string> {
return await level1ReturnAwait();
}
export default function App() {
const [stack, setStack] = useState<string>("");
const handleDoubleReturn = async () => {
try {
await level2Return();
} catch (e: any) {
setStack(e.stack);
}
};
const handleDoubleReturnAwait = async () => {
try {
await level2ReturnAwait();
} catch (e: any) {
setStack(e.stack);
}
};
return (
<div style=>
<h2>“이중 리턴” vs “이중 return await” 스택 비교</h2>
<button onClick={handleDoubleReturn} style=>
Test 이중 return
</button>
<button onClick={handleDoubleReturnAwait}>Test 이중 return await</button>
<h3>에러 스택 트레이스</h3>
<pre
style=
>
{stack}
</pre>
</div>
);
}
댓글남기기