Node.js 비동기 처리 이해하기: Promise 이전 방식부터 async/await, 병렬 실행까지
Node.js는 이벤트 루프 기반 구조로 비동기 I/O를 효율적으로 처리하는 런타임입니다.
비동기 처리 방식을 이해하면 Promise와 async/await의 차이뿐 아니라 병렬 실행 패턴까지 파악할 수 있습니다.
이 글에서는 초기 콜백 방식부터 현대적인 비동기 패턴까지 정리합니다.
1. Promise 이전 비동기 처리 방식
초기 Node.js는 콜백(callback) 방식을 중심으로 동작했습니다. 비동기 결과는 콜백 함수로 전달됩니다.
fs.readFile("data.txt", (err, data) => {
if (err) {
// 에러 처리
}
// 결과 사용
});
단순한 작업은 문제가 없지만, 여러 비동기 작업이 연속될 경우 콜백이 중첩되었습니다. 이른바 콜백 지옥(callback hell)입니다.
getUser(id, (err, user) => {
getPosts(user.id, (err, posts) => {
getComments(posts, (err, comments) => {
save(comments, (err) => {
// ...
});
});
});
});
비즈니스 로직을 따라가기 어렵고, 에러 처리 흐름도 복잡해졌습니다. 이를 해결하기 위한 개념이 Promise입니다.
2. Promise의 등장과 역할
Promise는 “미래에 완료될 작업을 나타내는 객체”입니다.
비동기 상태를 관리할 수 있고, 체이닝을 통해 흐름을 구성할 수 있습니다.
getUser()
.then(user => getPosts(user.id))
.then(posts => save(posts))
.catch(err => console.error(err));
콜백의 중첩을 줄이고, .catch()로 에러 처리 흐름을 일원화했습니다.
또한 Promise.all, Promise.race 등 여러 Promise를 조합하는 기능도 제공하여 병렬 처리까지 가능합니다.
3. async/await의 도입
async/await은 Promise 기반 문법으로, 비동기 작업을 동기 코드처럼 작성할 수 있습니다.
async function main() {
try {
const user = await getUser();
const posts = await getPosts(user.id);
await save(posts);
} catch (err) {
console.error(err);
}
}
조건문, 반복문 등과 자연스럽게 결합할 수 있어 가독성이 크게 향상됩니다.
비즈니스 로직을 명확하게 표현할 수 있다는 점에서 Promise 체이닝보다 우수한 경우가 많습니다.
4. Promise와 async/await의 상호 변환 가능성
Promise → async/await
모든 Promise 기반 코드는 async/await으로 변환 가능합니다.
async/await 자체가 Promise 위에서 동작하기 때문입니다.
async/await → Promise
async/await 코드도 Promise 체이닝 형태로 변환 가능합니다.
다만, 로직이 복잡할 경우 가독성이 떨어지고 유지보수성이 저하될 수 있습니다.
정리하면, 두 방식은 서로 100% 변환 가능합니다.
async/await은 문법적 설탕(syntactic sugar)일 뿐이며, 내부 동작은 Promise 기반입니다.
5. 직렬 실행과 병렬 실행
비동기 코드라고 해서 모두 병렬로 실행되지는 않습니다.await는 해당 Promise의 완료를 기다리기 때문에 코드가 자연스럽게 직렬 실행 형태를 띱니다.
직렬 실행 예시
await taskA(); // 1초
await taskB(); // 1초
await taskC(); // 1초
총 3초가 소요됩니다.
의존성이 있는 작업에 적합한 방식입니다.
병렬 실행 예시
const a = taskA();
const b = taskB();
const c = taskC();
await Promise.all([a, b, c]);
세 작업이 동시에 시작되며 총 1초만 소요됩니다.
서로 독립적인 작업일 때 효과적입니다.
비교 요약
| 구분 | 직렬 실행 | 병렬 실행 |
| 실행 순서 | 순서대로 진행 | 동시에 시작 |
| 총 실행 시간 | A + B + C | max(A, B, C) |
| 적합한 경우 | 앞 결과가 필요한 의존 작업 | 서로 독립적인 작업 |
병렬 실행을 하려면 Promise.all을 명시적으로 사용해야 합니다.
async/await은 기본적으로 직렬 처리 흐름을 만들기 때문입니다.
6. 마무리
Node.js 비동기 모델은 콜백에서 시작해 Promise를 거쳐 async/await으로 발전해 왔습니다.
세 방식은 모두 비동기 처리를 위한 기술이지만, 코드 복잡성과 유지보수성 측면에서 큰 차이가 있습니다.
특히 async/await은 가독성과 명료성이 뛰어나 현재 가장 널리 사용되는 방식입니다.
다만, 병렬 처리가 필요한 경우 Promise 조합 기능을 함께 사용해야 성능을 최적화할 수 있습니다.
Node.js 비동기 패턴의 흐름과 차이를 이해하면, 구조적이고 효율적인 서버 코드를 작성하는 데 도움이 됩니다.
🧾 작성 참고
이 글은 ChatGPT의 도움을 받아 내용을 정리하였습니다.