7 분 소요


[콜백함수↗️] 에 이어 작성하는 글입니다.


1. 동기와 비동기

  • 동기
    • 작업이 순차적으로 실행된다. 한 작업이 끝나야 다음 작업이 시작된다.
    • 즉, 동기적 처리는 기다리는 동안 아무 것도 할 수 없다. (멈춰있는 화면)

  • 비동기
    • 작업이 동시에 실행될 수 있다. 한 작업이 끝나기를 기다리지 않고 다음 작업을 진행할 수 있다.
    • 아래 그림처럼 응답이 오지 않았음에도 요청을 동시 다발적으로 할 수 있다.



비동기 함수의 동기적 표현이 왜 필요할까?

  1. 외부 서버로부터 날씨 데이터를 가져와 화면에 출력하는 경우

    • 상황: 외부 서버에서 데이터를 가져오는 작업은 시간이 걸릴 수 있다.
    • 필요성: 데이터를 가져오는 작업은 비동기적으로 처리하지만, 데이터를 가져온 후 화면에 출력하는 작업은 순차적으로(동기적으로) 처리되어야 한다.
    • 정리: 날씨 데이터를 가져오는 비동기 작업이 완료된 후 화면에 데이터를 출력하는 작업이 순차적으로 이루어져야 하므로, 비동기 작업을 동기적으로 표현할 필요가 있다.
  2. 신용카드 이용 내역에 대한 메일 알림 서비스 신청

    • 상황: 알림 서비스 신청 후 실제 메일 발송까지 시간이 걸린다(예: 5분 이상).
    • 필요성: 사용자가 신청을 완료했다는 즉각적인 피드백이 필요하지만, 메일 발송은 시간이 걸리므로 비동기적으로 처리된다.
    • 정리: 사용자가 신청 후 빠른 응답(“신청이 완료됐습니다.”)을 받을 수 있도록 비동기 작업의 결과를 기다리지 않고 즉시 응답을 제공해야 한다.
  3. 여러 외부 정보 사이트에서 데이터를 읽어와 내부 데이터베이스에 저장하는 경우

    • 상황: 여러 사이트에서 데이터를 읽어오는 작업은 동시에 비동기적으로 수행된다.
    • 필요성: 각 비동기 작업이 완료된 후에 데이터를 내부 데이터베이스에 저장하는 작업은 순차적으로(동기적으로) 이루어져야 한다.
    • 정리: 모든 외부 데이터가 준비된 후 동기적으로 데이터베이스에 저장하여 데이터를 일괄적으로 처리하기 위해 비동기 작업을 동기적 표현으로 처리한다.



2. 프로미스 개요

2.1 프로미스 개념

Promise는 비동기 작업에 대한 “약속”이다.

  • 🤙비동기 처리가 끝나면 알려줘! 그러면(then) 내가 성공(resolve) 또는 실패(reject) 객체를 반환(return) 해줄게!
  • new Promise로 생성된 프로미스는 인자로 전달된 콜백 함수를 즉시 실행한다.
    • 프로미스의 내부에서 resolve 또는 reject가 호출하는 구문이 있을 경우, 이 호출이 이루어진 후에 프로미스는 다음 단계(.then() 또는 .catch())로 넘어간다.
    • .then()으로 성공했을 때, .catch()로 실패했을 때 콜백을 등록한다.
  • 비동기 처리 결과(resolve(성공), reject(실패))와, 진행 상태(pending(대기), filfilled(이행), rejected(실패)) 상태를 가진다.


➡️ 즉, 프로미스를 사용하여 비동기 작업의 동기적 표현을 구현할 수 있는 것이다.


2.2 프로미스 state

Pending하고 나면 Fulfilled 또는 Rejected가 발생한다.

상태 (State) 설명
대기 (Pending) 프로미스가 생성된 초기 상태, 비동기 처리 로직이 완료되지 않은 상태.
이행 (Fulfilled) 프로미스가 성공적으로 완료된 상태이며 성공 리턴 값(resolve)이 전달됨.
거부 (Rejected) 프로미스가 실패한 상태이며 실패 리턴 값(reject)이 전달됨.


2.3 Promise 메서드

“then”, “catch”, “finally”를 사용하여 프로미스 객체의 성공, 실패, 완료 상태를 처리하며 데이터를 소비할 수 있다.

