콜백 지옥 (Collback Hell)
웹/앱 개발을 하면서 서버나 외부 시스템과 비동기 통신을 할 때, 우리는 종종 콜백 함수를 사용합니다.
하지만 콜백을 잘못 쓰면, 프로젝트 규모가 커질수록 지옥을 경험하게 됩니다.
이걸 "콜백 지옥 (Callback Hell)"이라고 부릅니다.
예시: 회원가입 프로세스
요구사항
회원가입 시 다음 작업을 순차적으로 처리해야 합니다.
- 사용자의 기본 정보(User) 등록
- 사용자의 주소(Address) 등록
- 회원가입 축하 이메일 발송
- 내부 Slack 채널에 가입 알림 전송
※ 각 작업은 앞 작업이 성공해야만 다음 작업을 진행할 수 있습니다. (의존성 O)
콜백 지옥 버전
registerUser(userInfo, (userError, user) => {
if (userError) {
console.error('유저 등록 실패:', userError);
return;
}
saveAddress(user.id, addressInfo, (addressError, address) => {
if (addressError) {
console.error('주소 저장 실패:', addressError);
return;
}
sendWelcomeEmail(user.email, (emailError) => {
if (emailError) {
console.error('환영 메일 발송 실패:', emailError);
return;
}
sendSlackNotification(user.name, (slackError) => {
if (slackError) {
console.error('Slack 알림 실패:', slackError);
return;
}
console.log('회원가입 프로세스 완료');
});
});
});
});
문제점
- 중첩의 중첩: registerUser → saveAddress → sendWelcomeEmail → sendSlackNotification
중첩이 너무 깊어 가독성이 급격히 떨어집니다. - 에러 처리 중복: 매 단계마다 따로 try-catch 처리하듯 if (error)를 써야 합니다.
- 확장성↓: 나중에 예를 들어 '가입 쿠폰 발급' 같은 추가 작업이 생기면? 또 콜백을 안에 넣어야 합니다.
- 디버깅 악몽: 어디서 실패했는지 트래킹이 어렵고, 수정 시 에러 포인트가 많아집니다.
이런 구조는 특히 회원가입, 결제, 주문처리 같은 비즈니스 크리티컬한 로직에선 치명적입니다.
해결 1 - Promise로 리팩터링
콜백 지옥을 피하기 위해 첫 번째 개선 방법은 Promise를 사용하는 것입니다.
registerUser(userInfo)
.then((user) => saveAddress(user.id, addressInfo))
.then((address) => sendWelcomeEmail(userInfo.email))
.then(() => sendSlackNotification(userInfo.name))
.then(() => {
console.log('회원가입 프로세스 완료');
})
.catch((error) => {
console.error('회원가입 중 에러 발생:', error);
});
- 한눈에 흐름이 보입니다.
- .catch 하나로 모든 에러를 통합 처리합니다.
- 새 작업을 추가하려면 .then()만 하나 더 붙이면 됩니다.
- 디버깅과 로깅이 깔끔해집니다.
해결 2 - async/await로 리팩터링
Promise 체이닝도 좋지만, 요즘은 async/await을 더 선호합니다.
코드가 동기 함수처럼 깔끔해지고, 예외 처리도 try-catch로 통합할 수 있기 때문입니다.
async function completeUserSignUp(userInfo: UserInfo, addressInfo: AddressInfo) {
try {
const user = await registerUser(userInfo);
const address = await saveAddress(user.id, addressInfo);
await sendWelcomeEmail(user.email);
await sendSlackNotification(user.name);
console.log('회원가입 프로세스 완료');
} catch (error) {
console.error('회원가입 중 에러 발생:', error);
}
}
- 들여쓰기가 얕고 흐름이 명확합니다.
- try-catch로 예외를 한 곳에서 관리할 수 있습니다.
- 에디터 자동완성(IntelliSense)도 잘 동작합니다.
- 디버깅 시 스택 추적(stack trace)이 직관적입니다.
서비스 로직 (Service Layer) 예시
class UserService {
async signUp(userInfo: UserInfo, addressInfo: AddressInfo) {
const user = await this.userRepository.createUser(userInfo);
const address = await this.addressRepository.saveAddress(user.id, addressInfo);
await this.emailService.sendWelcomeEmail(user.email);
await this.notificationService.sendSlackNotification(user.name);
return { user, address };
}
}
컨트롤러 호출부
@Post('/signup')
async signUp(@Body() dto: SignUpDto) {
await this.userService.signUp(dto.userInfo, dto.addressInfo);
return { message: '회원가입 성공' };
}
| 콜백 지옥 | Promise | async/await | |
| 가독성 | 최악 | 좋아짐 | 최고 |
| 에러 핸들링 | 매번 수동 | .catch 하나로 통합 | try-catch 통합 |
| 유지보수성 | 매우 낮음 | 높음 | 매우 높음 |
| 추천 여부 | 금지 | 가능 | 추천 |
'개발 공부' 카테고리의 다른 글
| 얕은 복사, 깊은 복사 (1) | 2025.05.10 |
|---|---|
| 객체지향 프로그래밍(OOP) (0) | 2025.05.03 |
| 정규화(Normalization) (0) | 2025.04.27 |
| 웹소켓(WebSocket) (1) | 2025.04.04 |
| REST API (0) | 2025.03.27 |