[JS] Callback, Promise, Async & Await

요들레이후·2022년 5월 12일
0

Javascript

목록 보기
5/11
post-thumbnail

드림코딩 by 엘리 : 자바스크립트 기초 강의 (ES5+)

1. Callback

1) Synchronous & Asynchronous

JavaScript는 동기적(synchronous)이다.
호이스팅이 된 이후부터 코드가 우리가 작성한 순서에 맞춰서 하나하나씩 동기적으로 실행이 된다.

hoisting

  • 인터프리터가 변수와 함수의 메모리 공간을 선언 전에 미리 할당하는 것을 의미
  • var로 선언한 변수의 경우 호이스팅 시 undefined로 변수를 초기화 한다.
  • 반면 letconst로 선언한 변수의 경우 호이스팅 시 변수를 초기화하지 않는다.
  • function declaration도 호이스팅 된다.

비동기적(Asynchronous)란 동기(synchronous)와 다르게 언제 실행될 지 예측할 수 없는 코드들을 말한다.

JavaScript엔진은 위에서 아래로부터 순차적(동기)으로 실행하게 된다.

setTimeout은 대표적인 비동기 함수로 콜백함수를 호출한다. 단, 브라우저는 setTimeout이 끝날때까지 기다리지 않는다.

때문에 결과는 1→3→2로 실행되며 이것이 비동기적 실행이다.

console.log("1"); // 동기
setTimeout(() => console.log("2"), 1000); // 비동기
console.log("3"); // 동기

콜백은 2가지 경우로 나뉜다.

Synchronous Callback

  • 즉각적, 동기적으로 실행
console.log("1"); // 동기
setTimeout(() => console.log("2"), 1000); // 비동기
console.log("3"); // 동기

// Synchronous callback
function printImmediately(print) {
  print();
}
printImmediately(() => console.log("hello")); // 동기

Asynchronous Callback

  • 나중에 실행 or 언제 실행될 지 예측할 수 없음
console.log("1"); // 동기
setTimeout(() => console.log("2"), 1000); // 비동기
console.log("3"); // 동기

// Synchronous callback
function printImmediately(print) {
  print();
}
printImmediately(() => console.log("hello")); // 동기

// Asynchronous callback
function printWithDelay(print, timeout) {
  setTimeout(print, timeout);
}
printWithDelay(() => console.log("async callback"), 2000); // 비동기

함수의 선언은 호이스팅이 되어 제일 위로 올라감 → 함수 선언 → 순차적으로 실행

  • Synchronous는 차례대로 실행
  • Asynchronous는 빠졌다가 지정된 시간에 실행

따라서 실제 JavaScript엔진은 아래와 같이 코드가 실행된다.

   function printImmediately(print) {
      print();
   }

   function printWithDelay(print, timeout) {
     setTimeout(print, timeout)
   }

   console.log("1"); // 동기
   setTimeout(() => console.log("2"), 1000); // 비동기
   console.log("3"); // 동기
   printImmediately(() => console.log("hello")); // 동기
   printWithDelay(() => console.log("async callback"), 2000); // 비동기



2) Callback Hell Example

  • UserStorage class
  1. loginUser API(로그인하는 API)
  2. getRoles API(유저인지 관리자인지 check API)
class UserStorage {
  loginUser(id, password, onSuccess, onError) {
    setTimeout(() => {
      if (
        (id === "hashin" && password === "hello") ||
        (id === "bomnal" && password === "2022")
      ) {
        onSuccess(id);
      } else {
        onError(new Error("not found"));
      }
    }, 2000);
  }
  getRoles(user, onSuccess, onError) {
    setTimeout(() => {
      if (user === "hashin") {
        onSuccess({ name: "hashin", role: "admin" });
      } else {
        onError(new Error("no access"));
      }
    }, 1000);
  }
}
  • 예제 코드 단계
  1. Client에서 ID & Password 입력
  2. 입력 받아온 ID & Password로 Server에서 login(setTimeout()을 이용해 Server에서 login하는 것처럼 보여지게 함)
  3. login 성공 시 받아오는 ID를 통해 getRoles API요청
  4. 마지막으로 API요청에 성공하면 namerole을 출력
