DEVELOPMENT/JavaScript

[JS] 프로미스 (Promise)

Watnu 2025. 2. 20. 16:20



01


Promise

자바스크립트에서 비동기 작업이 끝날 때, 그 결과를 알려주는 "약속"과 같은 역할의 오브젝트입니다.

예를 들어, 어떤 일이 잘 처리되면 결과를 알려주고, 문제가 생기면 에러를 알려주는 방식이죠.

 

1. Promise의 흐름: 생성, 사용, 결과 처리

✅ Producing Code (프로듀서 코드)

: Promise생성하는 코드입니다.

즉, 우리가 약속을 만들고 그 약속이 끝날 때까지 기다리는 코드라고 할 수 있습니다.

이 코드는 어떤 일이 완료되기를 기다리고, 완료되면 결과를 알려줄 resolve 또는 에러를 알려줄 reject를 호출합니다.

 

✅ Consuming Code (컨슘 코드)

: Consuming CodePromise사용하는 코드입니다.

즉, Promise가 완료될 때까지 기다리고, 그 결과(성공 또는 실패)를 처리하는 코드입니다.

 

✅ Promise

: 위에서 말한 것처럼 "약속"과 같아서, 어떤 일이 끝날 때까지 기다리며 그 결과를 전달합니다.

Producing Code에서 Promise를 생성하고, Consuming Code에서 그 결과를 처리하는 방식입니다.

 

  Promise는 세 가지 상태를 가질 수 있습니다:

  • Pending: 아직 완료되지 않은 상태 (약속을 했지만 결과를 아직 모름)
  • Resolved (Fulfilled): 일이 성공적으로 완료된 상태
  • Rejected: 일이 실패한 상태

2. Promise 문법

new Promise는 executor라는 함수를 인자로 받습니다.

이 함수는 작업이 성공적으로 끝나면 resolve(value)를 호출하고, 실패하면 reject(error)를 호출합니다.

let promise = new Promise(function(resolve, reject) {
  // executor 
});

 

한편, new Promise 생성자가 반환하는 promise 객체는 다음과 같은 내부 프로퍼티를 갖습니다.

  • state: 'pending' → 'fulfilled' (성공 시) 또는 'rejected' (실패 시)
  • result: 'undefined' → 'value' (성공 시) 또는 'error' (실패 시)

 

 

3. Promise 예시

✅ promise 생성자와 간단한 executor 함수로 만든 성공예시입니다.

let promise = new Promise(function(resolve, reject) {
  // 프라미스가 만들어지면 executor 함수는 자동으로 실행됩니다.

  // 1초 뒤에 일이 성공적으로 끝났다는 신호가 전달되면서 result는 '완료'가 됩니다.
  setTimeout(() => resolve("완료"), 1000);
});

📂 설명:

  • state: "pending" → "fulfilled"
  • result: undefined → "완료"

 

이번에는 executor가 에러와 함께 약속한 작업을 거부하는 예시입니다.

let promise = new Promise(function(resolve, reject) {
  // 1초 뒤에 에러와 함께 실행이 종료되었다는 신호를 보냅니다.
  setTimeout(() => reject(new Error("에러 발생!")), 1000);
});

📂 설명:

  • 1초 후 reject(...)가 호출되면 promise의 상태가 "rejected"로 변합니다.

 

 

이전 포스팅해서 다뤘던 Callback 함수를 Promise로 바꿔보겠습니다

//Callback
function loadScript(src) {
  return new Promise(function(resolve, reject) {
    let script = document.createElement('script');
    script.src = src;

    script.onload = () => resolve(script);
    script.onerror = () => reject(new Error(`${src}를 불러오는 도중에 에러가 발생함`));

    document.head.append(script);
  });
}
//promise
let promise = loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js");

promise.then(
  script => alert(`${script.src}을 불러왔습니다!`),
  error => alert(`Error: ${error.message}`)
);

promise.then(script => alert('또다른 핸들러...'));


02


Consumers: then, catch, finally

Promise의 소비자(Consumer) 함수들은. then,. catch,. finally 메서드를 사용해 구독될 수 있습니다.

