Menu

자바스크립트 Promise 완벽 정리: then, catch, Promise.all

자바스크립트 Promise의 세 가지 상태부터 then·catch 체이닝, Promise.all과 allSettled, 그리고 new Promise로 직접 만드는 방법까지 한 번에 정리합니다.

Promise는 미래 값에 대한 자리표시자입니다

자바스크립트에서 시간이 걸리는 작업을 해야 할 때가 있습니다. 네트워크 요청을 보내거나, 파일을 읽거나, 타이머가 끝나길 기다리는 경우죠. 이럴 때 결과를 바로 돌려줄 수는 없습니다. 대신 자바스크립트는 Promise를 건네줍니다. 언젠가 존재하게 될 값을 대신 들고 있는 객체라고 보면 됩니다.

index.js
Output
Click Run to see the output here.

첫 번째 console.log에는 아직 대기(pending) 상태인 Promise가 찍힙니다. 0.5초가 지나면 Promise가 이행(resolve)되고, .then에 등록한 콜백이 그 값을 받아 실행되죠. Promise 자체는 그냥 평범한 객체일 뿐이에요. 다만 값이 준비됐을 때 듣고 있던 쪽에 알려주는 방법을 알고 있다는 점이 특별합니다.

Promise의 세 가지 상태

Promise는 항상 다음 세 가지 상태 중 하나에 놓여 있습니다.

  • 대기(pending) — 작업이 진행 중입니다. 아직 값이 없어요.
  • 이행(fulfilled) — 작업이 성공했습니다. 결과 값을 쓸 수 있습니다.
  • 거부(rejected) — 작업이 실패했습니다. 에러 정보를 받을 수 있어요.

Promise는 pending에서 fulfilled 또는 rejected로 딱 한 번만 전환되고, 그 뒤로는 그 상태에 계속 머무릅니다. 한번 완료된 Promise는 되돌릴 수도 없고, 두 번 resolve할 수도 없어요.

index.js
Output
Click Run to see the output here.

Promise.resolve(value)는 이미 이행된 Promise를, Promise.reject(error)는 이미 거부된 Promise를 바로 만들어 줍니다. 테스트 코드를 짜거나, 함수에서 가끔 결과값을 즉시 돌려줘야 할 때 Promise로 감싸 반환하기 좋아서 꽤 자주 쓰게 됩니다.

값 꺼내 쓰기: .then.catch로 Promise 다루기

Promise는 직접 값을 꺼내 쓰는 방식이 아닙니다. 대신 .then에 콜백을 넘겨두면, 값이 준비되는 순간 Promise가 그 콜백을 호출해 줍니다.

index.js
Output
Click Run to see the output here.

.catch(fn)는 Promise가 reject됐을 때 실행됩니다. 내부적으로는 .then(undefined, fn)의 축약형이죠. 체인 맨 끝에 .catch() 하나만 붙여두면 위쪽 단계 어디서든 발생한 에러를 모두 잡아주기 때문에, .then 뒤마다 일일이 달 필요가 없습니다.

Promise 체이닝: .then은 매번 새로운 Promise를 반환한다

여기서 많은 분들이 헷갈려 합니다. .then()은 단순히 콜백만 실행하는 게 아니라, 콜백이 반환한 값으로 resolve되는 새로운 Promise 를 반환합니다. 덕분에 이렇게 체이닝이 가능한 거죠:

index.js
Output
Click Run to see the output here.

각 단계의 결과가 다음 단계로 그대로 넘어갑니다. .then 콜백이 Promise를 반환하면 체인은 Promise가 끝날 때까지 기다렸다가 다음으로 넘어가기 때문에, 비동기 작업을 깔끔하게 이어 붙일 수 있습니다:

index.js
Output
Click Run to see the output here.

순차적으로 실행되는 비동기 단계 3개를, 콜백 지옥 없이 깔끔하게 처리했습니다. 같은 로직을 콜백으로 짜 보면 왜 Promise가 단숨에 대세가 됐는지 바로 체감할 수 있습니다.

에러는 체인을 따라 흘러내려간다

reject된 Promise는 .catch를 만날 때까지 중간의 모든 .then을 건너뜁니다. Promise의 에러 처리 모델은 이게 전부라고 봐도 됩니다.

index.js
Output
Click Run to see the output here.

.then 안에서 에러를 던지면 그 .then이 반환한 Promise가 거부 상태가 됩니다. 다음 .then은 이 거부를 그대로 흘려보내고, 결국 .catch가 이를 받아서 처리하죠. 보통은 체인 끝에 .catch 하나만 붙여도 충분합니다. 반대로 .catch가 하나도 없는 체인은 "unhandled promise rejection" 경고를 띄우니, 이 경고가 보이면 반드시 잡아줘야 합니다.

new Promise로 직접 Promise 만들기

실무에서는 라이브러리가 돌려주는 Promise를 그대로 갖다 쓰는 경우가 대부분입니다. 하지만 가끔은 Promise를 반환하지 않는 코드, 특히 예전 스타일의 콜백 API를 Promise로 감싸야 할 때가 있습니다:

index.js
Output
Click Run to see the output here.

new Promise에 넘기는 함수를 executor(실행자) 라고 부릅니다. 이 함수는 두 개의 인자를 받는데요, resolve(성공 값으로 호출)와 reject(에러로 호출)입니다. 둘 중 하나를 정확히 한 번만 호출해야 하고, 그 이후의 호출은 무시됩니다.

