[javascript] 비동기 처리의 한계를 살펴보자 (콜백 헬, Callback Hell)

Chan의 기술 블로그·2025년 10월 11일

javascript

목록 보기
4/8

이 글은 『자바스크립트 딥 다이브』를 공부하며 정리한 내용입니다.

자바스크립트는 비동기 처리를 위한 하나의 패턴으로 콜백 함수(callback function) 를 사용한다.
하지만 전통적인 콜백 패턴은 다음과 같은 한계가 있다.

  • 콜백 헬(callback hell)로 인해 가독성이 나쁘다.
  • 비동기 처리 중 발생한 에러 처리가 어렵다.
  • 여러 비동기 작업을 순차 또는 병렬로 제어하기가 복잡하다.

이러한 문제를 해결하기 위해 ES6에서는 프로미스(Promise) 가 도입되었다.


비동기 함수와 실행 컨텍스트

앞서 자바스크립트 실행 컨텍스트와 이벤트 루프 정리 에서 살펴봤듯,
비동기 함수는 태스크 큐(Task Queue) 에 콜백이 대기하고 있다가
콜 스택(Call Stack) 이 비었을 때 이벤트 루프(Event Loop) 에 의해 실행된다.

즉, 비동기 함수는 호출 시점에 바로 실행되지 않으며,
그 결과를 외부로 반환하거나 상위 스코프 변수에 즉시 할당할 수 없다.


비동기 반환의 한계 예시

const get = url => {
	const xhr = new XMLHttpRequest();
  	xhr.open('GET', url);
  	xhr.send();
  	xhr.onload = () => {
  		if (xhr.status === 200) {
    		return JSON.parse(xhr.response);
  		} else {
    		console.error('요청 실패', xhr.status);
  		}
	};
}
const res = get("/posts/1");
console.log(res); // undefined

get 함수가 호출되면 XMLHttpRequest 객체가 생성되고,
HTTP 요청이 전송된 뒤 xhr.onload 이벤트 핸들러가 바인딩된다.
하지만 get 함수 자체에는 명시적인 반환문이 없기 때문에 undefined 를 반환한다.

이때 xhr.onload 내부의 return 문은 get 함수의 반환값이 아니다.
get은 이미 종료되어 스택에서 제거되었기 때문이다.


상위 스코프 변수에 할당 시도

let value;

const get = url => {
	const xhr = new XMLHttpRequest();
  	xhr.open('GET', url);
  	xhr.send();
  	xhr.onload = () => {
  		if (xhr.status === 200) {
    		value = JSON.parse(xhr.response);
  		} else {
    		console.error('요청 실패', xhr.status);
  		}
	};
}
get("/posts/1");
console.log(value); // undefined

이 경우에도 기대한 대로 동작하지 않는다.
xhr.onload는 비동기적으로 실행되므로,
console.log(value)가 실행될 때는 서버 응답이 아직 도착하지 않은 상태다.


실행 컨텍스트 관점에서 단계별 정리

  1. get 함수가 호출되면 실행 컨텍스트가 생성되어 콜 스택에 푸시된다.
  2. xhr.onload에 콜백이 등록되고 get 함수는 종료되어 콜 스택에서 제거된다.
  3. console.log(value)가 실행되어 콜 스택에 푸시된다.
  4. 서버 응답이 도착하면 load 이벤트가 발생하고,
    xhr.onload에 바인딩된 콜백이 태스크 큐에 저장되어 대기한다.
  5. 콜 스택이 비면 이벤트 루프가 태스크 큐의 콜백을 콜 스택으로 이동시켜 실행한다.

즉, xhr.onload는 항상 console.log(value) 실행이 끝난 이후에 동작한다.


콜백 헬의 시작

비동기 함수는 결과를 즉시 반환할 수 없기 때문에,
결과 처리를 콜백 내부에서 수행해야 한다.

그러나 비동기 처리가 중첩되면 다음과 같이 코드의 복잡도가 급격히 증가한다.

getUser(userId, user => {
  getPosts(user.id, posts => {
    getComments(posts[0].id, comments => {
      console.log(comments);
    });
  });
});

이런 구조를 콜백 헬(Callback Hell) 이라고 한다.


✨ 요약

  • 비동기 함수의 반환값은 즉시 사용할 수 없다.
  • 실행 컨텍스트와 이벤트 루프 구조상, 콜백은 항상 나중에 실행된다.
  • 콜백 헬은 이러한 비동기 흐름 제어의 복잡성에서 비롯된다.
  • Promise는 이 문제를 해결하기 위한 ES6의 새로운 비동기 패턴이다.

다음 게시물에서 Promise와 fetch API에 대해서 알아보자.

profile
퍼블리셔에서 프론트앤드로 전향하기

0개의 댓글