1. then

then은 Promise에서 가장 중요한 메서드로,

Promise가 해결되었을 때 결과를 다루거나, 에러가 발생했을 때 처리할 수 있습니다.

promise.then(
  function(result) { /* 결과(result)를 다룹니다 */ },
  function(error) { /* 에러(error)를 다룹니다 */ }
);

 

2. catch

에러만 다루고 싶다면. catch()를 사용합니다.. catch(f)는. then(null, f)와 동일하게 작동합니다.

let promise = new Promise((resolve, reject) => {
  setTimeout(() => reject(new Error("에러 발생!")), 1000);
});

promise.catch(alert);  // 1초 뒤 "Error: 에러 발생!" 출력

 

3. finally

finally는 try... catch에서 사용되던 것처럼, 결과와 상관없이 항상 마지막에 실행되는 코드입니다.

주로 로딩 인디케이터와 같은 정리 작업을 수행할 때 사용됩니다.

new Promise((resolve, reject) => {
  // 시간이 걸리는 작업을 수행한 뒤 resolve 또는 reject를 호출
})
  .finally(() => {
    // 로딩 인디케이터 중지
  })
  .then(result => {
    // 성공 결과 처리
  })
  .catch(error => {
    // 에러 처리
  });

 

4. 섞어서 사용 예시

then, catch, finally 3가지를 모두 한 번에 쓴다면 아래처럼 될 것입니다.

promise
  .then(result => console.log(result), // Promise가 해결되었을 때
    error => console.log(error) // Promise가 거부되었을 때
  )
  .catch(error => {
    console.log(error);
  })
  .finally(() => {
    // 마지막에 항상 실행됨
    // finally는 로딩 인디케이터와 같은 정리 작업을 수행할 때 좋음
    console.log('finally');
  });

 

 

처리된 프라미스의 핸들러는 즉각 실행됩니다.

 

프라미스가 대기 상태일 때,. then/catch/finally 핸들러는 프라미스가 처리되길 기다립니다.

반면, 프라미스가 이미 처리상태라면 핸들러가 즉각 실행됩니다.

// 아래 프라미스는 생성과 동시에 이행됩니다.
let promise = new Promise(resolve => resolve("완료!"));

promise.then(alert); // 완료! (바로 출력됨)


03


Promises chaining

Promise 체이닝은 여러 비동기 작업을 순차적으로 처리하기 위해 then() 메서드를 이어서 사용하는 방식입니다.

 

then()Promise가 해결된 후 실행되는 콜백 함수를 지정하는데, 여러 then()을 연결하여 여러 비동기 작업을 순차적으로 처리할 수 있습니다.

❌초보자는 프라미스 하나에. then을 여러 개 추가한 후, 이를 체이닝이라고 착각하는 경우가 있습니다. 하지만 이는 체이닝이 아닙니다.

 

then()은 항상 Promise를 반환하기 때문에, 각 작업을 순차적으로 처리하고, 후속 작업에 전달할 값을 반환할 수 있습니다.

 

 

✅ 이해를 돕기 위한 비유

Promise 체이닝을 설명할 때는 줄 서서 기다리는 것처럼 비유할 수 있습니다.

  1. 첫 번째 사람이 일을 끝내면, 두 번째 사람은 그 일을 이어서 하고,
  2. 두 번째 사람이 일을 끝내면, 세 번째 사람이 이어서 하는 방식입니다.

 

 예시

let promise = new Promise((resolve, reject) => {
  setTimeout(() => resolve("첫 번째 작업 완료"), 1000);
});

promise
  .then(result => {
    console.log(result);  // 첫 번째 작업이 끝난 후, "첫 번째 작업 완료" 출력
    return "두 번째 작업 완료";  // 두 번째 작업의 결과를 반환
  })
  .then(result => {
    console.log(result);  // 두 번째 작업이 끝난 후, "두 번째 작업 완료" 출력
    return "세 번째 작업 완료";  // 세 번째 작업의 결과를 반환
  })
  .then(result => {
    console.log(result);  // 세 번째 작업이 끝난 후, "세 번째 작업 완료" 출력
  });

 

 

