본문 바로가기
카테고리 없음

[새싹 x 코딩온] 웹 개발자 부트캠프 과정 5주차 회고 | Callback, Promise, Async/Await

by 새파란레몬 2023. 8. 21.

본래의 javascript는 비동기 처리 방식을 따릅니다.

 

console.log(1);
setTimeout(function () {
  console.log(2);
}, 2000);
// 2초
console.log(3);

setTimeout이라는 특수한 상황이 아니었더라면 1 2 3이 순서대로 출력이 되야할 것입니다. 하지만 실제의 출력 결과는 다음과 같습니다. 

setTimeout은 오래 걸리는 작업이기에 1과 3을 console에서 먼저 출력 후 다음 2 가 출력되게 됩니다. 


또 다른 예시를 들어보겠습니다.

아래의 예시에서는 마트를 가서 어떤 음료를 살지 3초간 고민 한 후 '고민 끝!!'을 출력 후 제로 콜라와 price를 확인 후 pay function을 실행시키려고 합니다.

function goMart() {
  console.log('마트에 가서 어떤 음료를 살지 고민한다.');
}

function pickDrink() {
  setTimeout(function () {
    // 3초 기다린 후에 코드 실행(= 3초 고민함)
    console.log('고민 끝!!');
    product = '제로 콜라';
    price = 2000;
  }, 3000);
}

function pay(product, price) {
  console.log(`상품명: ${product}, 가격 ${price}`); // undefined, 비동기 처리이기 때문.
}

let product;
let price;
goMart();
pickDrink();
pay(product, price);

위 코드의 출력 결과

하지만 위의 코드의 실행 결과를 보면 상품명과 가격이 undefined 로 출력됩니다. product와 price가 정해지기 전에 pay function이 실행되었기 때문입니다.

 

이런 문제를 해결하기 위해 콜백 함수를 이용한 비동기 처리를 해보겠습니다. 

function goMart() {
  console.log('마트에 가서 어떤 음료를 살지 고민한다.');
  console.log(1);
}

function pickDrink(callback) {
  // 여기서 콜백은 인자이기에 꼭 'callback' 이라고 작성하지 않아도 ok.
  //  *callback 매개변수: 콜백함수를 의미
  console.log(2);
  setTimeout(function () {
    // 3초 기다린 후에 코드 실행(= 3초 고민함)
    console.log('고민 끝!!');
    product = '제로 콜라';
    price = 2000;
    console.log(3);
    callback(product, price); // * 매개변수로 넘김 콜백함수 실행
  }, 3000);
}

function pay(product, price) {
  console.log(`상품명: ${product}, 가격 ${price}`); // undefined, 비동기 처리이기 때문.
}

let product;
let price;
goMart();
pickDrink(function pay(product, price) {
  console.log(`상품명: ${product}, 가격 ${price}`);
});

위와 같이 pickDrink 후에 function pay를 받으면, setTimeout 안에서 callback을 통해 실행이 되는 것입니다. 따라서 function pay가 받는 변수 product, price와 callback이 받는 변수가 서로 일치하는 것 또한 알 수 있습니다. 

위 코드의 출력 결과

이렇게 콜백을 이용하면 원하는 순서대로 흐름을 제어할 수 있습니다.


이런 비동기 처리 방식에는 3가지 방법이 존재합니다.

 

1. callback 함수

javascript에서는 함수를 인자로 받고 다른 함수를 통해 반환될 수 있는데, 이 때 인자(매개변수)로 대입되는 함수를 콜백함수라고 합니다. 즉, 함수를 인자로 받는 것입니다. (매개변수와 인자는 구별되는 개념이기는 하나 혼용해서도 사용한다고 합니다.) 대개 마지막에 콜백함수가 오도록 작성됩니다. 

 

콜백 함수를 사용하는 이유는 비동기 방식으로 작성된 함수를 동기 처리하기 위해서입니다. addEventListener와 같은 응답을 받은 이후 처리되야 하는 종속적인 작업을 위한 것입니다.

 

하지만 콜백에는 단점이 존재합니다. 매개변수로 넘겨지는 콜백 함수가 반복될수록 코드의 들여쓰기가 깊어져 가독성이 떨어지는데 이를 콜백 지옥이라고 합니다. 

콜백 지옥 (출처: 새싹x코딩온)

2. Promise

다른 방법으로 이번 ES6에서 추가된 JS 문법으로서 promise가 있습니다. 비동기를 동기 방식처럼 순서대로 실행할 수 있도록 만들 수 있는 객체로서(하지만 코드는 여전히 비동기 방식으로 작동합니다.), 미래에 대한 실패 또는 성공에 대한 결과 값을 약속한다는 의미입니다. 그렇기에 성공이 되었던, 실패가 되었던 어떤 결과는 반드시 나오게 됩니다.

 

