JS - Promise

sarang_daddy·2023년 2월 26일
0

Javascript

목록 보기
16/26
post-thumbnail

앞서 비동기 학습에서 JS는 비동기 처리를 위한 방법 중 하나로 콜백 함수를 사용한다고 했다.
하지만 콜백 패턴은 콜백 헬로 인해 가독성이 나쁘고 비동기 처리 중 발생한 에러의 수정도 어렵기에
여러개의 비동기를 처리하기에 어려움이 많다.

ES6에서는 이를 개선하기 위해 Promise 패턴을 도입했다.
Promise로 콜백 패턴의 단점을 보완하며 비동기 처리 시점을 명확하게 표현 할 수 있다.

먼저 콜백의 의미를 다시 정리하자.

비동기 함수란 함수 내부에 비동기로 동작하는 코드를 포함한 함수를 말한다.
비동기 함수는 실행되면 내부의 비동기로 동작하는 코드가 완료되지 않아도 기다리지 않고 종료된다. 비동기 함수 내부의 비동기로 동작하는 코드는 비동기 함수가 종료된 이후에 완료된다. 즉, 비동기 함수 내부의 비동기로 동작하는 코드는 처리 결과를 외부로 반환하거나 상위 스코프로 할당이 되지 않는다.

때문에 비동기 함수의 처리 결과(내부의 비동기로 동작하는 코드)에 대한 후속 처리는 비동기 함수 내부에서 수행되어야 한다. (후속 처리가 없으면 비동기 함수는 완료되어버린다.)

비동기 함수는 내부에서 비동기로 동작하는 코드의 결과를 처리해야 한다.
그래서 내부의 비동기 동작으로 동작하는 함수를 비동기 함수에게 콜백 한다.


콜백 헬(Callback hell)

콜백 함수를 통해 비동기 함수 내부의 비동기 동작으로 움직이는 코드의 처리 결과를 한번만 처리한다면 문제가 없지만, 이 결과를 가지고 또 다시 비동기 함수를 호출해야 한다면 그 비동기 함수 내부의 콜백함수가 다시 호출되어 중첩이 발생하며 이는 코드를 복잡하게 만든다. 이를 콜백 헬이라 한다.

// 유저정보를 가지고 있는 유저창고 클래스가 있다고 하자.
class UserStroage {
  
  // 유저창고는 로그인 메서드를 가지며
  loginUser(id, password, onSuccess, onError) {
    // 로그인 메서드는 setTimeout 비동기 함수로 성공여부를 콜백한다.
    setTimeout(() => {
      if (
        (id === "kim" && password === "123") ||
        (id === "lee" && password === "456")
      ) {
        onSuccess(id);
      } else {
        onError(new Error("not found"));
      }
    }, 2000);
  }

  // 유저창고는 사용자 정보를 확인하는 메서드를 가지며
  getRoles(user, onSuccess, onError) {
    // 정보 확인 메서드는 비동기 함수로 사용자 정보를 콜백한다.
    setTimeout(() => {
      if (user === "kim") {
        onSuccess({ name: "kim", role: "admin" });
      } else {
        onError(new Error("no access"));
      }
    }, 1000);
  }
}

const userStroage = new UserStroage();

const id = prompt("enter your id");
const password = prompt("enter your password");

// 로그인을 시도하면 로그인 메서드 내부의 콜백으로 성공 여부를 반환하고,
userStroage.loginUser(
  id,
  password,
  (user) => {
    // 성공 시 유저 이름을 가지고 콜백을 함수를 가진 다른 메서드 또 실행되고,
    userStroage.getRoles(
      user,
      // 반복된 콜백들의 반환 값으로 화면에 호출된다.
      (userWithRole) => {
        alert(`hi ${userWithRole.name}, ${userWithRole.role}`);
      },
      (error) => {
        console.log(error);
      }
    );
  },
  (error) => {
    console.log(error);
  }
);

콜백 헬은 가독성이 매우 좋지 않으며, 에러 처리에 어려움을 준다.


