HyunJun 기술 블로그

Callback,Promise, Async await 본문

JavaScript

Callback,Promise, Async await

공부 좋아 2023. 7. 14. 09:24
728x90
반응형

1. Callback

1초 뒤 실행 후, 2초 뒤 실행 후, 3초 뒤 실행 후, 4초 뒤 실행 후, 5초 뒤 실행이라는 코드를 구현하고 싶을 때 만약 아래와 같이 코드를 작성한다면, 비동기 함수의 특징으로 인해, setTimeout이 거의 동시간에 모두 실행되고 1초 간격으로 a~e가 뜰 것이다.

  setTimeout(() => {
    console.log("a");
  }, 1000);

  setTimeout(() => {
    console.log("b");
  }, 2000);

  setTimeout(() => {
    console.log("c");
  }, 3000);

  setTimeout(() => {
    console.log("d");
  }, 4000);

  setTimeout(() => {
    console.log("e");
  }, 5000);

 

1) 콜백 지옥 (Callback Hell)

그래서 서론에 말했던 1초 후, 2초 후, 3초 후, 4초 후, 5초 후 ...라는 로직을 실행하려면 아래처럼 콜백 함수를 계속 활용해야 하는데 이런 식으로 구현을 하면, 동작에는 문제가 없지만 가독성이 좋지 않기 때문에 코드가 길어질뿐더러, 유지 보수에도 좋지 않다.

  setTimeout(() => {
    console.log("a");
    setTimeout(() => {
      console.log("b");
      setTimeout(() => {
        console.log("c");
        setTimeout(() => {
          console.log("d");
          setTimeout(() => {
            console.log("e");
          }, 5000);
        }, 4000);
      }, 3000);
    }, 2000);
  }, 1000);

 

응용 방법으로는 아래와 같은 방법이 있다. 코드가 길어지므로 a, b, c 3개만 표현해 보자면 아래와 같이. 응용할 수 있다.

  const a = () => {
    setTimeout(() => {
      console.log("a");
      b();
    }, 1000);
  };

  const b = () => {
    setTimeout(() => {
      console.log("b");
      c();
    }, 2000);
  };

  const c = () => {
    setTimeout(() => {
      console.log("c");
    }, 3000);
  };
  a();

하지만 위의 코드는 원하는 결과는 당장 낼 수 있겠지만, 재사용성이 좋지 않고 time 및 콜백 함수 설정을 자유자재로 하지 못한다.

 

그래서 함수의 인자 값으로, time과 callback을 넘겨 아래처럼 활용할 수 있다.

  const a = (time, callback) => {
    setTimeout(() => {
      console.log("a");
      callback();
    }, time);
  };

  const b = (time, callback) => {
    setTimeout(() => {
      console.log("b");
      callback();
    }, time);
  };

  const c = (time, callback) => {
    setTimeout(() => {
      console.log("c");
      callback();
    }, time);
  };

  a(1000, () => {
    b(2000, () => {
      c(3000, () => {});
    });
  });

위처럼 코드를 짜면 아래와 같이 time과 콜백을 자유자재로 변경해도 원하는 대로 동작한다.

  c(3000, () => {
    b(2000, () => {
      a(1000, () => {});
    });
  });

 

인자 값으로 콜백 함수 값을 넘길 때, 함수에 대한 값만 넘기지 매개변수 인자 값까지는 넘기지 못한다.

const a = (num) => {
        console.log(num);
      };
      
      
// document.addEventListener("DOMContentLoaded", a(123)); 이건 값을 넘기는 게 아니라 호출하는 것

 

그래서 b처럼 함수 안에서 호출한 값을 넘기거나, 아니면 바로 익명 함수의 안에서 호출한 값을 넘겨서 활용한다.

  const a = (num) => {
    console.log(num);
  };

  const b = () => {
    a(123);
  };

  document.addEventListener("DOMContentLoaded", b);
  document.addEventListener("DOMContentLoaded", () => {
    a(456);
  });

 

2. Promise

Promise 객체는 아래처럼 new Promise로 매개변수의 인자 값으로 함수를 넣어 생성한다.

  const pr = new Promise((resolve, reject) => {});
  console.dir(pr);

위처럼 아무런 설정이 되어 있지 않은 기본 Promise 객체는 state: pending, result: undefined 상태이다.

 

