[TIL] Callback과 비동기 처리

신재욱·2023년 4월 24일
1
post-thumbnail

📒 오늘 공부한 내용

🔍수업목차

[29-1] 이벤트 루프 (Event Loop)
[29-2] Callback과 비동기 처리
[29-2] 매크로 태스크 큐 vs 마이크로 태스크 큐

✅ 이벤트 루프 (Event Loop)


📂 시간을 화면에 보여줄 때 발생할 수 있는 이슈

// 싸이월드때 했던 내용 복습
// setInterval(() => {
//   document.getElementById("timer")?.innerText = "2:59";
// }, 1000);

export default function TastQueuePage(){

	const onClickTimer = () => {

		console.log("=======시작~~=======")

		setTimeout(() => {
			console.log("1초 뒤에 실행된답니다 😎️")
		}, 1000)

		console.log("=======끝~~=======")
	}

	return <button onClick={onClickTimer}>시작~~</button>;
}

실행 결과

  • 코드 순서대로 라면 함수의 끝 부분의 ‘끝’ 콘솔보다 setTimeout 안에 있는 콘솔이 먼저 실행되어야 할 것 같은데, 순서가 뒤집혀있고 이는 자바스크립트의 동작 원리 때문이다.

📂 태스크 큐 (Task Queue)

export default function TastQueuePage(){

	const onClickTimer = () => {

		console.log("=======시작~~=======")

		setTimeout(() => {
			console.log("1초 뒤에 실행된답니다 😎️")
		}, 1000)

		console.log("=======끝~~=======")
	}

	return <button onClick={onClickTimer}>시작</button>;
}
  • 위와 같은 코드를 프론트엔드 서버에서 실행하면, 콜스택에 다음과 같이 Stack이 쌓인다.

1️⃣ callStack에서 onClickTimer 함수가 실행된다. (Stack - Last In First Out / LIFO 구조)
2️⃣ Background에 setTimeout()을 보내서 실행한다.
3️⃣ setTimeout()이 TaskQueue로 전달되어 쌓인다. (Queue - First In First Out / FIFO 구조)
4️⃣ TaskQueue에 쌓이는 함수는 CallStack이 다 비워진 다음 가장 마지막에 실행된다.

스레드(Thread) : TaskQueue에 있는 함수를 CallStack으로 보내는 역할

📂 싱글 스레드

  • 자바 스크립트는 싱글 스레드 방식을 가지고 있다.
  • 조금 더 구체적으로 표현하자면 싱글 이벤트 루프 스레드라고도 한다.
  • CallStack이 비어야만 TaskQueue에 있는 작업을 CallStack으로 가져온다는 것이다.
  • CallStack이 작업 중이라면 setTimeout에 설정한 1초가 이미 지났다고 하더라도 TaskQueue 안의 작업이 CallStack으로 들어오지 못한다.

비동기 작업 : setInterval, setTimeout처럼 CallStack에 쌓이지 않고 Background, TaskQueue로 넘겨지는 작업

📂 프로세스와 스레드

프로세스: 실행되어있는 프로그램
스레드: 프로세스 안에서 동작하는 일꾼

  • 스레드가 하나인 언어를 싱글 스레드 언어, 스레드가 여러개인 언어를 멀티 스레드 언어라고 한다.

💡 싱글 스레드 언어 VS 멀티 스레드 언어

자바스크립트 : 싱글 스레드 언어
자바, 파이썬 : 멀티 스레드 언어

🎯 멀티 스레드가 싱글 스레드보다 좋은 것 아닌가?

반드시 그런 것은 아니다.

  • 멀티 스레드 언어의 경우 동시에 여러가지 작업을 처리하고 있는 것처럼 보이지만, 사실은 여러 개의 스레드가 여러가지 작업을 번갈아가며 빠르게 수행하고 있는 것과 같다.
  • 멀티 스레드의 경우에도 하나의 요청에 대한 응답을 기다렸다가 다음 작업으로 이동해야 하는 것은 동일하다. (이러한 작업을 컨텍스트 스위칭이라고 한다.)
  • 그렇기 때문에 멀티 스레드라고 해서 싱글 스레드보다 두드러지게 빠르지는 않다.
  • 오히려 자바스크립트와 같은 이벤트 루프 싱글 스레드의 경우 오래 걸리는 작업을 TaskQueue로 빼서 처리하기 때문에 높은 퍼포먼스를 낼 수 있다.

✅ Callback과 비동기 처리


  • Callback 함수 : 함수의 인자로 들어가는 함수
  • aaa 함수의 인자에 들어가는 function(){}를 callback 함수라고 부른다.
    function aaa(qqq){
    	// 함수 로직
    }
    
    aaa(function(){})
  • 표현식으로 작성된 callback 함수를 화살표 함수로 바꾸면 다음과 같이 표현할 수 있다.
    aaa(() => {})

📂 callback 함수를 왜 사용하는 걸까?

function aaa(qqq){
	// 외부 API에 데이터 요청하는 로직
	// ...
	// ...
	// 요청 끝!
	const result = "요청으로 받아온 데이터 결과값"
	qqq(result) // 요청 끝나면 qqq 실행시키기
}

aaa(result) => {
	console.log("요청이 끝났습니다.")
	console.log("요청으로 받아온 데이터는" + result + "입니다")
}
  • 특정한 API 요청이 끝난 뒤, 그 결과 값을 가지고 다른 요청을 실행시켜야 하는 상황을 가정해본다.
  • 그럴 때 이런 식으로 callback 함수를 사용해서 요청을 실행할 수 있다.
  • async/awaitpromise 문법이 아직 존재하지 않았던 시기에는 callback 함수를 이용해 데이터를 요청하고 처리했다.

