[JS] 프로미스

박시은·2023년 5월 28일
0

JavaScript

목록 보기
33/58
post-thumbnail

콜백함수 》에 대해 안다는 전제 하에 포스팅합니다.
⚠️ 프로미스는 어려운게 당연한것이니 좌절하지 말자


▶ 프로미스

▷ 프로미스 개념

  • 프로미스란, 비동기 처리에 대해, 처리가 끝나면 알려달라는 ‘약속’이다.

    • new 연산자로 호출한 Promise의 인자로 넘어가는 콜백은 바로 실행된다.
    • 그 내부의 resolve(또는 reject) 함수를 호출하는 구문이 있을 경우 resolve(또는 reject) 둘 중 하나가 실행되기 전까지는 다음(then), 오류(catch)로 넘어가지 않는다.
    • 따라서, 비동기작업이 완료될 때 비로소 resolve(성공), reject(실패) 호출되게 된다.
  • 즉, 프로미스를 사용하여 비동기 작업의 동기적 표현을 구현할 수 있는 것이다.


▷ 프로미스의 state(상태)

  • 대기(Pending): 프로미스가 생성된 초기 상태

  • 이행(Fulfilled): 프로미스가 성공적으로 완료된 상태이며 성공 리턴 값(resolve) 전달

  • 거부(Rejected): 프로미스가 실패한 상태이며 실패 리턴 값(reject) 전달


▷ 프로미스 생성

Producer (정보 제공)

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

  • 프로미스 생성자는 콜백 함수를 매개변수로 받는다. 이 콜백 함수는 두 개의 매개변수(resolve, reject)를 가진다.
  • 이행단계일때 resolve를 통해 성공 리턴 값을 호출하며, 거부단계일 때 reject를 통해 실패 리턴 값을 호출한다.
const promise = new Promise((resolve, reject) => {
  // 무거운 일들을 실행한다.
});

보통 프로미스 안에서 무거운 일들을 실행한다.

  • 네트워크에서 데이터를 받아오거나 파일에서 큰 데이터를 읽어오는 과정은 시간이 많이 걸린다.
  • 만약 동기적으로 이 과정들을 처리하게 되면 파일을 읽어오고 네트워크에서 데이터를 받아오는 동안 아무 동작도 수행할 수 없게 된다.
  • 따라서 시간이 걸리는 일들은 프로미스를 만들어서 비동기적으로 처리하는 것이 좋다.

프로미스를 출력해보자

const promise = new Promise((resolve, reject) => {
  console.log("출력"); // 출력
});

위 코드를 통해 알 수 있는 사실은 프로미스를 만드는 순간 우리가 전달한 executor라는 콜백 함수가 바로 실행되는 것을 확인할 수 있다.

  • 위와 같은 식으로 작성하게 되면 사용자가 버튼을 눌렀을 때 네트워크 요청을 해야하는 경우 사용자가 요구하지도 않았는데 콜백 함수가 바로 실행이 되기때문에 문제가 발생할 수 있다.

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

const promise = new Promise((resolve, reject) => {
  console.log("출력");
  setTimeout(() => {
    resolve("sieun");
  }, 2000);
});

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




▶ 프로미스 사용하기

Consumers(정보 이용)은 then, catch, finally를 이용해 값을 받아올 수 있다.

▷ then

Resolve(성공리턴값)호출 -> then으로 연결

then을 사용하여 프로미스가 정상적으로 완료가 되면 마지막에 최종적으로 resolve라는 콜백함수를 통해 전달한 값("sieun")이 value에 파라미터로 전달되어져서 들어오는 것을 볼 수 있다.

// Producer
const promise = new Promise((resolve, reject) => {
  console.log("출력");
  setTimeout(() => {
    resolve("sieun");
  }, 2000);
});

// Consumers : then, catch, finally
promise.then((value) => {
  console.log(value); // (3초 뒤) "sieun"
});




▷ catch

Reject(실패리턴값)호출 -> catch로 연결

reject는 Error라는 object를 통해서 값을 전달한다. 어떤 에러가 발생했는지 이유를 잘 명시해서 작성해줘야한다.

// Producer
const promise = new Promise((resolve, reject) => {
  console.log("출력");
  setTimeout(() => {
    //resolve("sieun");
    reject(new Error("네트워크 오류"));
  }, 2000);
});