Promise 생성

콜백함수로의 비동기를 보완하고자 도입된 Promise는 생성자 함수로 사용된다.

Promise 생성자 함수는 비동기 처리를 수행할 콜백 함수를 resolve와 reject로 전달 받는다.

const promise = new Promise((resolve, reject) => {
    if (/* 비동기 처리 성공 */) {
        resolve('결과')
    } else { /* 비동기 처리 실패 */
        reject('실패 이유')
    }
})

비동기 처리가 성공하면 resolve 함수를 호출하고 실패하면 reject 함수를 호출한다.

앞서 학습 했던 콜백함수의 의미를 다시 보자.

비동기 함수는 내부에서 비동기로 동작하는 코드의 결과를 처리해야 한다.
그래서내부의 비동기 동작으로 동작하는 함수를 비동기 함수에게 콜백 한다.

비동기 함수는 콜백 받은 결과를 함수 내부에서 처리해야한다.


then, catch, finally

Promise 내부에서 실행된 콜백함수의 결과를 처리하는 메서드로
then, catch, fainally가 존재한다.

promise
  .then((value) => {
    console.log(value);
  })
  .catch((error) => {
    console.log(error);
  })
  .finally(() => {
    console.log("finally");
  });

// resolve(성공) or reject(실패)로 호출된 값을 then 메서드로 처리한다.
// reject(실패된)로 호출된 값을 catch 메서드로 처리한다.
// resolve(성공), reject(실패)와 상관없이 finally로 처리한다.

then은 값을 바로 전달 할 수도 있고, 또 다른 promise를 전달 할 수도 있다.

fetchNumber
  .then((num) => num * 2)
  .then((num) => num * 3)
  .then((num) => {
    return new Promise((resolve, reject) => {
      setTimeout(() => resolve(num - 1), 1000);
    });
  })
  .then((num) => console.log(num));

  // 5 

이처럼 then을 여러번 묶어서 처리하는 것을 프로미스 체이닝이라 한다.

catch를 이용한 에러처리

const step1 = () =>
  new Promise((reslove, reject) => {
    setTimeout(() => reslove(1), 1000);
  });

const step2 = (step1) =>
  new Promise((resolve, reject) => {
    setTimeout(() => resolve(`${step1} => 2`), 1000);
  });

const step3 = (step2) =>
  new Promise((resolve, reject) => {
    setTimeout(() => resolve(`${step2} => 3`), 1000);
  });

step1()
  .then((step1) => step2(step1))
  .then((step2) => step3(step2))
  .then((finish) => console.log(finish));

// 1 => 2 => 3

// 스텝이 문제 없이 진행된다면 괜찮지만, 
// 스텝2에서 에러가 발생했다고 생각한다면,

const step2 = (step1) =>
  new Promise((resolve, reject) => {
    setTimeout(() => reject(new Error(`${step1} => 2`)), 1000);
  });

// Uncaught (in promise) Error: error! 1 => 2
// 에러를 잡지 못했다는 메시지가 발생된다.

// 여기서 catch를 사용한다면, 에러를 잡을 수 있다.

step1()
  .then((step1) => step2(step1))
  .then((step2) => step3(step2))
  .then((finish) => console.log(finish))
  .catch((error) => console.log(error));

// Error: error! 1 => 2

에러를 잡지 못한것과 에러를 잡았다는 상황에서는 둘 다 에러가 발생했음은 동일하지만 "에러를 잡으면 대체 방법을 추가할 수 있다"

step1()
  .then((step1) => step2(step1))
  .catch((error) => {
    return `keep step2`;
  })
  .then((step2) => step3(step2))
  .then((finish) => console.log(finish))
  .catch((error) => console.log(error));

// keep step2 => 3

콜백 헬의 예제를 Promise로 바꿔보자.

class UserStroage {
  loginUser(id, password) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        if (
          (id === "kim" && password === "123") ||
          (id === "lee" && password === "456")
        ) {
          resolve(id);
        } else {
          reject(new Error("not found"));
        }
      }, 2000);
    });
  }

  getRoles(user) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        if (user === "kim") {
          resolve({ name: "kim", role: "admin" });
        } else {
          reject(new Error("no access"));
        }
      }, 1000);
    });
  }
}