⭐ Promise 체이닝에서 중요한 점

  • then()은 값을 반환하거나 다른 Promise를 반환할 수 있습니다. 이때 반환된 값은 다음 then()으로 전달됩니다.
  • 만약 then() 안에서 새로운 Promise를 반환하면, 그 Promise가 해결될 때까지 기다립니다.
new Promise((resolve, reject) => {
  setTimeout(() => resolve("첫 번째 작업 완료"), 1000);
})
  .then(result => {
    console.log(result);  // 1초 뒤 출력: "첫 번째 작업 완료"
    return new Promise(resolve => setTimeout(() => resolve("두 번째 작업 완료"), 1000));  // 두 번째 작업은 1초 대기 후 완료
  })
  .then(result => {
    console.log(result);  // 2초 뒤 출력: "두 번째 작업 완료"
    return "세 번째 작업 완료";
  })
  .then(result => {
    console.log(result);  // 출력: "세 번째 작업 완료"
  });


04


유용한 메서드

내용

 

1. Promise.all

Promise.all여러 개의 Promise를 병렬로 처리하고,

모든 Promise가 해결될 때까지 기다린 후 결과를 한 번에 처리할 수 있게 해주는 메서드입니다.

 

비유를 하자면 Promise.all은 여러 명이 줄을 서서 기다리는 것과 비슷합니다.

첫 번째 사람이 일을 끝내면 두 번째 사람이 일을 시작하고, 그다음 사람이 일을 시작하는 방식입니다.

모든 사람이 끝난 후에 결과를 함께 처리하는 방식입니다.

 

 

✅ Promise.all 사용법

Promise.all은 배열로 여러 Promise를 받아들이며, 모든 Promise가 성공적으로 완료될 때까지 기다립니다.

하나라도 실패하면 reject로 바로 종료됩니다.

let promise1 = new Promise((resolve, reject) => {
  setTimeout(() => resolve("첫 번째 작업 완료"), 1000);
});

let promise2 = new Promise((resolve, reject) => {
  setTimeout(() => resolve("두 번째 작업 완료"), 2000);
});

let promise3 = new Promise((resolve, reject) => {
  setTimeout(() => resolve("세 번째 작업 완료"), 3000);
});

Promise.all([promise1, promise2, promise3])
  .then(results => {
    console.log(results); // ["첫 번째 작업 완료", "두 번째 작업 완료", "세 번째 작업 완료"]
  })
  .catch(error => {
    console.error("Error:", error);
  });

 

📂 Promise.all 요약:

  • 배열로 전달된 모든 Promise가 해결될 때까지 기다립니다.
  • 모든 Promise가 성공적으로 해결되면, .then() 안에 전달된 results 배열로 모든 결과를 한 번에 받습니다.
  • 하나라도 실패하면 catch()로 처리됩니다. 실패한 Promise가 있으면 즉시 에러가 발생하고, 다른 Promise들은 계속해서 실행됩니다.

2. Promise.allSettled

Promise.allSettled는 모든 Promise가 끝날 때까지 기다리지만, 성공과 실패를 따로 다룹니다.

이 메서드는 모든 Promise의 결과배열로 반환하는데, 각 Promise의 상태(fulfilled 또는 rejected)를 명시적으로 확인할 수 있습니다.

let promise1 = new Promise((resolve, reject) => {
  setTimeout(() => resolve("첫 번째 작업 완료"), 1000);
});

let promise2 = new Promise((resolve, reject) => {
  setTimeout(() => reject("두 번째 작업 실패"), 2000);
});

let promise3 = new Promise((resolve, reject) => {
  setTimeout(() => resolve("세 번째 작업 완료"), 3000);
});

Promise.allSettled([promise1, promise2, promise3])
  .then(results => {
    console.log(results);
    /*
    [
      { status: "fulfilled", value: "첫 번째 작업 완료" },
      { status: "rejected", reason: "두 번째 작업 실패" },
      { status: "fulfilled", value: "세 번째 작업 완료" }
    ]
    */
  });

 

 