// Consumers : then, catch, finally
promise.then((value) => {
  console.log(value);
});

"출력" 실행 후 3초 뒤 네트워크 오류가 발생하는 것을 알 수 있다.


catch를 사용해서 에러가 발생했을 때 어떻게 처리할 것인지 콜백함수를 등록해보자

// Consumers : then, catch, finally
promise
  .then((value) => {
    console.log(value);
  })
  .catch((error) => {
    console.log(error);
  });

더 이상 에러가 발생하지 않고 우리가 받아온 에러가 console.log에 출력되는 것을 볼 수 있다.


위 과정은 Promise 체이닝을 통해 여러 개의 비동기 작업을 순차적으로 실행하고 결과를 처리한다.

  • then호출 -> 프로미스 리턴 -> 리턴된 프로미스의 catch 등록

▷ 정리!

// Producer
const promise = new Promise((resolve, reject) => {
  console.log("출력");
  setTimeout(() => {
    //resolve("sieun");
    reject(new Error("네트워크 오류"));
  }, 2000);
});

// Consumers : then, catch, finally
promise
  .then((value) => {
    console.log(value);
  })
  .catch((error) => {
    console.log(error);
  });
  • 프로미스 오브젝트를 만들 때, 비동기적으로 수행하고 싶은 작업을 Producer 부분에 작성하고 나서,
  • 성공적으로 실행이 됐다면 resolve를 통해 성공 리턴 값을 호출, 실패했다면 reject를 통해 실패 이유가 담긴 리턴값을 호출한다.
  • 나중에 Consumers 부분에 프로미스를 이용해서 then과 catch를 이용해 성공한 값, 실패한 에러를 받아와서 원하는 방식으로 처리하는 것이다.

▷ Finally

Finally 부분은 성공, 실패하던 상관없이 무조건 마지막에 호출된다.

// Producer
const promise = new Promise((resolve, reject) => {
  console.log("출력");
  setTimeout(() => {
    resolve("sieun");
    //reject(new Error("네트워크 오류"));
  }, 2000);
});

// Consumers : then, catch, finally
promise
  .then((value) => {
    console.log(value);
  })
  .catch((error) => {
    console.log(error);
  })

// finally 부분 추가로 작성
  .finally(() => {
    console.log("finally 호출");
  });

"출력" 호출 후, 2초 뒤 resolve를 통해 성공 리턴 값인 "sieun" 호출한 뒤에 finall() 호출


즉, 성공하던 실패하던 상관 없이 어떤 기능을 마지막으로 수행하고 싶을 때 finall를 사용하는 것이다.




▶ 프로미스 연결 (Promise chaining)

프로미스를 연결할 때 Promise chaining을 사용한다.

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

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));

fetchNumber Promise는 비동기적으로 1초 후에 숫자 2를 반환하고, 이후의 then 메서드 체인에서 각각의 비동기 작업을 순차적으로 처리하여 최종 결과를 출력한다. 자세한 과정은 아래와 같다.

  • 1초 있다가 숫자 2를 전달하는 프로미스를 생성한다.

  • 그 후 프로미스가 정상적으로 실행 되면 첫 번째 then 메서드에서는 이전에 해결(resolve)된 숫자를 가져와 2를 곱한다.

  • 두 번째 then 메서드에서는 이전에 해결된 숫자를 가져와 3을 곱한다.

  • 세 번째 then 메서드에서는 이전에 해결된 숫자를 가져와, 1을 뺀 값을 담은 새로운 Promise 객체를 생성한다. (then은 값을 바로 전달해도 되고 다른 비동기인 프로미스를 전달해도 됨)

  • 마지막 then 메서드에서는 최종적으로 해결된 숫자를 가져와서 출력한다.


▷ 프로미스를 체이닝 오류 처리

프로미스를 체이닝했을 때 어떻게 오류를 해결하까? 예시를 통해 살펴보자


아래 코드는 3가지의 프로미스를 리턴한다.

1초 뒤 암탉 리턴 -> 암탉으로부터 받은 달걀 1초뒤 리턴 -> 달걀을 받아 1초뒤 후라이 리턴

const getHen = () =>
  new Promise((resolve, reject) => {
    setTimeout(() => resolve("🐔", 1000));
  });

