[JS] Promise

전통적인 js 비동기 프로그래밍의 역사

1) Callback
2) Promise
3) Generator
4) Async / Await

 

Promise 란?

비동기 작업의 성공/실패 결과를 다루기 위한 객체이다. 

 

promise는 callback pattern의 단점을 극복하기 위해 출현하였다. 

callback pattern 은 콜백 지옥, 가독성 저하, 에러 처리 어려움 등의 문제가 있었다.

callback pattern 과 다르게 promise는 안전하며, 유지보수 쉬운 코드작성이 가능하다. 

 

promise는 다음 3가지 상태를 가진다. 

  • pending: 아직 완료되지 않음
  • fulfilled: 성공적으로 완료됨 (resolve)
  • rejected: 실패함 (reject)

pending → fulfilled or rejected → settled (fulfilled 또는 rejected 상태 둘다 포함하는 표현으로 작업이 완료됨을 의미한다.)

 

Promise . then( ) . catch( ) . finally( ) ... chaining까지

실제 Promise의 구조 요약버전

class Promise {
  constructor(nbfn) {
    this.state = 'pending';
    this.value = null;
    this.thenFns = [];
    this.catchFn = null;
    this.finallyFn = null;

    const resolve = this.runSuccess.bind(this);
    const reject = this.runFail.bind(this);

    try {
      nbfn(resolve, reject);
    } catch (e) {
      reject(e);
    }
  }

  runSuccess(ret) {
    if (this.state !== 'pending') return;
    this.state = 'fulfilled';
    this.value = ret;

    let t = ret;
    for (const fn of this.thenFns) {
      try {
        t = fn(t);
      } catch (e) {
        this.runFail(e);
        return;
      }
    }

    if (this.finallyFn) this.finallyFn(); // ✅ 성공 시 finally 실행
  }

  runFail(err) {
    if (this.state !== 'pending') return;
    this.state = 'rejected';
    this.value = err;

    if (this.catchFn) {
      this.catchFn(err);
    } else {
      console.error('Uncaught Promise rejection:', err);
    }

    if (this.finallyFn) this.finallyFn(); // ✅ 실패 시 finally 실행
  }

  then(fn) {
    this.thenFns.push(fn);
    return this;
  }

  catch(errFn) {
    this.catchFn = errFn;
    return this;
  }

  finally(finalFn) {
    this.finallyFn = finalFn;
    return this;
  }
}

 

예시

new Promise((resolve, reject) => {
  console.log('1️⃣ executor 시작');
  setTimeout(() => {
    const success = Math.random() > 0.5;
    if (success) {
      resolve('✅ 성공 데이터');
    } else {
      reject('❌ 실패 사유');
    }
  }, 1000);
})
  .then((val) => {
    console.log('2️⃣ then 처리:', val);
    return val + ' [처리됨]';
  })
  .then((val) => {
    console.log('3️⃣ 또다른 then:', val);
  })
  .catch((err) => {
    console.log('4️⃣ catch 처리:', err);
  })
  .finally(() => {
    console.log('5️⃣ finally는 무조건 실행됨!');
  });

 

성공한 경우 (Math.random() > 0.5)

1️⃣ executor 시작
2️⃣ then 처리: ✅ 성공 데이터
3️⃣ 또다른 then: ✅ 성공 데이터 [처리됨]
5️⃣ finally는 무조건 실행됨!

실패한 경우 (Math.random() <= 0.5)

1️⃣ executor 시작
4️⃣ catch 처리: ❌ 실패 사유
5️⃣ finally는 무조건 실행됨!

 

  • 각 then()은 새로운 Promise를 반환하고, 그 값은 다음 then() 에 전달된다. return 값은 다음 then의 입력값이 된다.
  • then에서의 반환 값이 일반값일 경우 바로 다음 then에 값을 전달하고 promise 객체인 경우 resolve 될때까지 기다렸다가 실행된다.
    예를들어, 글을 받아온 다음에 실행 그 글에 댓글을 받아온 다음에 실행 -> p(id).then((id)=>글).then((글)=>댓글)
    그래서 promise를 연속으로 사용하고 싶을때 then 체이닝을 많이 쓰고 사실 await을 쓰면 이 짓을 안해도됨
  • throw 나 오류가 발생하면 then 체이닝을 중단하고 에러가 catch의 입력값이 되어 .catch()로 이동한다.
  • finally는 체이닝의 중간이나 마지막에 넣을 수 있으며 성공, 실패와 무관하게 항상 실행된다.

 

