티스토리 뷰

현대 웹 애플리케이션은 사용자에게 끊임없는 '기다림'을 요구합니다. 서버에서 데이터를 가져오고, 이미지 파일을 업로드하고, 타이머를 작동시키는 모든 과정이 **비동기(Asynchronous)**로 이루어지죠. 자바스크립트는 싱글 스레드라는 한계를 극복하기 위해 이러한 비동기 처리를 선택했고, 그 중심에는 '콜백 함수'가 있었습니다. 하지만 편리함도 잠시, 복잡한 비즈니스 로직이 얽히면서 우리는 코드의 미로인 '콜백 지옥'에 갇히게 되었습니다.
1. 핵심 개념: 왜 콜백은 지옥이 되었나? (Deep Dive)
비동기 프로그래밍은 "지금 당장 실행되지 않아도 되는 작업"을 뒤로 미루는 기술입니다. 이때 **콜백(Callback)**은 "작업이 끝나면 이 함수를 실행해줘"라고 전달하는 일종의 '나중에 실행할 권리'입니다.
🧩 비유로 이해하는 콜백 지옥
레스토랑에 갔다고 상상해 보세요.
- 웨이터에게 메뉴판을 달라고 합니다 (비동기 요청).
- 메뉴판이 오면 주문을 합니다 (첫 번째 콜백).
- 요리가 나오면 한 입 먹어봅니다 (두 번째 콜백).
- 맛이 이상하면 셰프를 부릅니다 (세 번째 콜백).
- 셰프가 오면 따집니다 (네 번째 콜백)...
이 과정이 꼬리에 꼬리를 물고 이어지면 코드는 오른쪽으로 무한히 깊어집니다. 이를 **'멸망의 피라미드(Pyramid of Doom)'**라고 부르죠. 가독성은 바닥을 치고, 에러가 발생했을 때 어디서부터 잘못됐는지 찾는 것은 불가능에 가까워집니다.
2. 실전 예제: 이커머스 결제 시스템 (Hands-on)
실제 서비스에서 흔히 발생하는 '장바구니 확인 -> 주문 생성 -> 결제 요청' 로직을 통해 콜백 지옥의 실체와 해결책을 살펴보겠습니다.
❌ Bad Case: 콜백 지옥의 전형
// 끔찍한 중첩 구조의 시작
checkInventory(items, (inventoryErr, stock) => {
if (inventoryErr) {
console.error("재고 확인 실패:", inventoryErr);
} else {
createOrder(stock, (orderErr, orderId) => {
if (orderErr) {
console.error("주문 생성 실패:", orderErr);
} else {
processPayment(orderId, (paymentErr, receipt) => {
if (paymentErr) {
console.error("결제 실패:", paymentErr);
} else {
console.log("결제 완료! 영수증 번호:", receipt.id);
// 이 이후에 이메일 발송 로직이 추가된다면...?
}
});
}
});
}
});
✅ Good Case: Promise와 Async/Await로 정화하기
최근의 표준은 Promise를 기반으로 한 async/await입니다. 비동기 코드를 마치 동기 코드처럼 위에서 아래로 읽히게 만듭니다.
/**
* 결제 프로세스를 처리하는 메인 로직
*/
async function completePurchase(items) {
try {
// 1. 재고 확인 (await를 통해 비동기 처리가 끝날 때까지 대기)
const stock = await checkInventory(items);
// 2. 주문 생성 (이전 단계의 결과값인 stock을 활용)
const orderId = await createOrder(stock);
// 3. 결제 처리
const receipt = await processPayment(orderId);
console.log(`🎉 결제 성공: ${receipt.id}`);
return receipt;
} catch (error) {
// 모든 단계의 에러가 이 한 곳으로 모입니다 (Centralized Error Handling)
handlePurchaseError(error);
}
}
// 에러 핸들링 분리
function handlePurchaseError(err) {
const errorMap = {
INVENTORY_EMPTY: "상품 재고가 부족합니다.",
PAYMENT_REJECTED: "카드 한도 초과 혹은 정보 오류입니다."
};
console.error(errorMap[err.code] || "알 수 없는 시스템 오류 발생");
}
3. 트러블슈팅: 놓치기 쉬운 비동기 함정
비동기 제어권을 넘겨받을 때 가장 흔히 발생하는 문제는 **'에러 삼키기(Error Swallowing)'**입니다.
- 문제점: 콜백 함수 내부에서 throw new Error()를 발생시키면 외부의 try-catch 블록이 이를 감지하지 못합니다. 이벤트 루프가 이미 다음 틱으로 넘어갔기 때문입니다.
- 해결책: 반드시 Promise를 반환하도록 함수를 설계하고, await를 사용하거나 .catch() 체이닝을 명시해야 합니다.
4. 장단점 및 고려사항 (Trade-offs)
비동기 처리를 개선하는 과정에도 기회비용은 존재합니다.
| 방식 | 장점 | 단점 |
| Callback | 추가 라이브러리 없이 가장 빠름 (Low-level) | 가독성 최악, 에러 핸들링이 파편화됨 |
| Promise | 비동기 상태를 객체화하여 관리 가능 | .then()이 많아지면 결국 '프로미스 지옥' 발생 |
| Async/Await | 코드 가독성 극대화, 비즈니스 로직 집중 가능 | 구형 브라우저(IE 등) 지원을 위해 트랜스파일링 필요 |
수학적 효율성 관점:
비동기 작업 $n$개가 직렬로 연결될 때, 각 작업의 소요 시간이 $t_i$라면 전체 소요 시간은 $\sum_{i=1}^{n} t_i$가 됩니다. 하지만 만약 각 작업이 독립적이라면 Promise.all을 사용하여 $max(t_1, t_2, \dots, t_n)$으로 전체 시간을 단축하는 설계를 고민해야 합니다.
5. 결론 및 제언
콜백 지옥은 단순한 코드의 형태 문제가 아니라, 제어권의 상실 문제였습니다. Async/Await는 개발자에게 다시 프로그램의 흐름을 통제할 권한을 부여했습니다. 하지만 무분별한 await 사용은 오히려 병렬 처리가 가능한 로직을 직렬로 만들어 성능 저하를 일으키기도 합니다.
지금 작성 중인 비동기 로직 중에서, 반드시 순차적으로 실행되어야 하는 것과 동시에 실행되어도 무방한 것은 무엇인가요? 이 구분이 코드의 품질을 결정짓는 시니어의 한 끗 차이입니다.
'Frontend > JAVASCRIPT' 카테고리의 다른 글
| 자바스크립트의 유령, 클로저(Closure)와 스코프: 메모리 속에 살아있는 변수의 비밀 (0) | 2026.03.02 |
|---|---|
| Callback Hell에서 탈출하기: Promise와 async/await의 깊은 이해 (0) | 2026.03.02 |
| 동적인 웹을 위한 첫걸음: DOM 조작의 본질과 효율적인 제어 전략 (0) | 2026.03.02 |
| 유연한 데이터 핸들링의 정점: Modern JavaScript 객체 분해와 확산 연산자 활용법 (0) | 2026.03.02 |
| 배열 메서드 완벽 정리: map, filter, reduce 활용법 (0) | 2026.02.10 |
- Total
- Today
- Yesterday
- Rag
- 협력
- java
- It용어
- react
- 멀티모달
- MSA
- HBM
- LLM
- 엣지컴퓨팅
- 카카오
- prompt engineering
- Nextjs
- 웹기초
- on-device ai
- AI
- CSR
- Javascript
- 구글
- sLLM
- SSR
- 스마트안경
- CSS
- TypeScript
- HTML
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 |