const getEgg = (hen) =>
  new Promise((resolve, reject) => {
    setTimeout(() => resolve(`${hen}=>🥚`, 1000));
  });

const cook = (egg) =>
  new Promise((resolve, reject) => {
    setTimeout(() => resolve(`${egg}=>🍳`, 1000));
  });

위 코드를 사용하여 서버에서 닭, 달걀, 요리까지 해보자!!!

getHen() // (가독성을 이유로 여기에 주석을 쓴다.)
  .then((hen) => getEgg(hen))
  .then((egg) => cook(egg))
  .then((meal) => console.log(meal));
  • getHen() 함수를 호출 후, 그 결과인 hen을 인자로 getEgg(hen) 함수를 호출하고, 다시 그 결과인 egg를 인자로 cook(egg) 함수를 호출
  • 마지막으로 cook(egg) 함수의 결과인 meal이 console.log를 통해 출력(프로미스 체인 값 반환
    )

콜백함수를 전달할 때 받아오는 값을 다른 함수로 바로 호출하는 경우 생략이 가능하다. (이해x)

getHen() //
  .then(getEgg)
  .then(cook)
  .then(console.log);


만약 댤걀을 받아올 때 네트워크에 문제가 생겨 실패가 되면 어떻게 처리해야할까?

실패시 new Error object 이용

const getEgg = (hen) =>
  new Promise((resolve, reject) => {
    setTimeout(() => reject(new Error(`error! ${hen}=>🥚`)), 1000);
  });

위와 같이 에러 처리를 해주면 아래와 같이 출력이 된다.

어떤 에러 핸들링도 하지 않았기 때문에 에러가 발생한다.


getHen() //
  .then(getEgg)
  .then(cook)
  .then(console.log)
  .catch(console.log);

따라서 마지막에 .catch() 메서드를 사용하여 프로미스 체인에서 발생하는 오류를 처리하였다.

프로미스 체인에서 한 단계에서라도 오류가 발생하면, 해당 오류는 체인의 다음 .catch() 메서드로 전달된다.

이를 통해 오류를 적절하게 처리할 수 있는 것이다.


.catch() 메서드를 사용해 달걀을 받아올 때 문제가 생긴다면 다른 재료로 대처하는 에러 핸들링을 처리해보자

getHen() //
  .then(getEgg)
  .catch(error=>{
    return'🧙🏻';
  })
  .then(cook)
  .then(console.log)
  .catch(console.log);

계란을 받아오는 것에 문제가 생겨도 전체적인 프로미스 체인에 문제가 발생하지 않도록 처리를 해주었다.


➡️ 이렇게 에러를 처리하고 싶을 떄 .catch() 메서드를 사용하여 바로바로 문제를 해결할 수 있다.




▶ 코드 리팩토링

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

  • 콜백함수
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(function (resolve) {
	setTimeout(function () {
		var name = '에스프레소';
		console.log(name);
		resolve(name);
	}, 500);
}).then(function (prevName) {
	return new Promise(function (resolve) {
		setTimeout(function () {
			var name = prevName + ', 아메리카노';
			console.log(name);
			resolve(name);
		}, 500);
	});
}).then(function (prevName) {
	return new Promise(function (resolve) {
		setTimeout(function () {
			var name = prevName + ', 카페모카';
			console.log(name);
			resolve(name);
		}, 500);
	});
}).then(function (prevName) {
	return new Promise(function (resolve) {
		setTimeout(function () {
			var name = prevName + ', 카페라떼';
			console.log(name);
			resolve(name);
		}, 500);
	});
});

  • 위 코드의 반복적인 로직을 함수화 시켜보자
var addCoffee = function (name) {
  // 변수 이름을 name으로 받음
  return function (prevName) {
    return new Promise(function (resolve) {
      setTimeout(function () {
        // 백틱
        var nameName = prevName ? `${prevName}, ${name}` : name;
        console.log(name);
        resolve(nameName);
      }, 500);
    });
  };
};
addCoffee("에스프레소")()
  .then(addCoffee("아메리카노"))
  .then(addCoffee("카페모카"))
  .then(addCoffee("카페라떼"));



📎참조

profile
블로그 이전했습니다!

0개의 댓글