[자바스크립트] Promise (feat. all, allSettled, race)

Woonil·2025년 5월 19일
0

자바스크립트

목록 보기
4/8
post-thumbnail

자바스크립트의 Promise 는 비동기 작업을 관리하고, 해당 작업의 성공 또는 실패 결과를 나중에 사용할 수 있도록 하는 객체이다. 자바스크립트는 비동기 처리를 위해 콜백 함수를 사용하는데, 이는 콜백이 계속하여 중첩되는 ‘콜백 지옥’ 현상이 나타날 수 있다. 이때, Promise 는 비동기 작업의 동기적 표현을 가능하게 하여 비동기 처리의 가독성을 높이고, 코드 흐름을 명확하게 관리하여 콜백함수 대신 유용하게 쓰일 수 있다.

// 프로미스 선언
const promise = new Promise((resolve, reject)=> {
  console.log('doing something...');
  setTimeout(()=> {
    resolve('wonil');
  }, 3000);
});

// then, catch, finally
promise
  .then(value=> {
    console.log(value);  // then은 resolve가 전달한 값을 받음
  }) // promise를 반환
  .catch(error=> { 
    console.log(error);  // catch는 reject가 전달한 값을 받음
  });
  .finally(()=>{
    console.log('finally'); // 성공여부와 관계없이 출력
  });

🤔개념

Promise 개념을 알기 위해서는, 데이터의 제공자와 소비자의 다른 견해를 파악해야 한다. executor는 제공자 역할을, then, catch, finally 메서드를 사용하는 함수는 소비자 역할을 한다. 각각 성공과 실패를 나타내는 콜백 함수인 resolvereject가 제공하는 결과를 담은 promise 객체, 그리고 해당 결과를 소비하는 then, catch, finally 를 이해해보자.

Producer

executer

resolve 및 reject 인수를 전달할 실행함수로, promise 구현에 의해 두 콜백 함수 resolve 또는 reject를 받아 즉시 실행한다. new Promise 생성 시 자동으로 실행된다.

  • resolve: 일이 성공적으로 끝난(비동기 작업을 시작한 후 모든 작업을 끝난) 경우, 그 결과를 나타내는 value 와 함께 호출되어 프로미스를 이행하는 함수
  • reject: 에러가 발생한 경우, 에러 객체를 나타내는 error 와 함께 호출되는 함수이다. 프로미스는 거부되고 실행함수의 반환값은 무시하며, 보통 Error(자바스크립트에서 제공하는 오브젝트 중 하나)라는 오브젝트를 통해 값을 전달한다.

resolve나 reject는 인수를 하나만 받고(혹은 아무것도 받지 않음) 그 이외의 인수는 무시

promise

new Promise 생성자가 반환하는 promise 객체는 producer(executor)와 consumer(try, catch, finally 를 사용하는 함수)를 이어주는 역할을 하며, 다음과 같은 내부 프로퍼티를 가진다.

Promise는 동기

new 연산자와 함께 호출한 Promise의 인자로 넘겨주는 콜백 함수는 호출할 때 바로 실행되지만, 그 내부에 resolve 또는 reject 를 호출하는 구문이 있을 경우 둘 중 하나가 실행되기 전까지는 다음(then) 또는 오류 구문(catch)로 넘어가지 않는다.

[출처: 코어자바스크립트]

  • state: 프로세스 기능의 성공 또는 실패의 상태로, promise는 프로세스의 동작 수행 중에는 pending(보류)상태였다가 성공이나 실패에 따라 각각 fulfilledrejected 상태가 된다.
  • result: 처음에는 undefined 였다가 resolve 가 호출되면 value, reject 가 호출되면 error 로 변한다.

[출처: 코어자바스크립트]

promise는 성공 또는 실패만 하므로, resolvereject 중 하나를 반드시 호출해야 한다. 이때, 변경된 상태는 더 이상 변하지 않는다. 따라서 처리가 끝난 promise에 resolvereject를 호출하면 무시된다.

stateresult 는 내부 프로퍼티([[PromiseState]])이므로 개발자가 직접 접근할 수 없다.

then, catch, finally 로 접근 가능

Consumer

then