기본적인 Promise의 사용법은 Promise 객체에 콜백 함수와 매개 변수로, resolve와 reject를 넘겨줌으로써 구현할 수 있다.

  const pr = new Promise((resolve, reject) => {
    resolve("성공")
  });
  console.dir(pr);

결과를 확인해 보면, state: fulfilled, result: "성공"의 상태를 가진 객체가 나오는 걸 확인할 수 있다.

그 이유는, return 되고 있는 Promise 객체의 콜백 함수에는 resolve()와 reject()를 사용하게 되는데, resolve()에는 결과가 성공했을 때의 값을 넣고, reject에는 실패했을 때의 값을 넣으면 된다. 하여 현재는 resolve() 즉 성공했을 때의 값에만 성공을 넣었을 뿐인 객체를 리턴하고 있다.

 

  const pr = new Promise((resolve, reject) => {
    reject("실패")
  });
  console.dir(pr);

console.dir(pr);로 인해 Promise 객체가 나온 것이고, Uncaught.... 에러는 따로 에러 처리를 하지 않아 reject로 인해 발생하게 된다.

 

 

즉 Promise 객체는 아래와 같이 상태와, 결과 값을 가지고 해당 객체를 활용하는 메서드가 있는 형태를 가진다.

  • state
    • pending || fulfilled || rejected
  • result
    • undefined || "성공" || "실패" || ........
  • method
    • catch, finally, then

 

즉 Promise 객체의 초기 상태는 무조건 pending 상태이고, resolve 함수가 호출되면 fulfilled 상태가 되고, reject 함수가 호출되면 rejected 상태가 된다. 이때 Promise 객체 생성 자체 동기적으로 생성되나, 해당 객체를 활용하기 위한 메서드들은 무조건 자바스크립트 런타임에서 백그라운드에서 동작하게 되어있다. 즉 Promise 객체의 result를 활용하기 위해서는 백그라운드에서 조작하는 전용 메서드를 활용해야 한다.

 

1) then

Promise 객체의 state가 fulfilled 상태일 때만 실행되는 함수이다.

 

Promise 객체 내에서 성공 값을 뽑아내고 싶을 때에는 .then을 아래와 같은 형식으로 사용하면 된다.

  const pr = new Promise((resolve, reject) => {
    resolve("성공");
  });
  console.log("1");
  pr.then((result) => {
    console.log(result);
  });
  console.log("3");

then은 비동기적으로 활용되는 것을 확인할 수 있다.

2) catch

Promise 객체의 state가 rejected 상태일 때만 실행되는 함수이다.

 

그렇다면 위와 같은 코드로 reject 즉, 실패 결과값만 담아보자.

  const pr = new Promise((resolve, reject) => {
    // resolve("성공");
    reject("실패");
  });

  pr.then((result) => {
    console.log(result);
  });

아래와 같이 에러가 발생하는 걸 볼 수 있는데, 이는 result를 찍어서 보이는 것이 아니라, Promise 객체 내에서 reject() 값이 포함되어 있기에 나오는 것이다.

즉 then과 상관없이 아래처럼 Promise 객체 자체에 reject에 결과가 있다면 위 같은 에러를 뿜어내게 된다.

  new Promise((resolve, reject) => {
    reject("실패");
  });

 

그렇다면 에러를 확인은 하지만 프로그램에 이상이 없도록 결과값만 받으려면 어떻게 해야 할까? 아래처럼 catch를 사용해 주면 된다.

  const pr = new Promise((resolve, reject) => {
    // resolve("성공");
    reject("실패");
  });

  pr.catch((error) => {
    console.log(error);
  });

 

즉, then은 resolve()의 결과를 받아 처리하는 로직을 작성하는 부분이고, catch는 reject()의 결과를 받아 처리하는 로직을 작성하는 부분이다.

 

 

3) finally

finally는 Promise 객체의 상태가 fulfilled가 되거나 rejected가 되면, then, catch 상관없이 마지막으로 실행할 코드를 작성한다.

  const pr = new Promise((resolve, reject) => {
    resolve("!");
  });

  pr.then(() => {
    console.log("then()");
  }).finally(() => {
    console.log("finally()");
  });

pending 상태에서는 동작하지 않는다.

4) try, catch, finally

