JS - Callback, Promise, async & await

bkboy·2022년 7월 26일
0

웹 개발

목록 보기
1/26
post-thumbnail

비동기의 문제

JS는 비동기적으로 처리되는 언어이다. 특정 코드의 연산이 끝날 때까지 코드의 실행을 멈추지 않고 다음 코드를 먼저 처리한다는 뜻이다.

console.log('Hello');
setTimeout(function() {
	console.log('Bye');
}, 3000);
console.log('Hello Again');

JS가 비동기적으로 처리되는 걸 모른다면
hello => 3초 후 Bye, => Hello Again 의 순서대로 출력될 것이라고 예상할 것이다.
실제론 hello => Hello Again => 3초 후 Bye 이다.

비동기 처리는 굉장한 장점이라고 생각된다. 일상 생활에서도 빗대어 생각해보면 전자레인지에 밥을 데워놓고 가만히 앉아서 기다릴게 아니라 반찬을 꺼내놓으면 좋지 않은가! 허나 종속적인 함수가 있다면 문제가 발생할 수 있다.

const makeName = (lastName, firstName) => {
  let fullName;
  setTimeout(() => {
    fullName = lastName + firstName;
  }, 3000);

  return fullName;
};
const introduce = (fullName) => {
  console.log(`안녕하세요. 제 이름은 ${fullName}입니다.`);
};
const fullName = makeName("홍", "길동");

introduce(fullName);

이름을 만드는 makeName 함수를 의도적으로 setTimeout을 이용해 시간이 걸리게 만들었다.(비동기 작업이 되었다.) 그럼 introduce가 먼저 실행이 될테고 "안녕하세요. 제 이름은 undefined입니다." 라는 출력값이 나오게 된다. 이런 비동기 처리에서 발생되는 문제를 해결할 수 있는 것이 콜백함수이다.

콜백함수(Callback Function)

콜백함수는 함수 안에서 실행되는 또 다른 함수로, 파라미터로 전달되는 함수를 말한다. '다 끝나고 실행이 될때 불러줘~!' 라는 의미의 콜백이다.

function introduce(lastName, firstName, callback) {
  let fullName = lastName + firstName;
  callback(fullName);
}

introduce("홍", "길동", (name) => {
  console.log(`안녕하세요. 제 이름은 ${name}입니다.`);
});

이제 알맞게 출력된다. callback은 언뜻 보기엔 단순한 변수같지만 함수이다.(TS의 필요성을 이렇게..) 함수를 실제 사용하는 부분에선 화살표 함수를 넣어서 사용했다.

function introduce (lastName, firstName, callback) {
    var fullName = lastName + firstName;
    
    callback(fullName);
}
 
function say_hello (name) {
    console.log("안녕하세요 제 이름은 " + name + "입니다");
}
 
function say_bye (name) {
    console.log("지금까지 " + name + "이었습니다. 안녕히계세요");
}
 
introduce("홍", "길동", say_hello);
// 결과 -> 안녕하세요 제 이름은 홍길동입니다
 
introduce("홍", "길동", say_bye);
// 결과 -> 지금까지 홍길동이었습니다. 안녕히계세요

콜백의 유용함을 보여주는 코드이다. introduce에서 만든 fullName을 이용해 콜백으로 넣는 함수만 바꿔 다른 동작을 쉽게 할 수 있다.

콜백지옥(Callback Hell)

콜백 지옥은 콜백함수의 호출이 반복되어 가독성을 떨어트리는 코드를 말한다.

function add(x, callback) {
    let sum = x + x;
    console.log(sum);
    callback(sum);
}

add(2, (result) => {
  add(result, (result) => {
    add(result, (result) => {
      console.log("finish");
    });
  });
});
// 4, 8, 16, finish

여기서 프로미스의 등장이다.

Promise

프로미스는 JS 비동기 처리에 사용되는 객체이다. new 연산자를 사용하고 콜백으로 resolve, reject 각각 성공, 실패했을 때 실행할 콜백을 받는다. (콜백의 인자로 콜백을 받는다!)


function add(x) {
  let sum = x + x;
  console.log(sum);
  return new Promise((resolve, reject) => {
    resolve(sum);
  });
}

add(2)
  .then((result) => add(result))
  .then((result) => add(result))
  .catch((err) => console.log(err))
  .finally(() => {
    console.log("끝");
  });
// 4, 8, 16, 끝

