Callback hell은 비동기 작업의 결과를 callback 안에서 처리하고, 그 안에서 또 다른 비동기 작업을 호출하는 구조가 반복되어 코드가 깊게 중첩되는 문제입니다.
이렇게 되면 실행 순서를 읽기 어렵고, 에러 처리와 중간 결과 관리가 복잡해집니다. Promise chaining이나 async/await를 사용하면 흐름을 더 평평하게 만들 수 있습니다.
꼬리질문
조금 더 깊게 물어본다면
답변 뒤에 이어질 수 있는 질문들을 하나씩 열어볼 수 있어요.
Callback hell은 에러 처리를 왜 어렵게 하나요?
각 callback마다 실패 경로가 흩어지고 중첩되기 때문에 에러를 한 곳에서 일관되게 처리하기 어렵습니다.
Inversion of control이란?
콜백을 외부 함수에 넘기는 순간, 그 콜백을 언제, 몇 번, 어떤 인자로 호출할지에 대한 제어권이 호출자에서 수신자로 넘어가는 것을 말합니다. 라이브러리가 콜백을 두 번 호출하거나 아예 호출하지 않아도 호출자 쪽에서 막을 방법이 없습니다. Promise는 한 번만 settled되는 것을 명세로 보장해 이 문제를 줄입니다.
Promise는 callback hell을 어떻게 줄이나요?
then 체인으로 비동기 흐름을 이어갈 수 있고, catch로 에러 처리를 모을 수 있어 중첩을 줄입니다.
async/await의 장점은?
비동기 코드를 동기 코드처럼 위에서 아래로 읽을 수 있게 해 가독성이 좋아집니다. try/catch로 에러 처리를 한 곳에 모을 수도 있습니다.
Callback을 Promise로 바꿀 때 주의점은?
비동기 작업을 return하지 않으면 chain이 끊길 수 있고, reject를 catch하지 않으면 unhandled rejection이 발생할 수 있습니다.
Promise가 microtask queue에서 실행되는 이유는?
then 콜백은 현재 실행 컨텍스트가 끝난 직후, 다음 매크로태스크(setTimeout 등)보다 먼저 실행되어야 합니다. 이를 위해 Promise는 microtask queue를 사용합니다. 덕분에 then 체인이 다른 I/O 이벤트 사이에 끊기지 않고 이어집니다.
async/await는 내부적으로 어떻게 동작하나요?
async 함수는 항상 Promise를 반환합니다. await를 만나면 해당 Promise가 settled될 때까지 함수 실행을 일시 중단하고, 결과를 받으면 나머지 코드를 microtask로 재개합니다. 동작 방식은 generator와 Promise를 조합한 것과 유사합니다.
부가 설명
callback 자체가 나쁜 것은 아닙니다. 문제는 의존적인 비동기 작업이 많아질 때 중첩 구조가 커지는 것입니다.
현대 JavaScript에서는 Promise, async/await, 적절한 함수 분리로 이 문제를 줄입니다.