그렇다면 fulfilled 상태 혹은, rejected 상태를 구현하기 위해서 어떻게 활용해야 할까? 만약 아래처럼 단순하게 resolve, reject를 작성한다면, 먼저 읽히는 resolve 값만 넘어오게 된다.

  const pr = new Promise((resolve, reject) => {
    resolve("성공");
    reject("실패");
  });

  pr.catch((error) => {
    console.log(error);
  }).then((result) => {
    console.log(result);
  });

즉, 아래와 같은 결과가 발생한다. (.catch, .then 순서 상관없이 resolve, reject의 순서)

 

 

이를 제대로 활용하려면 자바스크립트 문법인 try, catch, finally를 활용해야 한다. 

 function promise(input) {
    return new Promise((resolve, reject) => {
      try {
        if (input == true) {
          resolve("성공");
        } else if (input == false) {
          throw new Error("실패");
        } else {
          // 입력값이 true, false가 아니면
          throw new Error("true 혹은 false 값을 입력해 주세요.");
        }
      } catch (error) {
        reject(error);
      } finally {
        console.log("promise()끝");
      }
    });
  }

  const pr = promise(123);
  console.log(pr);

  pr.then((data) => {
    console.log(data);
  })
    .catch((error) => {
      console.log(error);
    })
    .finally(() => {
      console.log("Promise.finally()");
    });

5true, false, 둘 다 아닌 값을 넣었을 때의 결과.

 

5) then, catch, finally가 실행되는 순간

아래 코드를 보면 Promise 객체는 5초의 시간이 지난 후 resolve()를 호출하고 있다.

  const pr = () => {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve("성공");
      }, 5000);
    });
  };

  pr().then((result) => {
    console.log(result);
  });

해서 pr().then()은 5초 뒤에 실행된다. 즉 백그라운드에서 돌아간다.

 

6) 콜백 지옥 해결 & Promise Chaining

여기서 아까 콜백 지옥을 프라미스로 구현해 본다면? return의 결과를 또 then으로 받아서 아래처럼 구현할 수 있다. 이렇게 Promise 객체와 then 을 계속 활용하는 것을 프라미스 체이닝이라고 한다.

  function notCallbackHell(msg, time) {
    return new Promise((resolve, reject) => {
      try {
        setTimeout(() => {
          resolve(msg);
        }, time);
      } catch (err) {
        reject(err);
      }
    });
  }

  notCallbackHell("a", 1000)
    .then((data) => {
      console.log(data);
      return notCallbackHell("b", 2000);
    })
    .then((data) => {
      console.log(data);
      return notCallbackHell("c", 3000);
    })
    .then((data) => {
      console.log(data);
      return notCallbackHell("d", 4000);
    })
    .then((data) => {
      console.log(data);
      return notCallbackHell("e", 5000);
    })
    .then((data) => {
      console.log(data);
    })
    .catch((error) => {
      console.log(error);
    })
    .finally(() => {
      console.log("콜백 지옥 해결??");
    });

하지만 가독성 면에서는 좋아졌지만 코드가 더 길어졌고 복잡한 건 여전하다.

 

3. Async await

1) Async 함수

function 키워드 앞에 async 키워드를 사용하면 해당 함수는 async 함수가 된다. async 함수가 된다는 건 아래와 같은 속성을 갖게 된다.

 

  • 항상 Promise 객체를 반환한다.
  • 일반 리터럴 값을 리턴해도 Promise.resolve()에 감싸져 Promise 객체로 리턴된다.
  • awiat 키워드는 async 함수 안에서만 동작한다.

 

return 123은 return Promise.resolve(123)과 같이 동작한다.

  async function foo() {
    return 123;
    // return Promise.resolve(123);
  }

  console.log(foo());

async 함수 내에서 명시적으로 reject를 사용할 수도 있다.

  const foo = async () => {
    try {
      throw new Error("err");
    } catch (e) {
      return Promise.reject(e.message);
    }
  };

  console.log(foo());

 

즉 async 함수도 Promise 객체를 리턴하기 때문에 값을 사용하려면 Promise 객체 사용법과 마찬가지로, then을 사용해야 함.

     async function foo() {
        return 123;
      }

      foo().then((v) => {
        console.log(v);
      });

 

2) Await

  • 자바스크립트가 await을 만나면 프라미스가 처리될 때까지 기다린다.
  • 무조건 Promise 객체를 리턴해야지만 await가 된다. Promise 객체가 아닌 경우 await이 먹히지 않는다.
  • await 사용 시, Promise 객체를 받는 것이 아니라, resolve, reject 값 자체를 바로 받는다.
  async function foo() {
    const pr = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve("완료!");
      }, 3000);
    });

    const result = await pr;
    console.log("--------------------");
    console.log(result);
  }

  foo();