📂 비동기 실습( callback-promise-async/await )

📌 Callback

const onClickCallback = () => {
	// 첫번째 랜덤숫자 api 요청
  const aaa = new XMLHttpRequest();
  aa.open("get", "http://numbersapi.com/random?min=1&max=200");
  aa.send();
  aa.addEventListener("load", (res: any) => {
    const num = res.target.response.split(" ")[0];

		 // 두번째 posts api 요청
	   const bbb = new XMLHttpRequest();
			bb.open("get", `https://koreanjson.com/posts/${num}`);
	    bb.send();
	    bb.addEventListener("load", (res: any) => {
	      const userId = JSON.parse(res.target.response).UserId;
 
			 // 세번째 UserId api 요청   
		   const ccc = new XMLHttpRequest();
		    cc.open("get", `https://koreanjson.com/posts?userId=${userId}`);
		    cc.send();
		    cc.addEventListener("load", (res: any) => {
					console.log(res)
		      console.log("최종 결과값 !!!!!!!!!");
		      console.log(JSON.parse(res.target.response));
      });
    });
  });
};

🎯 콜백 지옥 (Callback Hell)

지금은 총 3회의 요청만 들어갔지만, API 요청이 2~3번 정도만 더 중첩되어도 코드의 가독성이 심각하게 떨어지게 된다.

📌 Promise

const onClickPromise = () => {
  axios
    .get("http://numbersapi.com/random?min=1&max=200")
    .then((res) => {
      const num = res.data.split(" ")[0];
      return axios.get(`https://koreanjson.com/posts/${num}`);
    })
    .then((res) => {
      const userId = res.data.UserId;
      // prettier-ignore
      return axios.get(`https://koreanjson.com/posts?userId=${userId}`)
    })
    .then((res) => {
      console.log(res.data);
    });
};

💡 axios를 사용하는 이유

  • promise란 자바스크립트의 비동기 처리, 그 중에서도 특히 외부에서 많은 양의 데이터를 불러오는 작업에 사용되는 객체다.
  • promise 객체를 이용해서 만든 라이브러리가 axios
  • axios 뿐만 아니라 데이터 통신에 사용되는 현대 라이브러리들은 대부분 Promise를 기반으로 만들어져 있다.
  • promise를 사용할 경우 각 요청들이 체인처럼 연결되는데, 이러한 것을 프로미스 체인(Promise chain) 또는 프로미스 체이닝(Promise chaining)이라고 부른다.

🎯 Promise가 직관적이지 못한 이유

const onClickPromise = () => {
  console.log("여기는 1번입니다~");
  axios
    .get("http://numbersapi.com/random?min=1&max=200")
    .then((res) => {
      console.log("여기는 2번입니다~");
      const num = res.data.split(" ")[0];
      return axios.get(`https://koreanjson.com/posts/${num}`);
    })
    .then((res) => {
      console.log("여기는 3번입니다~");
      const userId = res.data.UserId;
      // prettier-ignore
      return axios.get(`https://koreanjson.com/posts?userId=${userId}`)
    })
    .then((res) => {
      console.log("여기는 4번입니다~");
      console.log(res.data);
    });
  console.log("여기는 5번입니다~");
};

  • axios를 이용한 비동기 작업이 TaskQueue 안에 들어가, 실행 순서가 뒤로 밀렸기 때문이다.

📌 Async/Await

const onClickAsyncAwait = async () => {
  console.log("여기는 1번입니다~");
  // prettier-ignore
  const res1 = await axios.get("http://numbersapi.com/random?min=1&max=200");
  const num = res1.data.split(" ")[0];

  console.log("여기는 2번입니다~");
  const res2 = await axios.get(`https://koreanjson.com/posts/${num}`);
  const userId = res2.data.UserId;

  console.log("여기는 3번입니다~");
  // prettier-ignore
  const res3 = await axios.get(`https://koreanjson.com/posts?userId=${userId}`)
  console.log(res3.data);
  console.log("여기는 4번입니다~");
};

✅ 매크로 태스크 큐 vs 마이크로 태스크 큐


매크로 태스크 큐 (MacroTaskQueue)

setTimeout, setInterval 등이 들어가는 큐

마이크로 태스크 큐 (MicroTaskQueue)

Promise 등이 들어가는 큐

💡 태스크 큐들 간의 실행 우선순위

  • 매크로 태스크 큐와 마이크로 태스크 큐가 부딪힐 경우에는 마이크로 태스크 큐가 우선순위를 가져온다.
export default function EventLoopWithQueuePage() {
  const onClickTimer = () => {
    console.log("===============시작~~===============");

    // 비동기 작업 (매크로큐에 들어감)
    setTimeout(() => {
      console.log("저는 setTimeout! 매크로 큐!! 0초 뒤에 실행될 거예요!!!");
    }, 0);

    new Promise((resolve) => {
      resolve("철수");
    }).then((res) => {
      console.log("저는 Promise! - 1!! 마이크로 큐!! 0초 뒤에 실행될 거예요!!!");
    });

    // 비동기 작업 (매크로큐에 들어감)
    setInterval(() => {
      console.log("저는 setInterval! 매크로 큐!! 1초 마다 계속 실행될 거예요!!!");
    }, 1000);

    let sum = 0;
    for (let i = 0; i <= 9000000000; i += 1) {
      sum = sum + 1;
    }

    new Promise((resolve) => {
      resolve("철수");
    }).then((res) => {
      console.log("저는 Promise! - 2!! 마이크로 큐!! 0초 뒤에 실행될 거예요!!!");
    });

    console.log("===============끝~~===============");
  };

  return <button onClick={onClickTimer}>시작</button>;
}

profile
1년차 프론트엔드 개발자

0개의 댓글