두 개의 인수를 받으며, 작업이 성공적으로 처리된 경우만 다루고 싶다면 .then에 인수를 하나만 전달한다.

  • 첫 번째 인수: promise가 이행되었을 때 실행되는 함수이고, 여기서 실행 결과를 받는다.
    let promise = new Promise(function(resolve, reject) {
      setTimeout(() => resolve("완료!"), 1000);
    });
    
    // resolve 함수는 .then의 첫 번째 함수(인수)를 실행합니다.
    promise.then(
      result => alert(result), // 1초 후 "완료!"를 출력
      error => alert(error) // 실행되지 않음
    );
  • 두 번째 인수: promise가 거부되었을 때 실행되는 함수이고, 여기서는 에러를 받는다.
    let promise = new Promise(function(resolve, reject) {
      setTimeout(() => reject(new Error("에러 발생!")), 1000);
    });
    
    // reject 함수는 .then의 두 번째 함수를 실행합니다.
    promise.then(
      result => alert(result), // 실행되지 않음
      error => alert(error) // 1초 후 "Error: 에러 발생!"을 출력
    );

catch

에러가 발생한 경우만 다루고 싶다면 .then(null, errorHandlingFunction)같이 null을 첫 번째 인수로 전달한다. .catch(errorHandlingFunction)를 써도 되는데, .catch는 .then 의 첫 번째 인자로 null을 전달하는 것과 동일하게 작동한다.

finally

성공 및 실패와 상관없이 마지막에 실행되며, 아무런 변수를 받지 않는다.

Promise 반환

// 함수에 프로미스 기능 추가-> 리턴값으로 프로미스 지정
function myAsyncFunction(url) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest()
    xhr.open("GET", url)
    xhr.onload = () => resolve(xhr.responseText)
    xhr.onerror = () => reject(xhr.statusText)
    xhr.send()
  });
}

체이닝

promise.then 을 호출하면 promise가 반환된다. 반환된 promise를 통해 또다시 then 을 호출한다. 핸들러는 값 또는 promise를 반환할 수 있다. 값은 promise의 result가 될 수 있으며, result가 핸들러 체인을 따라 전달된다. (체이닝은 결과를 반환하지 않으므로, return으로 반환값을 명시해주어야 한다.)

promise
  .then(value=> {
    console.log(value);
  }) // promise를 반환
 // 반환된 promise에 .catch를 등록 
 .catch(error=> { 
    console.log(error);
  });

const fetchNumber= new Promise((resolve, reject)=>{
  setTimeout(()=> resolve(1), 1000);
});

fetchNumber
  .then(num => num * 2) // 1->2
  .then(num => num * 3) // 2->6
  .then(num => {      
    return new Promise((resolve, reject)=> {
      setTimeout(() => resolve(num - 1), 1000); // 6->5 
    });
  })
  .then(num=> console.log(num)); // 5 (then은 값이나 promise를 전달)

정적메서드

promise의 메서드를 사용하여 promise의 병렬적 처리를 수행할 수도 있다.

all()

promise 결과가 모두 필요할 때 사용할 수 있는 메서드로, 한마디로 ‘모 아니면 도’ 방식으로 수행하는 것이다. 여러 개의 promise를 동시에 실행시키고, 모든 promise가 준비될 때까지 기다릴 때 사용된다. promise인 요소를 가진 배열(대개 배열을 받으며, 엄밀히 따지면 이터러블 객체)을 인자로 받고 새로운 promise를 반환한다.

let promise = Promise.all([...promises...]);

  • 특징
    • promise의 병렬적 처리가 가능
    • 결과 배열은 전달되는 promise 순서와 상응
    • 전달되는 모든 promise 작업이 이행될 때까지 대기
    • 전달되는 promise 중 하나라도 거부되면, Promise.all 전체가 거부된다. ⇒ 거부 에러는 Promise.all 전체의 결과가 된다.

      에러 발생 시 promise 무시

      특정 promise의 실패 여부와 관계없이 다른 프로미스 실행은 계속 진행되며, 진행된 promise는 처리만 될 뿐이지 그 결과는 무시된다. 즉, ‘취소’라는 개념이 없다.

allSettled()

