ES6 Promise 체이닝과 에러 핸들링 정리

배경

레거시 프로젝트의 콜백 기반 API 호출 코드를 ES6 Promise로 전환하는 작업을 진행했다. Node 6에서 7로 업그레이드하면서 async/await도 고려했지만, 아직 프로덕션에 적용하기엔 이르다고 판단했다.

자주 한 실수

1. catch 후 체이닝

fetchUser()
  .then(user => fetchPosts(user.id))
  .catch(err => console.error(err))
  .then(posts => renderPosts(posts)); // posts가 undefined

catch도 Promise를 반환하므로 이후 then이 계속 실행된다. 에러 발생 시 posts는 undefined가 되어 런타임 에러가 발생했다.

2. 중첩된 then

fetchUser().then(user => {
  fetchPosts(user.id).then(posts => {
    // 다시 콜백 지옥
  });
});

Promise를 사용해도 중첩하면 의미가 없다. return을 명시적으로 작성해야 체이닝이 된다.

개선한 패턴

fetchUser()
  .then(user => fetchPosts(user.id))
  .then(posts => {
    return posts.map(transformPost);
  })
  .then(transformedPosts => renderPosts(transformedPosts))
  .catch(err => {
    logger.error('Failed to load posts', err);
    renderError();
  });

마지막 catch 하나로 체인 전체의 에러를 처리할 수 있다. 각 단계에서 명시적으로 return하여 다음 then으로 값을 전달했다.

Promise.all 활용

여러 API를 병렬로 호출할 때 유용했다.

Promise.all([
  fetchUser(userId),
  fetchSettings(userId),
  fetchNotifications(userId)
])
  .then(([user, settings, notifications]) => {
    renderDashboard({ user, settings, notifications });
  })
  .catch(err => {
    // 하나라도 실패하면 여기로
    handleError(err);
  });

정리

Promise는 콜백보다 훨씬 읽기 좋은 코드를 만들어준다. 하지만 체이닝과 에러 처리 규칙을 정확히 이해하지 않으면 오히려 디버깅이 어려워진다. 팀 내 코드 리뷰에서 이 패턴들을 공유하고 컨벤션으로 정했다.