2025-10-07 13:33

  • 콜백은 다른 함수에 인자로 전달되어 특정 시점에 실행되는 함수이다.

  • 자바스크립트의 비동기 처리를 위해 탄생했으며, 이벤트 리스너나 타이머, 데이터 요청 등에서 핵심적인 역할을 한다.

  • 콜백이 중첩되어 발생하는 ‘콜백 지옥’은 코드의 가독성을 해치며, 이를 해결하기 위해 프로미스(Promise)와 async/await 문법이 등장했다.


자바스크립트 콜백 완벽 핸드북 비동기 프로그래밍의 문을 열다

자바스크립트(JavaScript)를 배우다 보면 반드시 마주치게 되는 개념이 바로 **콜백(Callback)**이다. 많은 입문자가 콜백의 개념과 비동기(Asynchronous) 처리 방식 앞에서 혼란을 겪는다. 하지만 콜백은 현대 웹 개발의 근간을 이루는 자바스크립트의 핵심 원리이므로, 이를 이해하는 것은 더 높은 수준의 개발자로 성장하기 위한 필수 관문이다.

이 핸드북은 콜백이 왜 필요한지, 어떻게 작동하는지, 그리고 실전에서 어떻게 활용되며 어떤 한계를 극복해왔는지에 대한 모든 것을 담고 있다. 단순한 문법 설명을 넘어, 콜백이라는 개념이 자바스크립트 생태계에서 어떤 의미를 가지는지 깊이 있게 탐구해 본다.

1. 콜백의 탄생 배경: 자바스크립트는 왜 기다리지 않을까?

콜백을 이해하기 위해서는 먼저 자바스크립트의 동작 방식, 특히 **‘싱글 스레드(Single Thread)‘**와 **‘이벤트 루프(Event Loop)‘**라는 두 가지 키워드를 알아야 한다.

  • 싱글 스레드: 자바스크립트는 한 번에 하나의 작업만 처리할 수 있는 싱글 스레드 기반 언어다. 이는 마치 요리사가 한 명뿐인 주방과 같다. 요리사는 한 번에 하나의 요리만 만들 수 있다. 만약 10분이 걸리는 스테이크를 굽기 시작하면, 그 10분 동안 다른 어떤 요리도 시작할 수 없다. 자바스크립트가 이런 식으로 동작한다면, 서버에서 데이터를 받아오는 데 5초가 걸릴 경우 그 5초 동안 웹사이트는 완전히 멈춰버릴 것이다. 사용자는 클릭도, 스크롤도 할 수 없는 ‘벽돌’이 된 화면을 마주하게 된다.

  • 비동기 처리의 필요성: 위와 같은 사용자 경험은 최악이다. 이 문제를 해결하기 위해 자바스크립트는 ‘비동기(Asynchronous)’ 방식을 도입했다. 시간이 오래 걸리는 작업(예: 서버에 데이터 요청, 대용량 파일 읽기, 타이머 설정)을 일단 다른 곳(Web API)에 위임하고, 자신은 바로 다음 작업을 처리한다. 그리고 위임했던 작업이 완료되면, 그 결과를 받아서 처리하는 방식이다.

바로 이 지점에서 콜백이 등장한다. “이 작업이 끝나면 나중에(back) 전화해 줘(call)” 라는 개념이다. 즉, 시간이 걸리는 작업이 완료되었을 때 실행할 함수를 미리 약속(등록)해두는 것이다.

비유로 이해하기: 바쁜 카페의 주문 시스템

  1. 손님(개발자): “아이스 아메리카노 한 잔 주세요.” (함수 실행 요청)

  2. 카운터 직원(자바스크립트 엔진): 주문을 받자마자, 커피를 만드는 시간이 걸리는 작업을 바리스타(Web API)에게 넘긴다. 그리고 손님에게 진동벨(콜백 함수)을 준다.

  3. 카운터 직원: 진동벨을 준 즉시, 다음 손님의 주문을 받는다. (멈추지 않고 다음 코드 실행)

  4. 바리스타: 커피가 완성되면, 진동벨을 울린다. (작업 완료 및 콜백 호출)

  5. 손님: 진동벨이 울리면, 커피를 받아온다. (콜백 함수 실행)

여기서 진동벨이 바로 콜백이다. 커피가 언제 완성될지 기다리며 카운터 앞을 막고 서 있는 대신(동기 방식), 진동벨을 받고 다른 일을 하다가(비동기 방식) 커피가 준비됐다는 신호가 오면 반응하는 효율적인 방식이다.

2. 콜백의 구조와 작동 원리: 함수를 선물처럼 전달하다

자바스크립트에서 함수는 **일급 객체(First-class citizen)**로 취급된다. 이는 함수가 변수에 할당될 수 있고, 다른 함수의 인자로 전달될 수 있으며, 함수의 반환 값으로도 사용될 수 있음을 의미한다. 콜백은 바로 이 ‘함수를 인자로 전달하는’ 특징을 활용한 것이다.

기본 구조

가장 기본적인 콜백의 구조는 다음과 같다.

JavaScript