이 때의 성공과 실패는  then, catch와의 연결구조로 받습니다.

 

이런 promise는 생성과 사용의 두 과정을 거쳐 사용할 수 있습니다.

 

<생성하는 단계>

function promise1(flag) { 
  // flag: false true를 위한 불리안 인자.
  return new Promise(function (resolve, reject) {
    if (flag) {
      resolve(
        `현재 프로미스의 상태는 fulfilled(이행)! then 메서드로 연결~ 이 때 flag 값은 ${flag}!`
      );
    } else {
      reject(
        `현재 프로미스의 상태는 rejected(거절)! catch 메서드로 연결~ 이 때 flag 값은 ${flag}!`
      );
    }
  });
}

new 를 이용해 promise 개체를 생성 후, resolve와 reject의 각각의 상태에 따른 값을 담아줄 수 있습니다.

 

<소비(사용)하는 단계>

promise1(5 > 3)
  .then(function (result) {
    console.log(result);
  })
  .catch(function (error) {
    console.log(error);
  });

위의 단계는 화살표 함수로도 가능합니다.

promise1(5 < 3)
  .then((result) => console.log(result))
  .catch((error) => console.log(error));

 

콜백에서 실행했던 마트 코드를 promise로 다시 작성해보면 다음과 같습니다. 

function goMart() {
  console.log('마트에 가서 어떤 음료를 살지 고민한다.');
}

function pickDrink() {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      console.log('고민 끝!!');
      product = '제로 콜라';
      price = 4000; //2000, 4000
      //resolve(); //resolve 안에 내용 없어도 무방. 성공으로 동기 처리한 것. 내용을 입력하면 resolve에 대한 어떤 결과가 입력된 것.
      if (price <= 2000) {
        resolve();
      } else {
        reject();
      }
    }, 3000);
  });
}

function pay() {
  console.log(`상품명: ${product}, 가격 ${price}`);
}

function nopay() {
  console.log('금액 부족 ㅜㅜ ');
}

let product;
let price;
goMart();
pickDrink().then(pay).catch(nopay);

 

한편 promise에는 상태가 존재하는데 이를 요약해보면 다음과 같습니다. 

 

 

3. async / await

promise도 then().then()가 반복되면 코드의 가독성이 떨어질 수 있어, 이를 위해서 async/await로 다르게 코드를 작성할 수 있습니다. async가 붙은 함수는 프로미스를 반환하고, await는 프로미스 앞에 붙어 기다리는 역할을 합니다. 

 

1초 뒤에 과일 배열을 출력하는 코드로 먼저 function fetchfruits를 작성 후, promise 생성하면 다음과 같습니다.

 

function fetchFruits() {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      const fruits = ['사과', '레몬', '수박'];
      resolve(fruits);
      //   reject(new Error('알 수 없는 에러 발생!! 아이템을 가져올 수 없음!!'));
    }, 1000);
  });
}

이를 then().catch()로 작성하면 다음과 같이 작성하게 됩니다. 

fetchFruits()
  .then(function (f) {
    console.log(f);
  })
  .catch(function (error) {
    console.log(error);
  });

한편, async 사용 시 에러 처리는 아래와 같은 try catch 구문으로 처리해줄 수 있습니다.

async function printItems() {
  try {
    const fruits = await fetchFruits();
    // fetchFruits가 실행될 때까지 기다림. 실행 후 그 결과를 fruits에 저장
    console.log(fruits);
  } catch (error) {
    console.log(error);
  }
}
printItems();

다시 마트 가는 코드를 async 로 표현하면 아래와 같습니다. 

function goMart() {
  console.log('마트에 가서 어떤 음료를 살지 고민한다.');
}

function pickDrink() {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      // 3초 기다린 후에 코드 실행(= 3초 고민함)
      console.log('고민 끝!!');
      product = '제로 콜라';
      price = 2000;
    }, 3000);
  });
}

function pay() {
  console.log(`상품명: ${product}, 가격 ${price}`);
}

async function exec() {
  goMart();
  await pickDrink();
  pay();
}

let product;
let price;
exec();

then().catch()로 연결하는 구조와 달리 await를 이용해 exec라는 하나의 함수 안에 담긴 것을 확인할 수 있습니다.


실습도 해보고 블로그도 작성해보고, 질문도 해봤는데, 익힌 것 같은 느낌이 안드는 이 3 가지 비동기 처리 방식... 체화되려면 더 많이 사용해봐야할 것 같다. 아직 초반인데도 CS지식에 대한 한계를 느끼고 있다. 공부해야 할 것이 많아서 헤매이기 보다는 하나라도 제대로 알려고 한다. 그 하나조차 쉽지 않은 게 문제이지만...