자바스크립트 Promise 구조와 사용방법, then 체이닝과 catch 예외 처리

개발/javascript&typescript1 2025. 12. 28. 19:37

Promise가 뭔가요?

Promise는 "미래에 완료될 작업의 결과"를 약속하는 객체입니다. 지금 당장 값이 없더라도, 작업이 끝나면 성공(fulfilled)하거나 실패(rejected)할 거라는 상태를 관리해줍니다.

 

Promise는 보통 아래 3가지 상태를 가집니다.

  • pending: 아직 진행 중
  • fulfilled: 성공적으로 완료 (결과 값이 생김)
  • rejected: 실패 (에러가 생김)

 


 

Promise 기본 구조: new Promise(resolve, reject)

Promise는 보통 생성자 형태로 시작합니다.

const p = new Promise((resolve, reject) => {
  // 비동기 작업 수행

  const ok = true;

  if (ok) {
    resolve("성공 결과");
  } else {
    reject(new Error("실패 이유"));
  }
});

여기서 핵심은 두 가지입니다.

 

  • resolve(값): 성공으로 상태를 바꾸고, "다음 then"으로 값을 넘깁니다.
  • reject(에러): 실패로 상태를 바꾸고, "catch"로 에러를 넘깁니다.

 

then: 성공했을 때 흐름을 이어가는 곳

then은 Promise가 성공(fulfilled)했을 때 실행됩니다.

p.then((value) => {
  console.log("성공:", value);
});

 

then은 체이닝(연결)이 가능합니다

then은 또 다른 Promise를 반환하기 때문에, 이어서 계속 연결할 수 있습니다.

doSomething()
  .then((res1) => {
    // res1 처리
    return doNext(res1); // Promise를 반환하면 다음 then이 기다렸다가 받습니다
  })
  .then((res2) => {
    // res2 처리
    return res2.total; // 일반 값을 return하면 바로 다음 then으로 전달됩니다
  })
  .then((total) => {
    console.log("total:", total);
  });

여기서 많이 헷갈리는 포인트가 하나 있는데요.

 

  • then에서 Promise를 return하면: "그 Promise가 끝난 결과"가 다음 then으로 전달됩니다.
  • then에서 일반 값을 return하면: "그 값이 즉시" 다음 then으로 전달됩니다.

 

catch: 실패했을 때 한 번에 잡는 곳

catch는 Promise 체인 중 어디서든 에러가 발생하면 (= reject되거나, then 내부에서 예외가 터지면) 흐름을 받아서 처리합니다.

doSomething()
  .then((res) => {
    // 처리 중 예외가 나도 catch로 갑니다
    return JSON.parse(res);
  })
  .then((obj) => {
    console.log(obj);
  })
  .catch((err) => {
    console.error("에러 처리:", err);
  });

 

즉, catch는 단순히 reject만 받는 게 아니라, then 내부에서 throw된 에러도 같이 처리합니다.

그래서 보통 체인 끝에 catch를 붙여서 "전체 예외 처리"를 하는 패턴이 자주 쓰입니다.

 


 

then의 두 번째 인자 vs catch, 뭐가 더 흔한가요?

then은 사실 두 번째 인자로 실패 콜백을 받을 수도 있습니다.

p.then(
  (value) => console.log("성공:", value),
  (err) => console.error("실패:", err)
);

 

그런데 보통 아래 형태를 더 많이 씁니다.

p.then((value) => console.log(value))
 .catch((err) => console.error(err));

이유는 간단합니다.

 

  • then 두 번째 인자로 처리하면, 그 then 구간에만 국소적으로 실패 처리가 붙는 느낌이 강합니다.
  • catch로 빼면, 체인 전체에서 발생하는 에러를 한 군데에서 모아 처리하기가 쉽습니다.

 

catch 이후에는 어떻게 되나요?

 

catch도 마찬가지로 Promise를 반환하므로, 이후에 then을 또 이어갈 수 있습니다.

doSomething()
  .then((res) => {
    return riskyWork(res);
  })
  .catch((err) => {
    console.error("복구 시도:", err);
    return { fallback: true }; // 에러를 처리하고 대체 값을 반환하면 다음 then은 정상 흐름
  })
  .then((value) => {
    console.log("이후 흐름:", value);
  });

여기서 중요한 점은:

 

  • catch에서 값을 return하면: 에러 흐름이 "정상 흐름으로 복구"될 수 있습니다.
  • catch에서 다시 throw하면: 에러가 다시 다음 catch로 전파됩니다.

 



일반적으로 자주 쓰는 패턴 3가지

 

1) 가장 기본: 성공 처리 + 마지막에 catch

fetch("/api/user")
  .then((r) => r.json())
  .then((user) => {
    console.log(user.name);
  })
  .catch((e) => {
    console.error("요청 실패:", e);
  });

2) 단계별로 데이터 가공해서 넘기기

getOrder()
  .then((order) => order.items)
  .then((items) => items.filter((it) => it.count > 0))
  .then((validItems) => {
    console.log("유효 아이템:", validItems.length);
  })
  .catch(console.error);

 

3) 중간에 에러를 감싸서 의미를 보강하기

doSomething()
  .catch((err) => {
    throw new Error("doSomething 단계에서 실패: " + err.message);
  })
  .then((res) => {
    console.log(res);
  })
  .catch((err) => {
    console.error("최종 에러:", err);
  });

 


 

흔한 오해 정리

  • Promise는 "비동기 그 자체"가 아니라, 비동기 결과를 다루는 규약(객체)입니다.
  • then은 성공 콜백만 있는 게 아니라, return으로 다음 흐름을 설계하는 곳입니다.
  • catch는 reject만 처리하는 게 아니라, then 내부 예외도 함께 처리합니다.
  • catch에서 값을 return하면 흐름을 다시 정상으로 되돌릴 수 있습니다.

 


 

정리하자면..

Promise는 비동기 코드를 "순서대로 읽히게 만드는 구조"에 가깝습니다.

resolve/reject로 성공과 실패를 명확히 나누고, then으로 성공 흐름을 이어 붙이고, catch로 에러를 모아서 처리하는 방식이요.

반응형
admin