[CN] 비동기 함수 (1): 동기와 비동기, 콜백 함수

곽재훈·2024년 5월 11일
2
post-thumbnail

여는 글

오늘은 챌린지 반 수업이 본격적으로 시작되기 전에 promise에 대해서 조금 더 공부한 내용을 정리해보고자 한다. 이번 프로젝트에서도 저번 프로젝트에서도 Promise 객체에 익숙하지 않다보니 api와 관련된 요청을 다룰 때마다 내내 발목을 잡혔다. 그래서 그 부분에 대해서 조금 더 공부하기로 했다.
이 글에는 비동기 함수와 관련된 기본적 개념보다는 내가 헷갈렸던 부분들을 정리하려고 한다.

동기비동기,콜백 함수는 무엇인가.
Promisethen()은 왜 등장했는가.
async, await은 또 무엇인가.
등장 배경과 흐름에 따라 정리하면 각 문법이 존재하는 이유를 조금 더 쉽게 이해할 수 있을 것 같다.

만약 지금 API를 호출했는데 계속 밑의 사진과 같은 Promise {<pending>}만 return 되고 있다면,

> 3-2 promise 객체와 promise의 난해함.

파트로 가시라.

  • 이 글에서는 new Promise 생성자를 통해 인스턴스를 생성하고 resolve()와 reject() 같은 함수를 활용하는 방법은 다루지 않는다.
    이건 개인적 공부 노트고, 그저 나같은 초보자가 Promise를 처음 마주했을 때 당황할 만한 점들에 대해 개인적인 비유를 활용하여 Promise에 대한 이해를 돕기 위함이라. 그런 구체적인 개념을 찾으려면 부디 다른 글을 봐주시길!

동기 함수와 비동기 함수, 그리고 Promise

1) 동기와 비동기의 개념

동기 함수 Synchronous function

  • 실행 완료 순서가 정해져있는 함수
  • 실행이 오래 걸리지 않는 함수
  • 실행이 언제 끝날 지 알 수 있는 함수
  • 요청을 보낸 후 응답을 받아야지만 다음 동작이 이루어지는 함수

비동기 함수 Asynchronous function

  • 실행 완료 순서가 정해져있는 않은 함수
  • 실행이 오래 걸리는 함수
  • 실행이 언제 끝날 지 알 수 없는 함수
  • 요청을 보낸 후 응답의 수락 여부와는 상관없이 다음 태스크가 동작하는 함수.

동기 함수와 비동기 함수에 대한 개념은 많이들 들어서 어렵지 않을 것이다.
간단히 말해서 동기는 실행 순서가 정해져 있는 함수이고, 비동기는 실행 순서가 정해져 있지 않은 함수다.
그 이유는 비동기 함수가 완료되는 데 시간이 얼마나 걸리는 지 알 수 없기 때문이다.
그래서 우리는 비동기 함수가 완료되었을 때 실행될 함수를 미리 등록해놓고, 우리의 컴퓨터는 함수의 작업이 완료될 동안 다른 작업을 먼저 진행하다가 함수가 완료되는 언젠가 그 함수에 미리 등록된 함수를 실행하는 것이다.
그리고 그 때 시간이 얼마나 걸릴 지 알 수 없는 작업인 비동기 함수가 완료될 때 실행해 달라고 등록하는 함수가 바로 콜백 함수다.

const callback = () => {
  console.log("First!");
};

setTimeout(callback, 1000);
// setTimeout(callback, milliseconds);

console.log("Second!");

setTimeout은 대표적인 비동기 함수다. setTimeout()은 두 개의 인자를 받는데, 첫 번째 인자로 실행시킬 함수를 받고 두 번째 인자로 몇 초 뒤에 함수를 실행시킬지를 결정한다.

여기서 첫 번째 인자에 들어가는 함수가 콜백 함수다. 물론 예시에는 함수 이름 자체가 callback으로 되어 있지만 당연하게도 저 이름은 아무렇게나 바뀌어도 상관이 없다.

그래서 저 코드의 결과를 본다면 아마도,

이렇게 Second!가 먼저 출력되고 First!가 나중에 출력되는 모습을 볼 수 있다.


2. callback 지옥

1) 누구나 callback 지옥을 마주한다.