메소드 설명
then 프로미스가 성공(resolve) 상태일 때 실행할 콜백 함수 등록
catch 프로미스 체인에서 발생한 에러를 처리하는 콜백 함수 등록
finally 프로미스 처리 완료시 항상 실행되는 로직을 정의하는 블록
  • Resolve(성공리턴값)호출 -> then으로 연결
  • Reject(실패리턴값)호출 -> catch로 연결
    • reject는 Error라는 object를 통해서 값을 전달한다. 어떤 에러가 발생했는지 이유를 잘 명시해서 작성해줘야한다.
  • Finally는 성공, 실패하던 상관없이 무조건 마지막에 호출된다.



3. 프로미스 사용하기

3.1 프로미스 생성

  • 프로미스를 만드는 사람(Producer)은 기존에 존재하지 않는 새로운 비동기 작업을 정의할 때 new Promise를 사용한다.
  • 프로미스를 이용하는 사람(consumer)은 이미 존재하는 프로미스(자신이 만들었거나 다른 사람이 만든)를 사용할 때 async/await을 사용한다.


프로미스는 클래스이기 때문에 new 라는 키워드를 사용해서 object를 생성할 수 있다.

new Promise : 프로미스 객체를 생성할 때 사용되는 구문이다. 이때 인자로 콜백 함수를 받는다. 이 콜백 함수는 resolve와 reject라는 두 개의 인자를 가진다.

const promise = new Promise();


프로미스 생성자는 콜백 함수를 매개변수로 받는다. 이 콜백 함수는 두 개의 매개변수(resolve, reject)를 가진다

  • 프로미스를 만드는 순간 우리가 전달한 executor라는 콜백 함수가 바로 실행된다.
  • 이행단계일때 resolve를 통해 성공 리턴 값을 호출하며, 거부단계일 때 reject를 통해 실패 리턴 값을 호출한다.
const promise = new Promise((resolve, reject) => {
  // 무거운 일들을 실행한다.
});
  • resolve
    • 비동기 작업이 성공적으로 완료되었을 때 호출되는 함수 -> .then()로 연결
  • reject
    • 비동기 작업이 실패했을 때 호출되는 함수 -> .catch로 연결


3.2 프로미스 사용

settimeout()을 이용해 원하는 콜백 함수를 1초 뒤에 실행시켜보자

resolve라는 콜백 함수를 호출하여 기능이 잘 수행됐을 때 “성공”을 호출하게 하였다.

// Promise 생성
const myPromise = new Promise((resolve, reject) => {
  // 비동기 작업을 수행
  setTimeout(() => {
    const success = true;

    if (success) {
      resolve("Promise was successful!");
    } else {
      reject("Promise failed.");
    }
  }, 1000);
});

// Promise 사용
myPromise
  .then((message) => {
    console.log(message); // 성공했을 때 호출: "Promise was successful!"
  })
  .catch((error) => {
    console.error(error); // 실패했을 때 호출: "Promise failed."
  })
  .finally(() => {
    console.log("Promise 작업이 완료되었습니다."); // 항상 실행: 성공, 실패와 무관하게 호출
  });


delay 함수를 이용해 1초 후 실행시켜보자

프로미스는 지정된 시간(ms)이 지난 후에 완료된다.

function delay(ms) {
  // 새로운 프로미스를 생성하여 반환
  return new Promise((resolve) => setTimeout(resolve, ms));
}

delay(1000).then(() => console.log("1초 후 실행"));



3. Promise chaining

프로미스를 연결할 때 Promise chaining을 사용하며, 각 Promise의 결과를 다음 Promise로 전달하며 작업을 이어나갈 수 있다.

// 1초 후에 숫자를 반환하는 Promise 생성
function waitAndReturnNumber(number) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(`Returning number: ${number}`);
      resolve(number);
    }, 1000);
  });
}

// Promise chaining
waitAndReturnNumber(1)
  .then((number) => {
    // 첫 번째 Promise가 성공적으로 완료된 후 호출됨
    return waitAndReturnNumber(number + 1); // 2를 반환하는 Promise
  })
  .then((number) => {
    // 두 번째 Promise가 성공적으로 완료된 후 호출됨
    return waitAndReturnNumber(number + 1); // 3을 반환하는 Promise
  })
  .then((number) => {
    // 세 번째 Promise가 성공적으로 완료된 후 호출됨
    return waitAndReturnNumber(number + 1); // 4를 반환하는 Promise
  })
  .then((number) => {
    // 마지막 Promise가 성공적으로 완료된 후 최종 값 출력
    console.log(`Final number: ${number}`); // 최종 결과: 4
  })
  .catch((error) => {
    // Promise 중 하나라도 실패하면 호출됨
    console.error(`Error occurred: ${error}`);
  })
  .finally(() => {
    // 성공 여부와 상관없이 항상 실행
    console.log("Promise chaining complete.");
  });