const userStorage = new UserStorage();
const id = prompt("enter your id");
const password = prompt("enter your password");
userStorage.loginUser(
  id,
  password,
  (user) => {
    userStorage.getRoles(
      user,
      (userWithRole) => {
        alert(
          `Hello ${userWithRole.name}, you have a ${userWithRole.role} role.`
        );
      },
      (error) => {
        console.log(error);
      }
    );
  },
  (error) => {
    console.log(error);
  }
);

이런식으로 콜백함수 안에 또 콜백함수를 부르고 꼬리를 물듯이 계속 이어지면 이것을 콜백 지옥이라고 부른다.

콜백 지옥 문제점
1. 가독성이 떨어짐, business logic을 알아보기 어려움
2. 에러 발생 및 디버깅 시 어려움을 가짐, 유지보수가 어려움

✨ 이러한 콜백 지옥에서 벗어나기 위해선 Promiseaync + await을 사용한다.




2. Promise

promise는 JavaScript에서 비동기(Asynchoronous)연산을 간편하게 처리해주는 Object이다.

Promise는 정해진 임무를 수행하고 나서 정상적으로 기능이 수행되었다면 성공의 메시지와 함께 처리된 결과값을 전달해 준다. 만약 임무를 수행하다 정상적으로 수행되지 못하면 에러를 발생시킨다.

[프로미스는 2가지 개념을 중심으로 이해]
1. State

  • pending(operation수행중)
  • fulfilled(operation완료) or rejected(operation문제발생)
  1. Producer & consumer 차이점
  • Producer(원하는 데이터를 제공)
  • Consumer(정보를 소비)

1) Producer

  • Promise 만들기
const promise = new Promise(executor);

promise는 class이기 때문에 new 키워드를 통해 객체를 생성한다.
promise는 executor라는 callback함수를 전달해야 하고, 이 callback함수에는 또 다른 두 가지의 callback함수를 받는다.

  • resolve : 기능을 정상적으로 수행해서 마지막에 최종 데이터를 전달
  • reject : 기능을 수행하다가 중간에 문제가 생기면 호출
const promise = new Promise((resolve, reject) => {
  // doing some heavy work (network, read files) => asynchronous
  console.log("doing something...");
  setTimeout(() => {
    // resolve("hashin");
    reject(new Error("no network"));
  }, 2000);
});
  • 코드에서 promise가 만들어지는 순간 executor가 바로 실행되고, 네트워크 통신을 수행하게 된다.
  • 만약 네트워크 요청을 사용자가 요구했을 때만 해야하는 경우라면, 위 코드처럼 작성했다면 사용자가 요청하지 않았는데 불필요한 통신이 일어날 수 있다.
  • 프로미스가 생성된 순간 executor라는 callback 함수가 바로 실행되기 때문에 이 점에 유의하면서 코드를 작성.

2) Consumers

- then, catch, finally를 이용해서 값을 받아올 수 있다.

  • then : promise operation이 성공적으로 작동했을 때, resolve에서 전달된 값 및 원하는 기능 수행
  • catch : promise operation 에러가 발생했을 때, reject에서 받아온 값 수행
  • finally : promise 성공 or 에러 상관없이 무조건 마지막에 호출

resolve - then

const promise = new Promise((resolve, reject) => {
  // doing some heavy work (network, read files) => asynchronous
  console.log("doing something...");
  setTimeout(() => {
    resolve("hashin");
  }, 2000);
});

promise 
  .then((value) => {
    console.log(value);
  });

  • Promise가 정상적으로 수행되어서 마지막으로 resolve콜백함수 통해 전달된 값이 value로 들어옴
  • resolve가 'hashin'라는 값을 전달

reject - catch

const promise = new Promise((resolve, reject) => {
  // doing some heavy work (network, read files) => asynchronous
  console.log("doing something...");
  setTimeout(() => {
    reject(new Error("no network"));
  }, 2000);
});

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

  • Promise reject에서 받아온 값이 error로 들어옴
  • catch를 사용해서 error발생 시 어떻게 처리할 것인지에 대한 콜백함수 등록

finally

