[javascript] Promise(ft. async, await)

Yuni·2022년 7월 27일
0

코드스테이츠

목록 보기
22/39
post-thumbnail

이 전에 배운 콜백 함수는 비동기 처리를 할 때 콜백 지옥에 빠질 수 있다는 단점이 있었다. 콜백 지옥을 해결하기 위해 각 동작을 독립적인 함수로 만들어도 코드의 가독성이 떨어지고 재사용이 불가능하다는 단점이 생겼다.

ES6에서는 비동기 처리를 위한 또 다른 방법으로 프로미스(Promise)를 도입했다. 프로미스는 콜백이 가진 단점을 보완하면서 비동기 처리 시점을 명확하게 표현할 수 있다는 장점이 있다.

Promise

프로미스는 자바스크립트 비동기 처리에 사용되는 객체이다.

아래 코드에 new Promise에 전달되는 함수는 executor(실행 함수)라고 부른다. new Promise가 만들어질 때 자동으로 실행되며 결과를 최종적으로 만들어내는 제작 코드를 포함한다.

let promise = new Promise(function(resolve, reject){
	// executor
    // 원하는 일 처리!
})

executor의 인수 resolve와 reject는 자바스크립트에서 자체 제공하는 콜백이다. executor의 처리가 끝나면 처리 성공 여부에 따라 resolve나 reject를 호출한다.
👉 콜백 중 하나는 반드시 호출해야한다.

  • resolve(value) : 일이 성공적으로 끝난 경우 그 결과를 나타내는 value와 함께 호출
  • reject(error) : 에러 발생 시 에러 객체를 나타내는 error와 함께 호출

📌 아래 코드처럼 resolve(data)의 data나 return한 값은 then에서 파라미터로 쓸 수 있다!

return new Promise((resolve, rejects) => {
    fs.readFile(filePath, "utf8", (err, data) => {
      if(err){
        rejects(err);
      }else{
        resolve(data);
      }
    })
});

Promise 객체의 내부 속성

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

  • state : 처음에는 pending(대기)이었다가 resolve가 호출되면 fulfilled(이행), reject가 호출되면 rejected(실패)로 변한다.
  • result : 처음에는 undefined이었다가 resolve(value)가 호출되면 value로, reject(error)가 호출되면 error로 변한다.

👇 예시

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

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

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

then, catch

then

then의 리턴값은 프로미스 객체이다.

  • .then의 첫 번째 인수는 프라미스가 이행되었을 때 실행되는 함수이고 여기서 실행 결과를 받는다.
  • .then의 두 번째 인수는 프라미스가 거부되었을 때 실행되는 함수이고 여기서 에러를 받는다.
promise.then(
  function(result) { /* 결과(result)를 다룬다 */ },
  function(error) { /* 에러(error)를 다룬다 */ }
);

프로미스가 성공적으로 이행됐을 경우

let promise = new Promise(function(resolve, reject) {
  setTimeout(() => resolve("완료!"), 1000);
});

// resolve 함수는 .then의 첫 번째 함수(인수)를 실행한다.
promise.then(
  result => alert(result), // 1초 후 "완료!"를 출력
  error => alert(error) // 실행되지 않음
);

프로미스가 거부된 경우

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

// reject 함수는 .then의 두 번째 함수를 실행한다.
promise.then(
  result => alert(result), // 실행되지 않음
  error => alert(error) // 1초 후 "Error: 에러 발생!"을 출력
);

작업이 성공적으로 처리된 경우만 다루고 싶다면

let promise = new Promise(resolve => {
  setTimeout(() => resolve("완료!"), 1000);
});

promise.then(alert); // 1초 뒤 "완료!" 출력

catch

.catch는 프라미스에서 발생한 모든 에러를 다룬다. reject()가 호출되거나 에러가 던져지면 .catch에서 이를 처리한다.

에러가 발생한 경우만 다루고 싶다면 .then(null, errorHandlingFunction)같이 null을 첫 번째 인수로 전달하거나 .catch(errorHandlingFunction)를 쓰면 된다.

👉 .catch.then에 null을 전달하는 것과 동일하게 작동한다.

💡 참고
reject됐을 때 catch가 없으면 상태는 rejected, catch를 쓰면 fulfilled가 된다.

프로미스 체이닝(Promise Chaining)

프라미스 체이닝은 result가 .then 핸들러의 체인(사슬)을 통해 전달된다는 점에서 착안한 아이디어이다. 아래 코드는 다음과 같은 순서로 실행된다.

  • 1초 후 최초 프라미스가 이행
  • 이후 첫번째 .then 핸들러가 호출
  • 2에서 반환한 값은 다음 .then 핸들러에 전달
  • 위 과정을 반복
new Promise(function(resolve, reject) {
  setTimeout(() => resolve(1), 1000); 

}).then(function(result) { 
  
  alert(result); // 1
  return result * 2;

}).then(function(result) { 

  alert(result); // 2
  return result * 2;

}).then(function(result) {

  alert(result); // 4
  return result * 2;

});

프라미스 체이닝이 가능한 이유는 promise.then을 호출하면 프라미스가 반환되기 때문이다. 반환된 프라미스엔 당연히 .then을 호출할 수 있다.

핸들러(.then(handler) / then의 전달인자)가 값을 반환하면 이 값이 프로미스의 result가 된다.

async와 await

async와 await를 사용하면 프라미스를 좀 더 편하게 사용할 수 있다.

async 함수

function 앞에 async를 붙이면 해당 함수는 항상 프로미스를 반환한다. 프로미스가 아닌 것은 프로미스로 감싸서 반환한다.

아래 코드를 실행하면 result가 1인 이행 프로미스를 반환한다.

async function f() {
  return 1;
}

f().then(alert); // 1

await

await는 async 함수 안에서만 동작한다. 자바스크립트는 await 키워드를 만나면 프로미스가 처리될 때까지 기다리고 결과는 그 이후 반환된다.

프라미스가 처리되길 기다리는 동안엔 엔진이 다른 일(다른 스크립트를 실행, 이벤트 처리 등)을 할 수 있기 때문에, CPU 리소스가 낭비되지 않는다.

async function f() {

  let promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve("완료!"), 1000)
  });

  let result = await promise; // 프라미스가 이행될 때까지 기다림

  alert(result); // "완료!"
}

f();

🚨 주의
일반 함수에는 await를 사용할 수 없다. async 함수에만 사용해야 한다.

에러 핸들링

프라미스가 정상적으로 이행되면 await promise는 프로미스 객체의 result에 저장된 값을 반환한다. 반면 프로미스가 거부되면 마치 throw문을 작성한 것처럼 에러가 던져진다.

아래 두 개의 코드는 동일하게 작동한다.

async function f() {
  await Promise.reject(new Error("에러 발생!"));
}

async function f() {
  throw new Error("에러 발생!");
}

await가 던진 에러는 throw가 던진 에러를 잡을 때처럼 try..catch를 사용해 잡을 수 있다.

async function f() {

  try {
    let response = await fetch('http://유효하지-않은-주소');
  } catch(err) {
    alert(err); // TypeError: failed to fetch
  }
}

f();

try..catch가 없으면 아래 예시의 async 함수 f()를 호출해 만든 프로미스가 거부 상태가 됩니다. f()에 .catch를 추가하면 거부된 프라미스를 처리할 수 있다.

참고

javascript.info 프로미스

javascript.info 프로미스 체이닝

자바스크립트 Promise 쉽게 이해하기

javascript.info async와 await

profile
배운 것을 기억하기 위해 기록합니다 😎

0개의 댓글