➡️ Promise Chaining은 비동기 작업을 순차적으로 처리할 때 유용하지만, 작업이 많아지면 코드가 복잡해지고 가독성이 떨어질 수 있다. [async와 await↗️]를 사용하면 이런 문제를 해결할 수 있다.



4. Promise와 병렬처리

4.1 Promise.all

Promise.all은 Promise를 병렬로 처리할 수 있게 해준다.

아래코드는 이 코드는 각 fetch 호출이 완료될 때까지 차례대로 기다린다. 따라서 첫 번째 요청이 완료되기 전까지는 두 번째 요청이 시작되지 않으며, 이로 인해 전체 작업 완료 시간이 길어지게 된다.

async function fetchPostsSequentially() {
  try {
    const response1 = await fetch(
      "https://jsonplaceholder.typicode.com/posts/1"
    );
    const post1 = await response1.json();
    const response2 = await fetch(
      "https://jsonplaceholder.typicode.com/posts/2"
    );
    const post2 = await response2.json();
    const response3 = await fetch(
      "https://jsonplaceholder.typicode.com/posts/3"
    );
    const post3 = await response3.json();

    console.log([post1, post2, post3]); // 3개의 게시물 데이터를 순차적으로 처리 후 출력
  } catch (error) {
    console.error("Error:", error);
  }
}

fetchPostsSequentially();


Promise.all을 사용해서 병렬 처리를 해보자!

병렬 처리는 각 요청이 독립적으로 이루어지며, 모든 요청이 동시에 시작되어 각각 완료되는 즉시 다음 단계로 진행할 수 있다.

Promise.all([
  fetch("https://jsonplaceholder.typicode.com/posts/1").then((response) =>
    response.json()
  ),
  fetch("https://jsonplaceholder.typicode.com/posts/2").then((response) =>
    response.json()
  ),
  fetch("https://jsonplaceholder.typicode.com/posts/3").then((response) =>
    response.json()
  ),
])
  // 모든 Promise가 성공적으로 완료되었을 때 실행됨
  .then((posts) => {
    console.log(posts); // 3개의 게시물 데이터가 배열로 반환
  })
  .catch((error) => {
    // 하나라도 실패하면 여기서 에러를 처리
    console.error("Error:", error);
  });

Promise.all은 모든 Promise가 성공적으로 완료되어야만 결과를 반환하지만, 한 개의 Promise가 실패하면 전체 작업이 실패로 간주된다. 따라서 여러 서버 중 하나가 다운되면 Promise.all에 전달된 모든 Promise가 실패하게 되어, 전체 작업이 실패하게 된다.


4.2 Promise.allSettle

Promise.allSettled는 모든 Promise의 결과와 오류 정보를 담은 객체들을 배열로 반환한다.

즉, Promise.all과 다르게 배열을 순회하면서 각 Promise의 상태에 따라 처리할 수 있다.
이는 하나의 Promise가 실패하더라도 다른 Promise들에 영향을 미치지 않고 각 요청의 성공 또는 실패 여부를 확인하여 적절한 처리를 할 수 있다.

Promise.all([
  fetch("https://jsonplaceholder.typicode.com/posts/1"),
  fetch("https://this-url-does-not-exist.typicode.com/posts/999"), // 의도적으로 실패하도록 설정
  fetch("https://jsonplaceholder.typicode.com/posts/3"),
])
  .then((results) => {
    results.forEach((result, index) => {
      if (result.status === "fulfilled") {
        console.log(`요청 #${index + 1} 성공:`, result.value);
        result.value.json().then((data) => console.log(data));
      } else if (result.status === "rejected") {
        console.error(`요청 #${index + 1} 실패:`, result.reason);
      }
    });
  })
  .catch((error) => {
    console.error("실패했습니다", error);
  })
  .finally(() => {
    console.log("------------------------");
  });