Promise class static method

Promise.resolve(x)

이미 존재하는 값 x곧바로 fulfilled(성공) 상태인 Promise로 감싸준다.

Promise.resolve(42).then(val => console.log(val)); 
// 👉 출력: 42
new Promise((resolve) => resolve(42)).then(console.log);
// 두 코드는 완전히 같은 코드이다.

 

Promise.reject(e)

Promise.reject(e)는 즉시 rejected 상태인 Promise를 만든다.

Promise.reject(new Error('에러!')).catch(console.error);
new Promise((_, reject) => reject(new Error('에러!'))).catch(console.error);
//두 코드는 완전히 같은 코드이다.

 

예시

x: Promise = b ? promi : Promise.resolve(y);

 

Promise 객체인 x에 값을 넣어줄 때 이미 promise인 promi 넣거나 y 를 즉시 성공상태인 Promise 객체로 만들어서 타입을 통일해 줄 수 있다. 

 

Promise.all

  • 여러 프로미스의 Fn을 동시에 실행한다. 
  • 모두 성공(fulfilled) 시 시간과 무관하게 순서를 보장한다. (결과순서가 아닌 입력순서를 보장한다는 뜻)
  • 하나라도 실패 시 바로 catch로 이동한다.
Promise.all([p1, p2, p3])
  .then(results => console.log(results))  // [res1, res2, res3]
  .catch(error => console.error(error));

 

Promise.race

  • 가장 먼저 완료된 하나만을 반환한다. 
  • 나머지는 무시된다.
  • 가장 빨리 끝난 하나가 실패하면 catch로 이동한다. 
Promise.race([p1, p2])
  .then(res => console.log(res))     // 가장 먼저 끝난 Promise의 결과
  .catch(err => console.error(err)); // 가장 먼저 실패한 Promise가 있다면 바로 catch

 

Promise.any

  • 여러 프로미스 중에서 가장 먼저 성공한 하나만을 반환한다.
  • 단 하나라도 성공한다면 .then을 실행한다.
  • 모두 실패해야 catch 로 이동한다.
Promise.any([p1, p2, p3])
  .then(res => console.log(res))     // 가장 빨리 성공한 하나
  .catch(err => console.error(err)); // 모두 실패한 경우에만 catch

 

Promise.allSettled

  • 여러 프로미스 Fn 이 성공이건 실패건 모두 settled 될때까지 기다린다.
  • 입력순서가 보장된다.
  • 결과는 각각의 status(fulfilled 또는 rejected), value, reason을 포함한 객체 배열로 반환된다.
  • catch를 쓰지 않고도 실패 정보를 알 수 있다.
Promise.allSettled([p1, p2, p3])
  .then(results => console.table(results));
  
// 결과 예시
[
  { status: 'fulfilled', value: 1 },
  { status: 'rejected', reason: 'Error!' },
  { status: 'fulfilled', value: 2 },
]

 

Micro Task Queue

프로미스는 Task Queue 가 아니라 무조건 Micro Task Queue에 들어간다. 

따라서 Micro Task Queue에 프로미스가 쌓여있다면, 이게 무조건 우선 시 되기 때문에 Task Queue에 있는 것들은 대기하게 된다.

fetch

fetch(url[, options])는 브라우저나 Node.js(v18 이상)에서 제공하는 비동기 HTTP 요청 함수이며, 실행 시
Promise<Response> 객체를 반환한다.

 

fetch('https://jsonplaceholder.typicode.com/users/1')
  .then(response => {
    return response.json(); // 다시 Promise 반환
  })
  .then(data => {
    console.log(data); // 실제 JSON 데이터 출력
  })
  .catch(error => {
    console.error('에러 발생:', error);
  });

// fetch()는 Response 객체를 담은 Promise를 반환하고,
// json()은 그 Response를 파싱해 다시 Promise를 반환한다.

'Dev > Javascript' 카테고리의 다른 글

[JS] EventLoop  (0) 2025.05.22
[JS] 비동기 프로그래밍  (1) 2025.05.20
[JS] 배열 (Array)  (1) 2025.04.15
[JS] this관련 예제 파헤치기  (0) 2025.04.11
[JS] 객체와 프로퍼티  (0) 2025.04.10