const promise = new Promise((resolve, reject) => {
  // doing some heavy work (network, read files) => asynchronous
  console.log("doing something...");
  setTimeout(() => {
    reject(new Error("no network"));
  }, 2000);
});

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

  • 성공, 실패 상관없이 마지막에 처리하고 싶은 결과 등록

3) Promise chaining

  • then은 값을 전달할 수도 있고, promise를 전달 할 수도 있다.
const fetchNumber = new Promise((resolve, reject) => {
  setTimeout(() => resolve(1), 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));

  • "1" 전달 → "2" 전달(1x2) → "6" 전달(2x3) → return에서 "5" 전달(6-1) → "5" 출력

4) Error Handling

  • Promise chain이 실패하지 않고 결과를 출력하게 하려면 catch메서드를 통해 에러를 받아올 수 있다.
  • 하나의 then메서드에서 에러가 발생하면 값을 받아온 then메서드 바로 밑에 catch메서드를 넣음으로써 catch메서드로 값을 반환해주며 에러 페이크 처리를 할 수 있다.
const getHen = () =>
  new Promise((resolve, reject) => {
    setTimeout(() => resolve("🐔"), 1000);
  });
const getEgg = (hen) =>
  new Promise((resolve, reject) => {
    setTimeout(() => reject(new Error(`error: ${hen} => 🥚`)), 1000);
  });
const cook = (egg) =>
  new Promise((resolve, reject) => {
    setTimeout(() => resolve(`${egg} => 🍳`), 1000);
  });

getHen()
  .then((hen) => getEgg(hen)) 
  .catch((error) => {
    return "🍗";
  }) 
  .then((egg) => cook(egg)) 
  .then((meal) => console.log(meal)) 
  .catch(console.log);

  • 받아오는 "한 개의" value를 다른 함수로 바로 보내서 사용하는 경우엔 코드를 다음과 같이 생략할 수 있다.
getHen()
  .then(getEgg)
  .catch((error) => {
    return "🍗";
  }) 
  .then(cook)
  .then(console.log)
  .catch(console.log);

5) Callback-to-Promise

위의 콜백지옥 예제를 Promise로 코드를 변경한 것이다.

class UserStorage {
  loginUser(id, password) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        if (
          (id === "hashin" && password === "hello") ||
          (id === "bomnal" && password === "2022")
        ) {
          resolve(id);
        } else {
          reject(new Error("not found"));
        }
      }, 2000);
    });
  }

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

const userStorage = new UserStorage();
const id = prompt("enter your id");
const password = prompt("enter your password");

userStorage
  .loginUser(id, password)
  .then(userStorage.getRoles)
  .then((user) => alert(`Hello ${user.name}, you have a ${user.role} role.`))
  .catch(console.log);



3. Async & Await

aync & awaitPromise를 조금 더 간결하고 간편하고 동기적으로 실행되는 것처럼 보이게 만들어준다.

Promise를 여러 개 체인형식으로 묶을 수 있는데 Promise마다 then을 여러 개 chaining을 하게 되면 코드가 난잡해 질 수 있다.

async & await은 마치 동기식으로 순서대로 작성하는 것처럼 간편하게 해주는 API제공해준다.

Syntactic sugar
기존에 존재하는 것 위에 간편하게 쓸 수 있는 api를 제공하는 것 ex) Class

async & awaitpromise 위에 덮혀진 Syntactic sugar이지만 그렇다고 해서 async & await만 쓰는 것이 아니라 때에 따라서 Promise를 유지해서 써야할 경우가 있으며 async await을 사용해야 더 깔끔해지는 경우도 있다.


1) Async

function fetchUser() {
	// do network request in 10 secs...
  return "hashin";
}

const user = fetchUser();
console.log(user);

자바스크립트는 동기적으로 실행되기 때문에, 예를 들어 위의 함수처럼 네트워크 요청을 10초동안 받아오는 함수가 있다면 동기로 처리되기 때문에 fetchUser함수가 끝날때까지 기다려야하는 현상을 유발시킨다.

만약, 이 함수 뒤에 브라우저 UI를 나타내야 하는 중요한 코드들이 기다리고 있다면 브라우저는 fetchUser함수가 끝나길 기다리고 끝날 때에서야 비로소 다음 코드들이 실행되는 것을 알 수 있다.