Promise.allSettled([
  fetch("https://jsonplaceholder.typicode.com/posts/1"),
  fetch("https://this-url-does-not-exist.typicode.com/posts/999"), // 의도적으로 실패하도록 설정
  fetch("https://jsonplaceholder.typicode.com/posts/3"),
]).then((results) => {
  results.forEach((result, index) => {
    if (result.status === "fulfilled") {
      console.log(`요청 #${index + 1} 성공:`, result.value);
      result.value.json().then((data) => console.log(data));
    } else if (result.status === "rejected") {
      console.error(`요청 #${index + 1} 실패:`, result.reason);
    }
  });
});


정리

  Promise.all Promise.allSettled
동작 방식 여러 프로미스를 병렬로 실행하고 모든 프로미스가 성공적으로 완료될 때까지 기다립니다. 모든 프로미스가 fulfilled 상태가 되면, 결과값들의 배열을 반환합니다. 여러 프로미스를 병렬로 실행하고, 모든 프로미스가 완료될 때까지 기다립니다. 여기서 “완료”는 성공(fulfilled)이든 실패(rejected)이든 상관없습니다. 각 프로미스의 결과를 나타내는 객체 배열을 반환합니다.
실패 처리 만약 주어진 프로미스 중 하나라도 rejected 상태가 되면, Promise.all은 즉시 rejected 상태가 됩니다. 이때 첫 번째 발생한 에러가 전체 Promise.all의 에러로 반환됩니다. 반환되는 각 객체는 status 속성(값은 fulfilled 또는 rejected)과, 성공한 경우 value, 실패한 경우 reason 속성을 가집니다.
반환 값 성공한 경우, 결과값들의 배열 반환 / 실패한 경우, 첫 번째 에러 반환 결과 및 오류 정보를 담은 객체들의 배열 반환
주 사용 사례 모든 작업이 성공적으로 완료되어야 하며, 어느 하나라도 실패하면 전체가 실패하는 것으로 간주될 때 사용합니다. 여러 작업의 결과가 각각 독립적일 때 사용하며, 성공과 실패를 각각 처리해야 할 필요가 있을 때 유용합니다.
  • Promise.all: 모든 프로미스가 성공해야 하며, 하나라도 실패하면 전체가 실패한 것으로 간주한다.
  • Promise.allSettled: 모든 프로미스의 성공/실패 여부에 상관없이 모든 결과를 받아 각각을 개별적으로 처리한다.



5. 코드 리팩토링 연습

이전에 했던 예시인 콜백함수를 프로미스로 변경해보자!

콜백함수

setTimeout(
  function (name) {
    var coffeeList = name;
    console.log(coffeeList);

    setTimeout(
      function (name) {
        coffeeList += ", " + name;
        console.log(coffeeList);

        setTimeout(
          function (name) {
            coffeeList += ", " + name;
            console.log(coffeeList);

            setTimeout(
              function (name) {
                coffeeList += ", " + name;
                console.log(coffeeList);
              },
              500,
              "카페라떼"
            );
          },
          500,
          "카페모카"
        );
      },
      500,
      "아메리카노"
    );
  },
  500,
  "에스프레소"
);


프로미스로 변경해보자

new Promise((resolve) => {
  setTimeout(() => {
    const name = "에스프레소";
    console.log(name);
    resolve(name);
  }, 500);
})
  .then(
    (prevName) =>
      new Promise((resolve) => {
        setTimeout(() => {
          const name = `${prevName}, 아메리카노`;
          console.log(name);
          resolve(name);
        }, 500);
      })
  )
  .then(
    (prevName) =>
      new Promise((resolve) => {
        setTimeout(() => {
          const name = `${prevName}, 카페모카`;
          console.log(name);
          resolve(name);
        }, 500);
      })
  )
  .then(
    (prevName) =>
      new Promise((resolve) => {
        setTimeout(() => {
          const name = `${prevName}, 카페라떼`;
          console.log(name);
          resolve(name);
        }, 500);
      })
  );


위 코드의 반복적인 로직을 함수화 시켜보자

const addCoffee = (name) => (prevName) =>
  new Promise((resolve) => {
    setTimeout(() => {
      const nameName = prevName ? `${prevName}, ${name}` : name;
      console.log(nameName);
      resolve(nameName);
    }, 500);
  });

// 커피 이름을 순차적으로 추가
addCoffee("에스프레소")()
  .then(addCoffee("아메리카노"))
  .then(addCoffee("카페모카"))
  .then(addCoffee("카페라떼"));



6. 참조

  • https://www.youtube.com/watch?v=JB_yU6Oe2eE&t=508s


댓글남기기