const userStroage = new UserStroage();
const id = prompt("enter your id");
const password = prompt("enter your password");

userStroage
  .loginUser(id, password)
  .then(userStroage.getRoles)
  .then((user) => alert(`hi ${user.name}, ${user.role}`))
  .catch(console.log);

Promise를 사용함으로서 콜백 함수만을 이용한 비동기 처리보다 가독성이 좋아지고 에러 처리 또한 가능하게 개선되었다.
하지만, Promise 또한 콜백 헬이 없을 뿐 콜백함수를 반복함에는 변함이 없다.


async/await

async/await는 프로미스를 기반으로 동작한다.
여기서 후속처리를 담당했던 then/catch/finally의 사용없이
동기 처리 처럼 프로미스가 처리 결과를 반환 하도록 구현할 수 있게 한다.

async

async 함수는 async키워드를 사용해 정의하며 언제나 프로미스를 반환한다.
(async 함수는 암묵적으로 반환값을 resolve하는 프로미스를 반환한다.)

// async 사용 예시

// 함수선언문
async function foo(n) {
  return n;
}

// 함수 표현식
const bar = async function (n) {
  return n;
};

// 화살표 함수
const baz = async (n) => n;

// async 메서드
const obj = {
  async foo(n) {
    return n;
  },
};

// 클래스 메서드
class Myclass {
  async bar(n) {
    return n;
  }
}

await

await 키워드는 프로미스가 settled 상태가 될 때까지 대기하다가
settled 상태가 되면 프로미스가 resolve한 처리 결과를 반화한다.

// async와 await 사용 예시
function delay(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function getApple() {
  await delay(1000);
  return "🍎";
}

async function getBanana() {
  await delay(1000);
  return "🍌";
}

// 반복되는 콜백 함수들을
// function pickFruits() {
//   return getApple().then((apple) => {
//     return getBanana().then((banana) => `${apple} + ${banana}`);
//   });
// }

// 동기적으로 보이게끔 (비동기로 작동) 해준다.
async function pickFruits() {
  const apple = await getApple();
  const banana = await getBanana();
  return `${apple} + ${banana}`;
}

pickFruits().then(console.log);

try ~ catch를 사용한 에러처리

function delay(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

// error 발생한다면
async function getApple() {
  await delay(1000);
  throw 'error'
  return "🍎";
}

async function getBanana() {
  await delay(1000);
  return "🍌";
}

// try ~ catch 를 통한 에러처리
async function pickFruits() {
  try {
    const apple = await getApple();
    const banana = await getBanana();
  } catch (error) {}
  return `${apple} + ${banana}`;
}

pickFruits().then(console.log);

Promise.all (병렬처리)

위 예제에서는 사과를 따는데 1초 바나나를 따는데 1초로
총 2초 후 사과와 바나나가 호출된다.
하지만 사과와 바나나는 별개의 과일로 1초후 두 과일이 모두 따져야 한다.

이럴때는 콜백함수들을 병렬적으로 처리해주는 Promise.all() 사용한다.

function pickFruits() {
  return Promise.all([getApple(), getBanana()]).then((fruits) =>
    fruits.join(" + ")
  );
}

Promise.race (먼저 실행된 콜백만 호출)

여러 콜백함수들 중 가장 먼저 실행되는 결과값만을 반환할때 사용한다.

async function getApple() {
  await delay(2000);
  return "🍎";
}

async function getBanana() {
  await delay(1000);
  return "🍌";
}

function pickOnlyOne() {
  return Promise.race([getApple(), getBanana()]);
}

// 바나나만 나온다.

참고자료

모던 자바스크립트 Deep Dive
드림코딩 유부트 강의

profile
한 발자국, 한 걸음 느리더라도 하루하루 발전하는 삶을 살자.

0개의 댓글