위에서 본 것처럼 비동기 함수에서 우리가 비동기 함수의 실행 순서를 결정하려면 콜백 함수를 인자로 전달해야 했다. 그런데 비동기 함수 여러개를 연결하다보면 흔히들 말하는 callback 지옥 현상이 나타난다.

그럼 언제 이런 일들을 마주하게 되는가?
예를 들어, 내가 시간차를 두고 작동하는 여러개의 함수를 만들었다고 하자.
각 함수는 1초에서 5초까지 다양한 차이를 두고 작동하는데, 그 시간은 내가 임의로 등록한다.

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

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

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

이 코드에 따르면 1초 간격을 두고 단어들이 잘 출력될 것이다. 그런데 만약 내가 "First!"를 1초가 아니라 2초 뒤에 출력하고 싶다면 어떻게 해야할까?

그렇다면 나는 모든 코드를 1000, 2000, 3000에서 2000, 3000, 4000으로 수정해야 할 것이다.

그렇다면 그런 상황들을 막기 위해 위의 함수를 이렇게 작성할 수도 있을 것이다.

setTimeout(()=>{
  console.log("First!");
  
  setTimeout(()=>{
  	console.log("Second!");
    
    setTimeout(()=>{
  	  console.log("Third!");
      
	},1000);
  },1000);
},1000); // 여기만 1000에서 2000으로 바꾸면 된다.

이렇게 하면 내가 "First!"가 출력되는 시간을 조정하고 싶을 경우 첫 번째 코드만 바꾼다면 원하는 대로 작동하게 만들 수 있을 것이다. 하지만 어떤가? 벌써 가독성이 안좋아지는 것이 보이지 않는가?

이건 실제로 이번에 내가 애니메이션을 주기 위해서 작성한 함수인데, 작성 당시에는 promise에 대해 거의 무지한 상태여서 아주 가독성이 극악이다.
코드를 자세히 볼 필요는 없다. 그냥 콜백에 콜백이 이어지는 것이 가독성을 얼마나 망가뜨리는지만 보면 된다.

2) 내가 작성한 코드

const printTicket = () => {
  loadingBar.style.animationName = "none";
  ticketBox.style.animationName = "none";
  ticket.style.animationName = "none";
  eventArea.style.overflow = "visible";
  ticket.style.opacity = "0";
  const ticketContent = document.querySelector(".ticket p");
  if (ticketContent) {
    ticketContent.remove();
  }
  coinInsertPlay();
  setTimeout(() => {
    printBtn.classList.add("working");
    setTimeout(() => {
      loadingBar.style.animationName = "loading";
    }, 500);

    setTimeout(() => {
      ticketBox.style.animationName = "wiggle";
    }, 6000);

    setTimeout(() => {
      eventArea.style.overflow = "hidden";
      ticket.style.opacity = "1";

      generateTicketData();

      setTimeout(() => {
        ticket.style.animationName = "printingTicket";

        setTimeout(async () => {
          updateTicketCount();
          printBtn.classList.remove("working");
          // ticket.style.animationName = "none";
          await decreaseCoin();
          myRestTickets.innerText = `남은 동전: ${await getRestCoin()}`;

          setTimeout(() => {
            const ticketGrade = localStorage.getItem("ticketGrade");
            const seatInfo = localStorage.getItem("seatInfo");
            cardModal(ticketGrade, seatInfo);
          }, 1000);
        }, 4000);
      }, 1000);
    }, 9000);
  }, 3000);
};

3) 간단하게 바꾼 내용 (근데 안 간단함)

const printTicket = () => {
  console.log("1번!");
  
  setTimeout(() => {
	console.log("2번!");
    
    setTimeout(() => {
      console.log("3번!");
    }, 500);

    setTimeout(() => {
      console.log("4번!");
    }, 6000);

    setTimeout(() => {
      console.log("5번!");
      
      setTimeout(() => {
        console.log("6번!");

        setTimeout(async () => {
          console.log("7번!");

          setTimeout(() => {
            console.log("8번!");
          }, 1000);
        }, 4000);
      }, 1000);
    }, 9000);
  }, 3000);
};

이런 callback hell의 문제를 해결하기 위해 Promise는 등장했다. 즉, Promise 문법은 callback 함수와 마찬가지로 비동기 함수의 실행 순서를 제어하기 위해서 그리고 callback 방식의 단점을 해소하기 위해서 나왔다고 볼 수 있는 것이다.

profile
개발하고 싶은 국문과 머시기

0개의 댓글