고양의 성장일기

[Javascript] 자바스크립트 비동기 3부작 <Callback> 본문

🖥️ Front-End/Javascript

[Javascript] 자바스크립트 비동기 3부작 <Callback>

고 양 2025. 12. 17. 20:58
반응형
자바스크립트 비동기 3부작 <Callback>

지난 글에서 비동기를 처리하는 방법과 관련해 Promiseasync / await 방식에 대해 살펴보았습니다.

이번 글에서는 그보다 더 근본적인 비동기 처리 패턴인 Callback에 대해 정리해보려 합니다.

Callback이란 무엇인가

콜백이란 어떤 작업이 끝난 뒤 실행할 함수를 다른 함수의 인자로 전달하는 방식을 의미합니다.

 

자바스크립트에서 함수는 1급 객체로서

  • 변수에 할당할 수 있고
  • 다른 함수에 인자로 전달할 수 있으며
  • 객체로 참조할 수 있습니다.

이 특성에 의해 자바스크립트는 비동기 작업의 완료 시점을 직접 기다리고 관측하는 대신

"작업이 끝났을 때 무엇을 할지"를 함수로 전달하는 구조를 사용하곤 했었습니다.

 

그 덕분에 비동기 작업의 완료시점을 보장받을 수 없는 자바스크립트에서 안정적인 실행흐름을 제어할 수 있었습니다.

function fetchData(callback) {
    setTimeout(() => {
        callback('data');
    }, 1000);
}

fetchData(result => {
    console.log(result);
});

Callback 기반 비동기 처리는 어떻게 동작하는가

콜백 패턴의 핵심 흐름은 아래와 같습니다.

  1. 비동기 함수 호출
  2. 함수는 즉시 반환되고 호출 스택은 후속 작업 진행
  3. 비동기 작업이 완료되면

간단히 도식화하면 아래와 같은 패턴이며, 콜백은 비동기 작업 완료 시점에 실행됩니다.

여기서는 console.log('async'); 이 한 줄이 포함된 익명 함수 부분이 콜백이 되는 셈이죠.

console.log('start');

setTimeout(() => {
    console.log('async');
}, 1000);

console.log('end');

에러 핸들링하기

콜백 패턴에서의 예외나 에러는 비동기 작업 결과로 직접 받아내어 명시적으로 전달하는 방식이 일반적입니다.

function fetchData(callback) {
    setTimeout(() => {
        const error = null;
        const data = { value: 10 };

        callback(error, data);
    }, 1000);
}

fetchData((error, result) => {
    if (error) {
        console.error(error);
        return;
    }
    console.log(result);
});

지옥 창조하기

콜백 패턴에서의 가장 큰 문제는 비동기 작업이 중첩될수록 코드가 급격히 복잡해진다는 점입니다.

마치 한 층 한 층 내려갈수록 더욱 끔찍한 풍경이 펼쳐지는 지옥을 연상시키죠

login(user, () => {
    getProfile(user, profile => {
        getPosts(profile.id, posts => {
            saveLog(posts, () => {
                console.log('지옥에 온 걸 환영한다.');
            });
        });
    });
});

내가 바로 이 코드를 짠 장본인이오.

예외처리까지 더하면 코드는 아래와 같은 형태가 됩니다.

step1(result => {
    if (error) return handleError();
    step2(result, next => {
        if (error) return handleError();
        step3(next, done => {
            if (error) return handleError();
        });
    });
});

예외를 종류별로 처리해야 한다면 if 하나로 안 끝나겠죠 (연쇄 if-else마의 출현)

이 복잡한 비동기의 흐름을 직관적으로 제어하기 위해 등장한 것이 이전 글에서 다뤘던 Promiseasync / await입니다.

Callback 패턴의 이쁜 점

단점만 있는 것은 아닙니다.

 

원시적인 형태이니만큼 개념이 매우 단순하고 소규모 비동기 작업에는 여전히 유효합니다.

트리거가 있어야 코드가 실행되는 이벤트 핸들링에도 여전히 활용되고 있죠.

button.addEventListener('click', () => {
    console.log('clicked');
});

Callback 패턴의 한계와 진화 과정 되짚어보기

비동기 3부작의 마지막 시리즈이자, Promise와 async / await의 프리퀄인 Callback의 진화과정을

인류 역사와 비교해 보면 마치 이 그림과 같습니다.

  • Callback : 단순하지만 중첩과 에러 핸들링 문제, 원시인 다리털처럼 복잡해지기 쉬운 코드
  • Promise : 체이닝, 중앙 집중식 에러 처리( catch() )
  • async / await : 동기 코드와 유사한 가독성, try-catch 블록을 활용한 에러 처리

그럼에도 Callback 이해해야 하는 이유

실무에서는 Promise패턴이나 async / await 패턴이 주력이지만, 콜백을 이해하면 좋은 점은 분명히 있습니다

  • 레거시 코드 유지보수 (이게 진짜...)
  • 이벤트 기반 프로그래밍의 이해
  • Promise 내부 동작 원리 이해
  • 비동기 흐름의 본질 파악

때로 지옥이라 멸시받는 콜백은 사실 자바스크립트 비동기 처리의 알파이자 오메가입니다.

마치며

"자바스크립트는 싱글 스레드일까요? 멀티 스레드일까요?"

지금은 제가 속한 파트의 파트장으로 있는 모 대리님이 파트원일 때 다른 파트원들에게 던졌던 질문입니다.

이어진 짧은 강연이 너무 명료하고 감명 깊어서 언젠가 비동기에 관해 공부하고 포스팅해야겠다고 결심했죠.

 

사실 비동기와 콜백은 우리 실생활에도 이미 깊게 녹아 있습니다.

 

  • 세탁기를 돌리며 블로그에 포스팅을 하는 것 (비동기 흐름)
  • 세탁이 완료되면
    1. 내가 세탁물을 정리할지
    2. 동생에게 부탁할지 정하는 것 (콜백 설정)
  • 세탁이 잘 안됐을 때 뒤처리 방법 (에러 핸들링)

처음엔 단순했던 자바스크립트 애플리케이션들의 구조가 방대해지고 복잡해짐에 따라

다양한 비동기 관련 패턴이 발맞춰 발전했을 것입니다.

 

저 또한 이번 포스팅을 통해 단순히 비동기 흐름을 이해하는 것을 넘어 현실세계의 패턴들이 어떻게 프로그램으로 구현되는지,

그리고 스트럭쳐와 시스템이 왜 필요한지 다시 한 번 생각하게 된 좋은 계기였던 것 같습니다.

 

다음 포스팅은 번외편, 자바스크립트는 싱글 스레드 주제에 어떻게 비동기를 해내는지에 대해서 찾아보겠습니다.

 

감사합니다!

반응형