주어진 프로미스를 이행하거나 거부한 후, 각 프로미스에 대한 결과를 나타내는 객체 배열을 반환한다. 여러 요청 중 하나가 실패해도 다른 요청 결과는 필요할 때 사용하며, 각 promise의 상태와 값 또는 에러를 받을 수 있다.

  • 반환 배열 요소
    • 응답 성공: {status:"fulfilled", value:result}
    • 에러 발생: {status:"rejected", reason:error}

race()

Promise.race() 는 한마디로 promise들의 경주 중 1등만이 필요할 때 사용할 수 있는 메서드라고 할 수 있다. 이 메서드는 Promise.all 과 비슷하지만 가장 먼저 처리(처리 결과는 성공이든 실패이든 상관없음)되는 promise의 결과를 반환한다. 따라서 가장 먼저 처리되는 promise가 나타날 시, 다른 promise의 결과 또는 에러는 무시된다.

let promise = Promise.race(iterable);

😎실습

처리하지 못한 에러를 부탁해,,

axios를 사용하는 프로젝트에서 인터셉터를 사용하여 접근 처리 등을 수행할 수 있다. 이때, 에러 응답에 대한 모든 처리를 해주었음에도 처리되지 못한 에러가 있을 수 있다. 이때는 에러를 호출부까지 전파해야 한다(호출부의 catch 블럭에서 에러를 잡는다). 이때, 에러를 던지기 위해서 거절된 상태(rejected)의 Promise를 명시적으로 반환한다.

// INTERCEPTOR: Axios 요청, 응답을 가로채어 중간처리를 하는 객체
import axios, { AxiosError, AxiosResponse } from "axios";
import { IServerErrorResponse } from "../interface/error-interface";
import isServerError from "../error/is-server-error";

// 응답 처리
axiosInstance.interceptors.response.use(
  (response: AxiosResponse) => response,
  (error) => {
    if (!isServerError(error)) {
      console.error("알 수 없는 에러가 발생했습니다. 관리자에게 문의해주세요.");
      return Promise.reject(error);
    }

    // error는 AxiosError<IServerErrorResponse> 타입
    const { status, data } =
      error.response as AxiosResponse<IServerErrorResponse>;

    if (error.response) {
      switch (status) {
        case 401:
          // 401: UNAUTHORIZED
          if (data.message === "Unauthorized") {
            window.location.href = "/";
          }
          break;
        default:
          // 기타 상태 코드 처리 로직
          break;
      }
    }
    return Promise.reject(error);
  }
);

연대책임? 버릴 놈은 버려야지..

동아리 홈페이지 프로젝트 중 사진 게시판에서 리사이징을 적용하는 리팩토링 수행 중이었다. 사용자가 업로드한 이미지 모두에 대해 비동기로 수행되는 리사이징을 적용하려는데, 어느 하나가 실패하더라도 전체가 실패하면 사용자 경험에 좋지 못할 것이다. 따라서, Promise.all이 아닌 Promise.allSettled를 사용하여 성공이든 실패이든 결과 배열을 반환하려고 한다.

🧐마무리

Promise가 비동기 작업의 가독성을 높이는 면에서 콜백 지옥을 어느정도 해결해주지만, 여전히 단점이 존재한다.

첫째, 복잡한 에러 처리이다. 가령, allSettled() 의 프로젝트 예시에서 실제로는 filePromises 를 감싸는 바깥 try...catch 도 존재한다. 해당 try...catch 는 파일 리사이징과는 다른 오류를 잡는 데에 사용된다. 이렇듯 특정 단계에서만 발생하는 에러를 세밀하게 다루기에는 코드 복잡성 면에서 한계를 보인다.

둘째, 콜백 지옥을 완전히 해결하지 못한다는 점이다. 비동기 작업이 복잡하게 중첩되면, 여전히 콜백과 유사하게 then() 메서드가 연속하여 등장하며 이는 가독성을 떨어뜨릴 수 있다. 이는 이후에 등장하는 개념인 async/await 를 통해 개선할 수 있다.

참고 자료
코어 자바스크립트 - 프라미스
Promise - JavaScript | MDN
Promise.all() - JavaScript | MDN
Promise.allSettled() - JavaScript | MDN
Promise.race() - JavaScript | MDN

profile
프론트 개발과 클라우드 환경에 관심이 많습니다:)

0개의 댓글