본문 바로가기

개발 공부

콜백 지옥 (Callback Hell)

콜백 지옥 (Collback Hell)

웹/앱 개발을 하면서 서버나 외부 시스템과 비동기 통신을 할 때, 우리는 종종 콜백 함수를 사용합니다.
하지만 콜백을 잘못 쓰면, 프로젝트 규모가 커질수록 지옥을 경험하게 됩니다.

이걸 "콜백 지옥 (Callback Hell)"이라고 부릅니다.

 

예시: 회원가입 프로세스

요구사항

회원가입 시 다음 작업을 순차적으로 처리해야 합니다.

  1. 사용자의 기본 정보(User) 등록
  2. 사용자의 주소(Address) 등록
  3. 회원가입 축하 이메일 발송
  4. 내부 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