티스토리 뷰
JavaScript 개발자가 “프로미스 체이닝(Promise Chaining)”을 검색하는 주요 이유는 다음과 같은 실전 문제 때문임:
- 비동기 작업을 순차적으로 처리해야 하는데 콜백 지옥(callback hell)으로 코드 가독성이 떨어짐.
- `.then()`을 여러 번 쓰면 동작이 어떻게 이어지는지, 값이 다음 체인으로 어떻게 전달되는지 이해하기 어려움.
- 에러가 발생했을 때 어디서 잡아야 하는지, 에러 처리 흐름이 혼란스러움.
- `.then()` 안에서 값을 리턴했을 때과 `.then()` 자체가 항상 새로운 Promise를 반환한다는 사실이 직관적이지 않음.
특히 “어떤 시점에서 프로미스가 실행되고 결과가 다음 체인으로 어떻게 전달되는가?”는 많은 초중급 개발자에게 추상적으로 느껴짐. 이로 인해 비동기 로직의 논리적 오류, 예기치 않은 상태 전이, 중복된 에러 처리 코드 등이 발생함.
프로미스 체이닝의 원리와 실행 메커니즘
JavaScript의 Promise 객체는 비동기 작업의 완료 또는 실패를 나타내는 객체로, 세 가지 상태를 가질 수 있음: pending(대기), fulfilled(이행), rejected(거부) 상태이다. Promise 체이닝은 `.then()` 또는 `.catch()`가 반환하는 새로운 Promise 인스턴스를 기반으로 이어지는 비동기 작업의 순차 연결임.
핵심은 `.then()`과 `.catch()`는 다음과 같은 규칙으로 작동한다:
- `.then(onFulfilled)`에서 `onFulfilled` 콜백이 값을 반환하면, 해당 값은 자동으로 `Promise.resolve()`로 감싸져 다음 체인으로 전달됨.
- 반환값이 또 다른 Promise라면, 그 Promise가 완료될 때까지 대기하고, 그 결과를 다음 `.then()`에 전달함.
- 에러가 발생하면 (예: 콜백 내 throw), 가장 가까운 `.catch()`로 제어 흐름이 이동함.
체이닝의 메커니즘을 이해하려면 아래의 규칙을 기억해야 함:
- 각 `.then()`은 원래 Promise와 별개로 새 Promise를 반환함.
- 이 반환된 Promise는 이전 콜백의 리턴 값 또는 리턴 Promise가 resolve/ reject 된 결과에 따라 상태가 결정됨.
- 중간에 에러가 있으면 `.catch()`가 수신한 후, 해당 체인은 종료되거나 이후 추가 `.then()`으로 흐름이 이어질 수 있음.
체이닝 동작 비교와 코드 예시
다음 표는 프로미스 체이닝의 다양한 동작 시나리오 및 예상되는 출력과 흐름을 정량적으로 정리한 것임:
| 시나리오 | 반환 값 | 다음 체인으로 전달 | 에러 처리 |
|---|---|---|---|
| `.then()`에서 primitive 반환 | 예: 숫자, 문자열 | 그 값이 wrap 되어 전달 | 다음 `.catch()`로 에러 없음 |
| `.then()`에서 Promise 반환 | Promise의 resolved 결과 | Promise가 완료될 때까지 대기 후 전달 | 해당 Promise가 reject 되면 `.catch()`로 이동 |
| `.then()` 내부에서 throw | no return | 다음 `.then()`은 실행 안 함 | 가장 가까운 `.catch()` 실행 |
아래 단계별 예시는 프로미스 체이닝의 순차 실행, 값 전달, 에러 처리 흐름을 명확히 보여줌:
- 기본 체이닝 – 세 개의 비동기 작업을 순차적으로 실행:
function task(msg, delay) { return new Promise(resolve => setTimeout(() => resolve(msg), delay)); }
task('A완료', 1000) .then(result => { console.log(result); // A완료 return task('B완료', 1500); }) .then(result => { console.log(result); // B완료 return task('C완료', 1000); }) .then(result => { console.log(result); // C완료 }) .catch(error => { console.error('에러:', error); });
-
이 코드는 총 3,500ms(1초 + 1.5초 + 1초) 이후에 순차적으로 로그가 출력됨. - 에러 전파 – 중간에 에러 발생 시, 이후 체인은 생략:
이 경우 첫 번째 `.then()`에서 throw 되므로 두 번째 `.then()`은 실행되지 않고 `.catch()`로 이동함.Promise.resolve(10) .then(value => { if (value > 5) throw new Error('값이 너무 큼'); return value; }) .then(() => console.log('실행 안됨')) .catch(err => console.error('캐치됨:', err.message));
체이닝 오해 정리 및 주의사항
- 여러 개의 `.then()`이 같은 Promise에 붙는 것 ≠ 체이닝: 동일 Promise에 독립적으로 `.then()`을 여러 개 추가하는 것은 체이닝이 아니며, 각각은 같은 값으로 실행됨.
- `.then()` 내부에서 반환되는 값은 자동으로 Promise로 래핑됨: primitive 값 반환도 다음 체인에서 Promise.resolve(value)처럼 취급되어 정상적으로 전달됨.
- Async/Await는 체이닝의 가독성 대안: ES2017 이후 `async/await`는 프로미스 체인을 동기 코드처럼 표현할 수 있어 가독성과 유지보수성을 크게 향상시킴.
- 에러 핸들링 전략: 모든 체인 끝에 `.catch()`를 추가하는 것은 전체 체인에 대한 단일 에러 처리 포인트를 제공함.
기술의 변화가 빠르지만, 오늘 다룬 본질적인 원리를 이해하신다면 어떤 응용 도구도 금방 익히실 수 있을 겁니다.