function mainFunction(callback) {
  console.log("메인 함수 작업을 수행합니다...");
  // 어떤 작업이 끝난 후
  console.log("작업이 거의 끝났습니다. 콜백 함수를 호출합니다.");
  callback(); // 인자로 받은 함수를 실행
}

function myCallback() {
  console.log("안녕하세요! 콜백 함수입니다!");
}

mainFunction(myCallback);

실행 결과:

메인 함수 작업을 수행합니다...
작업이 거의 끝났습니다. 콜백 함수를 호출합니다.
안녕하세요! 콜백 함수입니다!

mainFunctioncallback이라는 이름의 매개변수를 받는다. 그리고 함수 내부 로직을 수행한 뒤, 마지막에 이 callback을 호출한다. 우리가 mainFunction을 실행할 때 myCallback이라는 함수 자체를 인자로 넘겨주었기 때문에, mainFunction 내부에서는 myCallback()이 실행되는 것이다.

이때 함수를 인자로 넘길 때 myCallback()처럼 괄호를 붙여 실행 결과를 넘기는 것이 아니라, myCallback이라는 함수 객체 자체를 넘겨야 한다는 점에 유의해야 한다.

3. 콜백의 실용적인 사용법: 언제 어디서 쓰이는가

콜백은 동기적 상황과 비동기적 상황 모두에서 유용하게 사용된다.

가. 동기적 콜백 (Synchronous Callback)

코드가 순차적으로 실행되는 와중에 사용되는 콜백이다. 대표적인 예는 자바스크립트 배열의 고차 함수(Higher-Order Function)들이다.

  • forEach(): 배열의 각 요소를 순회하며 콜백 함수를 실행한다.

  • map(): 배열의 각 요소에 콜백 함수를 적용하여 새로운 배열을 반환한다.

  • filter(): 콜백 함수가 true를 반환하는 요소만 모아 새로운 배열을 반환한다.

  • reduce(): 배열의 모든 요소를 순회하며 콜백 함수의 반환 값을 누적하여 최종적으로 하나의 값을 반환한다.

JavaScript

const numbers = [1, 2, 3, 4, 5];

// map 메소드의 인자로 전달된 화살표 함수가 바로 동기적 콜백이다.
const squaredNumbers = numbers.map((num) => {
  return num * num;
});

console.log(squaredNumbers); // [1, 4, 9, 16, 25]

위 예제에서 map 함수는 배열의 각 요소 num을 인자로 받아 우리가 정의한 콜백 함수 (num) => num * num을 즉시 실행한다. 모든 작업이 순차적으로, 막힘없이 진행되므로 동기적 콜백이라 부른다.

나. 비동기적 콜백 (Asynchronous Callback)

자바스크립트가 콜백을 사용하는 주된 이유이며, 앞서 설명한 ‘기다리지 않는’ 동작 방식의 핵심이다.

  • setTimeout(callback, delay): 지정된 delay(밀리초) 이후에 callback 함수를 한 번 실행한다.

    JavaScript

    console.log("작업 시작");
    
    setTimeout(() => {
      console.log("3초 후에 이 메시지가 보입니다.");
    }, 3000);
    
    console.log("setTimeout 다음에 있는 코드");
    

    실행 결과:

    작업 시작
    setTimeout 다음에 있는 코드
    3초 후에 이 메시지가 보입니다.
    

    setTimeout은 콜백 함수를 즉시 실행하지 않고, 타이머 설정만 한 뒤 바로 다음 코드인 console.log("setTimeout...")를 실행한다. 3초가 지난 후에야 비로소 등록해두었던 콜백 함수가 실행된다.

  • 이벤트 리스너(Event Listener): 사용자의 행동(클릭, 키보드 입력, 마우스 이동 등)이 발생했을 때 실행될 콜백 함수를 등록한다.

    JavaScript

    const myButton = document.getElementById('myBtn');
    
    // 'click' 이벤트가 발생하면 전달된 콜백 함수가 실행된다.
    myButton.addEventListener('click', () => {
      alert('버튼이 클릭되었습니다!');
    });
    

    사용자가 언제 버튼을 클릭할지 알 수 없으므로, “클릭 이벤트가 발생하면 이 함수를 실행해 줘”라고 약속만 해두는 전형적인 비동기 처리 방식이다.

  • 서버 통신 (Ajax/Fetch): 서버에 데이터를 요청하고 응답을 받았을 때 처리할 로직을 콜백 함수로 전달한다. (최근에는 주로 Promise나 async/await를 사용하지만, 기본적인 원리는 콜백에 기반한다.)

    JavaScript

    // jQuery의 Ajax 예시
    $.get('https://api.example.com/data', function(response) {
      // 서버로부터 응답(response)을 성공적으로 받으면 이 콜백이 실행된다.
      console.log(response);
    });
    

4. 심화 탐구: 콜백 지옥과 해결사들

콜백은 비동기 처리를 가능하게 하는 강력한 패턴이지만, 여러 개의 비동기 작업을 순차적으로 처리해야 할 때 심각한 문제를 드러낸다.

가. 콜백 지옥 (Callback Hell)

예를 들어, 1번 작업을 통해 얻은 결과로 2번 작업을 하고, 2번 작업의 결과로 3번 작업을 해야 하는 상황을 가정해 보자.