📂 Promise.allSettled 요약:

  • 모든 Promise가 완료된 후(성공 또는 실패) 그 결과를 배열로 반환합니다.
  • 각 결과 객체는 status와 value(성공 시) 또는 reason(실패 시) 속성을 가집니다.
  • 성공한 Promise는 status: "fulfilled", 실패한 Promise는 status: "rejected"로 나타납니다.

3. Promise.race

Promise.race는 여러 개의 Promise 중 가장 먼저 처리된 Promise기다리고 그 결과를 반환합니다.

다른 Promise들은 처리되지 않더라도 기다리지 않고 바로 결과를 처리합니다.

let promise1 = new Promise((resolve, reject) => {
  setTimeout(() => resolve("첫 번째 작업 완료"), 1000);
});

let promise2 = new Promise((resolve, reject) => {
  setTimeout(() => resolve("두 번째 작업 완료"), 500);
});

let promise3 = new Promise((resolve, reject) => {
  setTimeout(() => resolve("세 번째 작업 완료"), 2000);
});

Promise.race([promise1, promise2, promise3])
  .then(result => {
    console.log(result); // "두 번째 작업 완료"
  })
  .catch(error => {
    console.error("Error:", error);
  });

 

📂 Promise.race 요약:

  • 가장 먼저 완료된 Promise의 결과만을 반환합니다.
  • 첫 번째로 해결된 Promise가 성공하면 그 값을 반환하고, 실패하면 catch로 오류를 처리합니다.
  • 다른 Promise들은 무시되고, 첫 번째로 해결된 Promise의 결과만 처리합니다.

4. Promise.any

Promise.any는 다수의 Promise첫 번째로 성공한 Promise의 결과를 반환합니다.

만약 모든 Promise가 실패하면, 마지막에 AggregateError를 반환합니다.

let promise1 = new Promise((resolve, reject) => {
  setTimeout(() => reject("첫 번째 작업 실패"), 1000);
});

let promise2 = new Promise((resolve, reject) => {
  setTimeout(() => resolve("두 번째 작업 완료"), 500);
});

let promise3 = new Promise((resolve, reject) => {
  setTimeout(() => reject("세 번째 작업 실패"), 2000);
});

Promise.any([promise1, promise2, promise3])
  .then(result => {
    console.log(result); // "두 번째 작업 완료"
  })
  .catch(error => {
    console.error("Error:", error);
  });


📂 Promise.any 요약:

  • 여러 Promise 중 하나라도 성공하면 그 값을 반환합니다.
  • 모든 Promise가 실패하면 AggregateError가 발생하고, 이를 catch로 처리합니다.


05


마무리

Promise 체이닝을 통해 여러 비동기 작업을 순차적으로 처리할 수 있으며, then(), catch(), finally()를 이용해 결과를 처리하고, 에러를 관리할 수 있습니다. 이를 통해 코드가 더 깔끔하고 직관적으로 작성될 수 있습니다.

 

또한, Promise.all, Promise.allSettled, Promise.race, Promise.any와 같은 다양한 메서드를 활용하면 여러 비동기 작업을 효율적으로 처리할 수 있습니다. 각 메서드는 특정 상황에 따라 유용하게 사용될 수 있습니다.

 

  • Promise.all: 모든 작업이 끝날 때까지 기다린 후, 모든 결과를 처리합니다.
  • Promise.allSettled: 모든 작업이 완료된 후, 각각의 결과를 반환합니다.
  • Promise.race: 가장 먼저 해결된 작업을 처리합니다.
  • Promise.any가장 먼저 성공한 작업을 처리합니다.

 

이들 메서드를 적절히 활용하면, 비동기 작업을 병렬로 처리하거나, 가장 빠른 작업을 우선적으로 처리하는 등의 다양한 방식으로 유용하게 활용할 수 있습니다. 이처럼 Promise는 비동기 작업을 효율적이고 관리하기 쉬운 방식으로 처리할 수 있게 도와주는 중요한 도구입니다.