콜백 지옥을 탈출했다. 이제 then 지옥인가? 하지만 이건 이해하기 어려움을 주는 수준은 아니다!
catch는 오류 즉 reject가 실행되거나 다른 오류가 발생하면 실행되고 finally는 마지막에 실행된다.

function add(x) {
  let sum = x + x;
  console.log(sum);
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(sum);
    }, 2000);
  });
}

add(2)
  .then((result) => add(result))
  .then((result) => add(result))
  .catch((err) => console.log(err))
  .finally(() => {
    console.log("끝");
  });

setTimeout을 추가했다. 2초 간격으로 같은 값이 출력된다.
함수를 나눠서 예제를 들어보겠다.

const f1 = () => {
  return new Promise((res, rej) => {
    setTimeout(() => {
      res("1번 주문 완료");
    }, 1000);
  });
};

const f2 = (message) => {
  console.log(message);
  return new Promise((res, rej) => {
    setTimeout(() => {
      res("2번 주문 완료");
      //   rej("xxx"); 
    }, 3000);
  });
};

const f3 = (message) => {
  console.log(message);
  return new Promise((res, rej) => {
    setTimeout(() => {
      res("3번 주문 완료");
    }, 2000);
  });
};

f1()
  .then((res) => f2(res))
  .then((res) => f3(res))
  .then((res) => console.log(res))
  .catch(console.log)
  .finally(() => {
    console.log("끝");
  });


각 초간격으로 잘 나온다. 만약 중간에 f2에서 reject를 실행하게 되면 3번 주문은 실행되지 않고 바로 '끝'이 출력된다.

Promise.all

현재 f1, f2, f3의 작업에는 1+3+2초 즉 6초가 걸린다. 세개를 동시에 실행하면 3초면 끝날 것이다. 이럴 때 사용할 수 있는게 Promise.all이다

... 함수는 동일
Promise.all([f1(), f2(), f3()]).then((res) => {
  console.log(res);
}); // [ '1번 주문 완료', '2번 주문 완료', '3번 주문 완료' ]

실행하고 3초뒤에 배열 형태로 [ '1번 주문 완료', '2번 주문 완료', '3번 주문 완료' ] 이렇게 출력이 된다. 만약 reject가 하나라도 껴있다면 어떤 데이터도 얻을 수 없다.

Promise.race

... 함수는 동일
Promise.race([f1(), f2(), f3()]).then((res) => {
  console.log(res);
}); // 1번 주문 완료

Promise.race 말그대로 race이다 가장 먼저 끝나는 f1의 출력 값만 나온다.

async & await

then 지옥까진 아니지만 그래도 더욱 가독성이 좋게 수정할 수 있다. 바로 async & await, 함수에 async를 붙이고 내부에서 promise를 반환하는 값 앞에 await을 붙인다. 해당 프로미스가 끝날때를 기다린다는 의미이다. async가 있어야 await을 사용할 수 있다.

function add(x) {
  let sum = x + x;
  return new Promise((res, rej) => {
    setTimeout(() => {
      res(sum);
      //   rej(new Error("err..."));
    }, 1000);
  });
}

const threeTimesAdd =  () => {
  try {
    let result1 = await add(2);
    console.log(result1);
    let result2 = await add(result1);
    console.log(result2);
    let result3 = await add(result2);
    console.log(result3);
  } catch (err) {
    console.log(err);
  } finally {
    console.log("끝");
  }
};

threeTimesAdd();

async의 기능이 한가지 더있다 함수를 promise를 반환하는 함수로 바꿔주는 것이다. 따라서..

async function add(x) {
  let sum = x + x;

  //   throw new Error("err...");
  return sum;
}

const threeTimesAdd = async () => {
  try {
    let result1 = await add(2);
    console.log(result1);
    let result2 = await add(result1);
    console.log(result2);
    let result3 = await add(result2);
    console.log(result3);
  } catch (err) {
    console.log(err);
  } finally {
    console.log("끝");
  }
};

threeTimesAdd();

이렇게도 작성이 가능하다. 에러는 throw 키워드로 발생시킨다.

정리

JS는 비동기 처리를 지원하는 언어인데 이로 인해 통신에 문제가 생길 수 있다. 이를 콜백을 통해 해결했지만 콜백지옥이라는 가독성이 나쁜 패턴이 발생했고 그를 해결하기 위해 Promise가 탄생했다. 라는 이야기~~~ 이걸 이렇게 이해하기까지 많은 시간이 걸렸다..

참고

profile
음악하는 개발자

0개의 댓글