싱글 스레드 기반의 Node.js 환경에서 동적 UI, 네트워크 통신, 파일 I/O 등과 '동시성(Concurrency)' 의 문제로 비동기 처리 전략이 항상 문제다.
과거 법무법인에서 ERP를 개발하던 시절에 Ext.js를 사용하며 처음으로 진짜 '콜백 지옥'을 경험했다. 시간이 꽤 흐른 뒤, 우연히 마주한 레거시 코드에서 이 지옥을 다시 만났을 때 주님을 찾는건 어쩔 수 없었다. JavaScript 의 버전 업데이트가 진행되면서 불편한게 많이 개선되었지만 개선되기 전에 작성된 코드에는 Node.js 6 이라는 버전을 사용하는 프로젝트에서 콜백 지옥이 나를 기다리고 있었다.
콜백 지옥
지금은 async/await 이 당연한 문법처럼 느껴지지만, Node.js 가 처음 등장했을 때 비동기 처리는 혁명적이었으나, 비동기 작업이 완료된 시점에 실행할 코드를 인자(Argument)로 넘겨주는 '콜백(Callback)' 은 답이 없었다.
간단한 로직이면 문제가 없지만 코드를 작성하다 보면 가독성 등의 문제로 호락호락하지 않기 떄문이다.
간단한 로지인 Legacy Code
const fs = require('fs');
fs.readFile('config.json', 'utf8', function(err, configData) {
if (err) {
console.error('설정 파일 로드 실패', err);
return;
}
const config = JSON.parse(configData);
db.connect(config.dbUrl, function(err, connection) {
if (err) {
console.error('DB 연결 실패', err);
return;
}
connection.query('SELECT * FROM users', function(err, rows) {
if (err) {
console.error('쿼리 실행 실패', err);
connection.end();
return;
}
fs.writeFile('users.log', JSON.stringify(rows), function(err) {
if (err) {
console.error('로그 저장 실패', err);
} else {
console.log('작업 완료!');
}
connection.end();
});
});
});
});
이러한 형태의 코드를 Pyramid of Doom 라고 부른다고 한다.
이 패턴의 치명적인 문제점
- 가독성 문제: 로직의 흐름이 아래가 아닌 오른쪽으로 간다. 닫히는 괄호의 짝을 찾는데 너무 많은 시간을 보낸다.
- 에러 처리: 모든 콜백 단계마다 if (err) 를 반복해야한다. 예외를 누락하면 원인을 찾는데 너무 긴 시간이 걸린다.
- 제어권의 상실: 비동기 작업의 순서를 제어하거나 병렬로 처리하기 위해 매우 복잡한 상태 관리를 직접 구현해야한다.
- 유지보수의 어려움: 로직 변경이나 순서 수정이 발생하면, 코드 전체를 다시 들여다봐야 한다.
caolan/async 라이브러리
콜백 지옥을 해결하기 위해 caolan/async 같은 라이브러리를 많이 사용했으며 아직도 많은 사람들이 레거시 코드의 유지보스를 위해서인지는 잘 모르겠으나 아직도 많이 사용하고 있다.

async.waterfall 의 사용 예시
const async = require('async');
const fs = require('fs');
async.waterfall([
function(callback) {
fs.readFile('config.json', 'utf8', callback);
},
function(configData, callback) {
const config = JSON.parse(configData);
db.connect(config.dbUrl, callback);
},
function(connection, callback) {
connection.query('SELECT * FROM users', function(err, rows) {
callback(err, rows, connection);
});
},
function(rows, connection, callback) {
fs.writeFile('users.log', JSON.stringify(rows), function(err) {
connection.end();
callback(err);
});
}
], function (err, result) {
if (err) {
console.error('작업 중 에러 발생:', err);
} else {
console.log('모든 작업 완료');
}
});
async.waterfall 이나 async.series를 사용하여 들여쓰기 지옥에서 어느 정도 벗어날 수 있다. 또한 마지막 콜백 함수에서 에러를 한 번에 처리할 수 있다는 점은 유지보수 관점에서 큰 장점이다. 하지만 외부 라이브러리에 강하게 의존해야 한다는점과 함수 시그니처가 복잡하다는 근본적인 한계는 해결되지 않았다. 이 문제는 ES6(ECMAScript 2015) 에서 Promise 의 도입으로 조금씩 해결이 되었다.
콜백 패턴은 원시적이고 불안정해 보이지만 당시 브라우저와 서버 양쪽에서 비동기 I/O를 처리할 수 있는 가장 가볍고 확실한 방법이었다. 돌이켜보면 절차적인 사고방식에서 벗어나 이벤트 기반(Event-driven) 으로의 사고를 유연하게 만들어해 준 계기였다.
'Programming > 노드 (NodeJS)' 카테고리의 다른 글
| [Node.js] 자바스크립트 비동기 처리 - 비동기의 완성 Async/Await (0) | 2026.01.09 |
|---|---|
| [Node.js] 자바스크립트 비동기 처리 - 프로미스 지옥 Promise (0) | 2026.01.08 |