하나의 함수 때문에 모든 코드들이 실행될 수 없는 현상이 발생하기 때문에 이렇게 요청이 긴 함수들은 반드시 비동기로 처리해야 한다.

  • Promise로 해결
function fetchUser() {
	// do network request in 10 secs...
  return new Promise((resolve, reject) => {
    resolve("hashin");
  })
}

const user = fetchUser();
use.then(console.log);
console.log(user);

Promise((resolve, reject)=> ...)를 사용하지 않고 function 앞에 async를 붙이면 code block이 자동으로 promise로 바꿔준다.

async function fetchUser() {
  // do network request in 10 secs...
  return "hashin";
}

const user = fetchUser();
user.then(console.log);
// 결국 promise를 return하기 때문에 .then을 사용하여 출력한다.
console.log(user);


2) Await

  • awaitasync가 붙은 함수 안에서만 사용이 가능하다.
  • function delay: 정해진 ms(시간)를 지나면 resolve를 호출하는 promise를 return한다.
function delay(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

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

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

만약 위의 getBanana함수를 Promise로 만들게 되면

function getBanana() {
  return delay(3000).then(() => "🍌");
}

이렇게 chaining을 해줘야 한다.

asyncawaitPromise에서 각각 Producer(←async)Consumer(←await)의 역할을 한다고 볼 수 있다.

function pickFruits() {
  return getApple().then(apple =>{
    return getBanana().then(banana => `${apple} + ${banana}`);
  });
}

pickFruits().then(console.log);

Promise도 체이닝을 여러번 하게 되면 콜백지옥과 같이 비슷한 문제점이 발생한다.

우리는 이것을 async & await로 간결하게 만들어줄 수 있다.

async function pickFruits() {
  const apple = await getApple();
  const banana = await getBanana();
  return `${apple} + ${banana}`;
}

pickFruits().then(console.log);

동기식으로 코드를 작성해도 비동기식으로 작동하며 코드도 상당히 간결해진다.


async & await 에러 처리

async function pickFruits() {
   try {
     const apple = await getApple();
     const banana = await getBanana();
     return `${apple} + ${banana}`;
   } catch {
     // ...
   } finally {
     // ...
   }
}
  • Promise에서 thenasync & await에서 try와 흡사하며 catch와 finallyasync & await에서 catch / finally와 똑같다.

await 병렬 처리

async function pickFruits() {
  const apple = await getApple();
  const banana = await getBanana();
  return `${apple} + ${banana}`;
}

pickFruits().then(console.log);

위의 코드에는 awaitgetApple()함수를 1초 기다리고 그 뒤에 getBanana()함수를 또 1초 기다리는 문제점이 있다.

두 개의 함수는 별개이므로 서로 기다려 줄 필요가 없으니 아래처럼 코드를 짜서 해결할 수 있다.

async function pickFruits() {
  // await 병렬처리
  const applePromise = getApple();
  const bananaPromise = getBanana();
  const apple = await applePromise;
  const banana = await bananaPromise;
  return `${apple} + ${banana}`;
}

pickFruits().then(console.log);

이렇게 하면 apple함수와 banana함수가 동시에 실행되고 실행되자마자 Promise객체를 생성하기 때문에 병렬적으로 실행되게 된다.


3) useful APIs

위와 같이 병렬 처리를 할 때 유용한 API가 있다.

Promise.all

  • Promise배열을 전달하게 되면 모든 Promise들이 병렬적으로 다 받을 때까지 모아주는 역할을 한다.
function pickAllFruits() {
  return Promise.all([getApple(), getBanana()]).then((fruits) =>
    fruits.join(" + ")
  );
}
pickAllFruits().then(console.log);

Promise.race

  • Promise배열에 전달된 promise중 가장 먼저 값을 return하는 아이만 전달된다.
function pickOnlyOne() {
  return Promise.race([getApple(), getBanana()]);
}

pickOnlyOne().then(console.log);


-> banana가 먼저 전달 되어서 banana가 출력됨


callback이해 & callback지옥 체험
Promise개념 & 활용
Async & Await, Promise APIs

profile
성공 = 무한도전 + 무한실패

0개의 댓글