await을 만남으로서, 해당 구문은 fulfilld 상태가 될 때까지 기다리게 된다. 그 후에 ----------와 result가 찍히게 된다.

 

 

아래의 코드는 3초 후에 ---------와 e, 즉 reject인 "실패!"가 뜨게 된다.

 async function foo() {
    const pr = new Promise((resolve, reject) => {
      setTimeout(() => {
        reject("실패!");
      }, 3000);
    });

    try {
      const result = await pr;
      console.log(result);
    } catch (e) {
      console.log("--------------------");
      console.log(e);
    }
  }

  foo();

 

지금까지 해오던 예제 코드를 Async await로 깔끔하게 구현할 수 있다. await를 사용하게 되면 .then(데이터를 받았을 때)의 행동이 필요가 없어지게 된다. 즉 await는 promise와 다르게, resolve() 안의 값을 그대로 전달받는다.

  function notCallbackHell(msg, time) {
    return new Promise((resolve, reject) => {
      try {
        setTimeout(() => {
          resolve(msg);
        }, time);
      } catch (err) {
        reject(err);
      }
    });
  }

  async function asyncFunction() {
    try {
      let temp = await notCallbackHell("a", 1000);
      console.log(temp);

      temp = await notCallbackHell("b", 2000);
      console.log(temp);

      temp = await notCallbackHell("c", 3000);
      console.log(temp);

      temp = await notCallbackHell("d", 4000);
      console.log(temp);

      temp = await notCallbackHell("e", 5000);
      console.log(temp);
    } catch (err) {
      console.log(err);
    }
  }

  asyncFunction();

await를 사용하려면 해당 스코프의 함수에 async가 적용되어 있어야 한다. 만약 위의 코드의 Promise 객체에서 resolve(msg)가 아닌 reject나 throw가 발생하게 된다면?

  function notCallbackHell(msg, time) {
    return new Promise((resolve, reject) => {
      try {
        setTimeout(() => {
          resolve(msg);
          // reject(msg);
          // throw new Error(msg);
        }, time);
      } catch (err) {
        reject(err);
      }
    });
  }

async 함수의 catch 부분에서 잡히므로 아래처럼 하나만 찍히고 더 이상 진행되지 않는다.

 

async 함수가 끝난 뒤 무엇을 실행시키고 싶다면 then을 활용하면 된다.

  console.log("1");
  asyncFunction().then(() => {
    console.log("2");
  });

 

 

4. PromiseAll

Promise.all은 배열로 인자 값을 받아 해당 배열 내의 Promise 객체들이 모두 fulfilled 상태가 되어야 결과를 반환한다.

 

아래의 코드처럼 각각의 얼마나 걸릴지 모르는 Promise 객체가 있다고 봤을 때, 이들을 한 번에 실행해도 되는 경우(결과의 순서가 상관없는 경우) 아래처럼 사용할 수도 있지만,

  const a = () =>
    new Promise((resolve, rejct) => {
      setTimeout(() => {
        resolve("a");
      }, 1210);
    });

  const b = () =>
    new Promise((resolve, rejct) => {
      setTimeout(() => {
        resolve("b");
      }, 1400);
    });

  const c = () =>
    new Promise((resolve, rejct) => {
      setTimeout(() => {
        resolve("c");
      }, 800);
    });

  a().then(console.log);
  b().then(console.log);
  c().then(console.log);

 

 

Promise.all을 활용해서 아래처럼 구현할 수도 있다. init이라는 async 함수를 만들어서 모든 Promise 코드를 한 번에 동작시키고 결과를 배열로 반환받는다.

  const a = () =>
    new Promise((resolve, rejct) => {
      setTimeout(() => {
        resolve("a");
      }, 1210);
    });

  const b = () =>
    new Promise((resolve, rejct) => {
      setTimeout(() => {
        resolve("b");
      }, 1400);
    });

  const c = () =>
    new Promise((resolve, rejct) => {
      setTimeout(() => {
        resolve("c");
      }, 800);
    });

  const init = async () => {
    const pr = Promise.all([a(), b(), c()]);
    console.log(pr);
  };

  init();

728x90
반응형
Comments