JavaScript

step1(function (result1) {
  step2(result1, function (result2) {
    step3(result2, function (result3) {
      step4(result3, function (result4) {
        // ... 계속 중첩된다.
        console.log("모든 작업이 완료되었습니다.");
      });
    });
  });
});

콜백 함수 안에 또 다른 콜백 함수가 꼬리를 물고 계속 중첩되는 이 코드는 마치 피라미드처럼 생겼다고 해서 **‘파멸의 피라미드(Pyramid of Doom)‘**라고도 불린다. 콜백 지옥은 다음과 같은 심각한 문제를 야기한다.

  • 가독성 저하: 코드의 흐름을 파악하기 매우 어렵다.

  • 유지보수의 어려움: 중간에 로직을 하나 추가하거나 수정하려면 전체 구조를 건드려야 할 수 있다.

  • 에러 처리의 복잡성: 각 콜백 단계마다 에러 처리를 따로 해주어야 하며, 에러가 발생했을 때 디버깅이 매우 힘들다.

나. 해결사 1: 프로미스 (Promise)

이러한 콜백 지옥을 해결하기 위해 ES6(2015)에서 도입된 객체가 바로 **프로미스(Promise)**다. 프로미스는 비동기 작업의 ‘미래’ 상태(성공 또는 실패)와 그 결과 값을 나타내는 대리자(proxy) 객체다.

콜백 지옥 코드를 프로미스로 개선하면 다음과 같이 바뀐다.

JavaScript

step1()
  .then(result1 => step2(result1))
  .then(result2 => step3(result2))
  .then(result3 => step4(result3))
  .then(result4 => {
    console.log("모든 작업이 완료되었습니다.");
  })
  .catch(error => {
    console.error("작업 중 에러 발생:", error);
  });

들여쓰기가 깊어지는 대신 .then() 체인을 통해 코드가 위에서 아래로 자연스럽게 흐른다. 또한, .catch()를 사용해 모든 단계에서 발생하는 에러를 한 곳에서 처리할 수 있어 훨씬 깔끔하고 직관적이다.

다. 해결사 2: Async/Await

ES2017(ES8)에서는 프로미스를 더욱 쉽게 사용할 수 있도록 하는 async/await 문법이 도입되었다. 이는 비동기 코드를 마치 동기 코드처럼 보이게 만드는 강력한 ‘문법적 설탕(Syntactic Sugar)‘이다.

async 키워드는 함수가 항상 프로미스를 반환하도록 하고, await 키워드는 프로미스가 처리될 때까지(성공하거나 실패할 때까지) 함수 실행을 일시 중지시킨다.

JavaScript

async function runAllSteps() {
  try {
    const result1 = await step1();
    const result2 = await step2(result1);
    const result3 = await step3(result2);
    const result4 = await step4(result3);
    console.log("모든 작업이 완료되었습니다.");
  } catch (error) {
    console.error("작업 중 에러 발생:", error);
  }
}

runAllSteps();

콜백 지옥이나 프로미스 체이닝에 비해 훨씬 간결하고, 우리가 평소에 코드를 읽는 방식과 유사하여 가독성이 극적으로 향상된다. 에러 처리도 동기 코드에서 사용하는 try...catch 구문을 그대로 사용할 수 있다.

구분콜백 (Callback)프로미스 (Promise)Async/Await
핵심 개념함수를 인자로 전달하여 실행비동기 상태와 결과를 나타내는 객체프로미스를 동기 코드처럼 보이게 하는 문법
코드 형태중첩된 함수 구조 (피라미드).then() 체이닝try...catch를 사용한 동기적 코드 형태
가독성낮음보통높음
에러 처리각 콜백마다 개별 처리 필요.catch()로 중앙 집중 가능try...catch 구문으로 직관적 처리
등장 시기JavaScript 초기부터ES6 (2015)ES8 (2017)

5. 결론: 콜백은 여전히 자바스크립트의 심장이다

프로미스와 async/await의 등장으로 콜백 지옥은 과거의 유물이 되어가고 있다. 하지만 이것이 콜백의 중요성이 사라졌음을 의미하지는 않는다.

  • 프로미스의 .then(callback) 이나 .catch(callback)에 전달되는 함수도 결국 콜백이다.

  • async/await 문법은 프로미스를 기반으로 동작하고, 프로미스는 콜백의 개념을 바탕으로 만들어졌다.

  • 배열의 고차 함수나 이벤트 리스너 등 수많은 자바스크립트 내장 기능들은 여전히 콜백 패턴을 적극적으로 사용하고 있다.

콜백은 자바스크립트의 비동기 프로그래밍을 떠받치는 가장 근본적인 개념이다. 콜백의 작동 원리를 깊이 이해하는 것은 프로미스와 async/await를 더욱 효과적으로 사용하고, 복잡한 비동기 로직을 능숙하게 다루는 개발자로 성장하는 데 필수적인 자양분이 될 것이다. 콜백 지옥을 피하는 방법을 배우는 것만큼, 콜백이 왜 그렇게 작동하는지를 이해하는 것이 중요하다.