삽질을 줄여주는 두 가지 습관을 소개합니다.

  • new Promise는 아직 Promise 기반이 아닌 것을 감쌀 때만 쓰세요. 이미 Promise를 반환하는 함수라면 그대로 반환하면 됩니다.
  • reject에는 문자열이 아니라 반드시 Error 객체를 넘기세요. 스택 트레이스는 버리기엔 너무 아까운 정보입니다.

병렬로 실행하기: Promise.all 예제

.then 체이닝은 순차적으로 실행됩니다. 서로 독립적인 여러 개의 비동기 작업을 동시에 돌리고 싶을 때 꺼내 드는 카드가 바로 Promise.all입니다.

index.js
Output
Click Run to see the output here.

타이머 세 개가 동시에 돌아갑니다. Promise.all은 입력한 순서 그대로 결과를 배열에 담아 resolve하며, 모든 Promise가 fulfilled 상태가 되어야 완료됩니다. 총 소요 시간은 900ms가 아니라 대략 400ms 정도죠.

주의할 점이 하나 있습니다. Promise.all은 포함된 Promise 중 하나라도 reject되면 즉시 전체가 reject되고, 나머지 결과는 그대로 버려집니다. 페이지 하나를 그리는 데 API 세 개의 응답이 모두 필요한 상황처럼 "전부 다 있어야 의미가 있는" 경우엔 이 동작이 맞습니다. 그렇지 않다면 allSettled를 쓰는 게 낫습니다.

일부 실패를 허용하고 싶을 때: Promise.allSettled

Promise.allSettled는 모든 Promise가 끝날 때까지 기다립니다. 성공이든 실패든 상관없이 전부 기다린 뒤, 각각의 결과를 정리해서 보고서처럼 돌려줍니다.

index.js
Output
Click Run to see the output here.

각 결과는 { status: "fulfilled", value } 또는 { status: "rejected", reason } 형태의 객체로 반환됩니다. 일부가 실패해도 괜찮은 상황, 예를 들어 이벤트 배치 로깅, 여러 썸네일 한꺼번에 가져오기, 서로 독립적인 헬스체크를 돌릴 때 특히 유용합니다.

알아두면 좋은 다른 조합 메서드도 두 가지 있습니다.

  • Promise.race([...]) — 여러 Promise 중 가장 먼저 결과가 확정된(성공이든 실패든) Promise의 상태를 그대로 따라갑니다. 타임아웃 구현에 딱 좋습니다.
  • Promise.any([...]) — 가장 먼저 성공한 값으로 이행(fulfill)되고, 중간에 실패한 건 무시합니다. 모든 Promise가 실패했을 때만 거부됩니다.

Promise는 항상 비동기로 동작한다

이미 resolve된 Promise라도 .then 콜백은 항상 비동기로 실행됩니다. 절대 동기적으로, 같은 틱에서 바로 호출되지 않습니다.

index.js
Output
Click Run to see the output here.

출력 결과는 , , 즉시 순서입니다. .then 콜백은 현재 실행 중인 코드가 모두 끝난 뒤, 마이크로태스크 큐에서 실행되죠. "Promise 콜백은 절대 동기적으로 실행되지 않는다"는 이 규칙 덕분에 Promise와 동기 코드를 섞어 써도 동작이 예측 가능합니다. 동기 코드가 항상 먼저 끝나니까요.

다음 주제: async/await

.then 체이닝도 물론 동작하지만, 단계가 두세 개를 넘어가기 시작하면 계단처럼 들여쓰기가 쌓여서 보기 불편해집니다. async/await는 Promise 위에 얹은 문법 설탕으로, 똑같은 로직을 마치 동기 코드처럼 쓸 수 있게 해줍니다. 에러는 try/catch로 잡고, 중간 결과값은 평범한 변수에 담으면 끝이죠. 다음 장에서 자세히 살펴보겠습니다.

자주 묻는 질문

자바스크립트에서 Promise란 무엇인가요?

Promise는 '아직 준비되지 않은 값'을 표현하는 객체입니다. 보통 네트워크 요청처럼 비동기 작업의 미래 결과를 담죠. 상태는 항상 pending, fulfilled, rejected 세 가지 중 하나이며, 실제 값은 .then().catch()로 콜백을 붙여서 꺼내 씁니다.

then과 catch는 어떻게 다른가요?

.then(onFulfilled)은 Promise가 성공적으로 이행됐을 때 실행되고 resolve된 값을 받습니다. 반면 .catch(onRejected)는 해당 Promise나 체인 위쪽 어딘가에서 reject가 발생했을 때 실행되며 에러를 받습니다. 체인 마지막에 .catch() 하나만 두면 그 위 모든 단계의 에러를 한꺼번에 처리할 수 있어요.

Promise.all은 어떤 역할을 하나요?

Promise.all([p1, p2, p3])은 Promise 배열을 받아서, 모두 이행됐을 때 resolve된 값들의 배열로 fulfill되는 하나의 Promise를 돌려줍니다. 단, 그중 하나라도 reject되면 전체가 즉시 reject됩니다. 실패 여부와 상관없이 모든 결과를 받고 싶다면 Promise.allSettled를 쓰세요.

Promise와 async/await 중 뭘 써야 하나요?

사실 둘은 같은 구조입니다. async/await는 Promise 위에 얹은 문법 설탕이에요. 요즘 새로 짜는 코드는 async/await 쪽이 읽기 편하지만, 여전히 함수는 Promise를 반환하고 에러는 try/catch.catch()로 잡으며, 병렬 실행은 Promise.all에 맡깁니다. Promise 동작 원리를 알아야 async/await도 제대로 이해돼요.

Coddy로 코딩 배우기

시작하기