Q24. ES6의 Promise 체이닝 방식과 ES8의 Async/Await 방식의 가장 큰 차이점은 무엇인가요?
1. 에러 핸드링의 통합(Error Handling)
가장 큰 기술적 차이는 "동기 에러와 비동기 에러를 한곳에서 처리 할 수 있는가?" 입니다.
- Promise: .catch()는 Promise 체인 안에서 발생한 에러만 잡습니다. 만약 체인 내부가 아닌 곳에서 에러가 발생하면, .catch가 못잡고 프로세스가 죽을 수도 있습니다.
- Async/Await : try/catch 문법을 사용하므로, 네트워크 에러(비동기)와 데이터 파싱 에러(동기)를 하나의 블록에서 완벽하게 잡아냅니다.
// [Promise] 에러 처리가 분산됨
function loadData() {
try { // 동기 에러용 try
getData()
.then(json => {
const data = JSON.parse(json); // 여기서 에러나면 .catch로 감
return data;
})
.catch(e => { // 비동기 에러 처리
console.log('Promise Error', e);
});
} catch (e) { // 동기 에러 처리 (Promise 밖의 에러)
console.log('Sync Error', e);
}
}
// [Async/Await] 모든 에러를 한 방에 처리 (Good)
async function loadData() {
try {
const json = await getData(); // 비동기 에러 발생 가능
const data = JSON.parse(json); // 동기 에러 발생 가능 (둘 다 여기서 잡힘)
return data;
} catch (e) {
console.log('All Error', e);
}
}
2. 스코프(Scope)와 변수 공유
복잡한 비동기 로직을 짤 때 "변수 전달" 문제에서 큰 차이가 납니다.
- Promise: 체이닝이 길어지면, 첫 번재 .then에서 받은 결과(user)를 세번째 .then에서 쓰기 위해 계속 리턴으로 넘겨주거나 전역변수를 파야 하는 Props Drilling 같은 귀찮음이 발생합니다.
- Async/Await: 함수 내부가 하나의 스코프이므로, 상단에 선언한 변수를 어디서든 자유롭게 접근할 수 있습니다. 이것 때문에 코드가 깔끔해집니다.
// [Promise] user1을 저 밑에서 쓰려면 계속 넘겨줘야 함 (귀찮음)
fetchUser(1)
.then(user1 => {
return fetchUser(2).then(user2 => [user1, user2]); // user1을 안 잃어버리려고 클로저나 배열 사용
})
.then(([user1, user2]) => {
console.log(user1, user2);
});
// [Async/Await] 그냥 변수 선언하면 끝 (깔끔)
const user1 = await fetchUser(1);
const user2 = await fetchUser(2);
console.log(user1, user2); // 자유롭게 사용
3. 디버깅과 콜스택(Debug & Call Stack)
- Promise : .then 내부의 콜백함수는 비동기로 실행되므로, 에러가 났을 때 콜 스택(Call Stack)에 이전 함수들의 정보가 명확히 남지 않을 때가 많습니다. (익명 함수로 처리되기도 함)
- Async/Await: 엔진이 await 지점의 컨텍스트를 저장해두기 때문에, 에러가 터졌을 때 "어떤 함수에서 호출하다가 죽었는지" 스택 트레이스(Stack Trace)가 훨씬 명확하게 나옵니다.
=> 해당 부분에 대해서 테스트를 진행해보면, 둘다 동일하게 에러 추적이 잘되는 것을 볼 수 있습니다.
이는 V8엔진의 Zero-cost async stack traces라는 기능 때문에, Promise 체이닝도 가능한 스택을 복구해줄 수 있도록 엔진이 support 해주기 때문입니다.
하지만, setTimeout과 같이 call stack 외부에서 실행되는 순간 실행컨텍스트가 이벤트 루프의 다음 바퀴로 넘어가면서 스택에 남아있는 정보가 아무것도 없기 때문에 Async/Await을 써도 엔진이 문백을 잃어버리는 것입니다.
"Promise도 스택이 잘 나오는데 굳이 Async/Await를?"
"V8 엔진이 Promise 스택도 잘 복구해 주는 건 맞지만, 복잡한 제어 구문(for문, if문) 안에서의 비동기 처리는 Async/Await가 압도적으로 유리합니다.
Promise로 반복문을 짜면 재귀를 돌거나 복잡한 체이닝을 해야 해서 스택이 꼬이기 쉽지만, Async/Await는 언어 차원에서 문맥(Context)을 블록 단위로 보존해주기 때문에 디버깅 난이도가 훨씬 낮습니다."
[Promise VS Async/Await 스택 차이]
- Promise : 비동기 작업이 끝날 때마다 새로운 콜 스택이 생성됩니다. 이전 주자는 배턴(결과값)만 넘겨주고 퇴근해버리는 방식입니다.
[Time 1] funcA -> funcB 호출
Stack: [funcA, funcB]
[Time 2] Promise 리턴 직후
Stack: [] <-- 텅 빔! (맥락 단절)
[Time 3] .then() 실행 시점
Stack: [Anonymous Callback] <-- 뜬금없이 혼자 실행됨
(V8이 억지로 [funcA -> funcB] 정보를 붙여줌)
- Async/Await : 제너레이터(Generator) 기반이므로, 함수가 리턴하고 사라지는 게 아니라 메모리 힙(Heap) 어딘가에 상태를 저장해두고 '일시 정지'합니다.
[Time 1] funcA -> funcB 호출
Stack: [funcA, funcB]
[Time 2] await 만남
Stack: [] (비워지지만, funcA의 컨텍스트는 메모리에 살아있음)
[Time 3] funcB 완료 후 복귀
Stack: [funcA] <-- 되살아남! (Resume)
└-> [funcB 결과 처리]
사실 Async/Await는 제너레이터(Generator, function*)의 문법적 설탕(Sugar)입니다.
제너레이터가 실행될 때 생성되는 Iterator 객체가, 함수 내부의 모든 로컬 변수와 실행 위치를 객체 형태로 캡슐화해서 들고 있습니다.
실행컨텍스트 객체를 통해 힙에 저장해 두었다가 복구하는 기술입니다.
그러면, Async/Await는 함수가 끝난 뒤에도 변수가 살아있다는 부분에 대해서 클로저하고 비슷한것 같습니다.
그러면 Async/Await도 클로저라고 말할 수 있나요?
결론부터 말씀드리자면, 비슷한 부분이 많기는 하지만 같은 기술은 아닙니다.
클로저는 함수가 리턴되어 완전히 종료 되었는데, 내부의 변수만 힙(Heap)메모리 어딘가에 따로 참조해서 살아남은 것입니다.
=> 함수는 죽었고, 변수만 Heap에 저장되어 있는 상태
Async/Await은 함수가 종료된 게 아니라, 일시 정지 상태로 메모리에 함수 전체(실행 컨텍스트)가 통째로 보존되어 있는 것입니다.
=> 함수는 안죽었고, 그냥 얼음 상태입니다.

4. 결정적 차이 : Non-blocking의 시각화
- Promise: 코드가 .then으로 분리되어 있습니다.
- Async/Await: 코드가 위에서 아래로 흐르니 동기 코드처럼 착각하기 쉽습니다.
- 주의점 : await는 Blocking 처럼 보이지만, 실제로는 Main Thread를 차단하지 않습니다. await를 만나는 순간 함수 실행을 멈추고 제어권을 이벤트 루프에게 넘겨줘서 다른 작업을 할 수 있게 해줍니다.
'CS 질문' 카테고리의 다른 글
| [Deep Dive CS - Q27] 코드리뷰 (0) | 2025.12.22 |
|---|---|
| [Deep Dive CS - Q25,Q26] Promise (0) | 2025.12.18 |
| [Deep Dive CS - Q23] Class (0) | 2025.12.17 |
| [Deep Dive CS - Q22] Prototype Chain (0) | 2025.12.17 |
| Deep Dive CS 질문 리스트 2 (0) | 2025.12.17 |