Index

콜백과 비동기 처리
나만의 axios
매크로 태스크 큐 vs 마이크로 태스크 큐
await과 마이크로큐의 관계

intro

Callback & 비동기 처리

콜백함수란? 함수의 인자로 들어가는 함수

function aaa(qqq){
	// 함수 로직
}

aaa(function(){})

위와 같은 코드에서, aaa 함수의 인자에 들어가는 **function(){}**를 callback 함수라고 부른다.

표현식으로 작성된 callback 함수를 화살표 함수로 바꾸면 다음과 같이 표현할 수 있다.

aaa(() => {} )

그렇다면, 이러한 callback 함수를 왜 사용하는 걸까?

특정한 API 요청이 끝난 뒤, 그 결과 값을 가지고 다른 요청을 실행시켜야 하는 상황을 가정해보자. 그럴 때 이런 식으로 callback 함수를 사용해서 요청을 실행할 수 있다.

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

aaa(result) => {
	console.log("요청이 끝났습니다.")
	console.log("요청으로 받아온 데이터는" + result + "입니다")
}

async/await나 promise 문법이 아직 존재하지 않았던 시기에는 callback 함수를 이용해 데이터를 요청하고 처리했다.

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

콜백 실습

<!DOCTYPE html>
<html lang="en">
	<head>
		<title> 콜백과 친구들 </title>
		<script>
			const myCallback = ()=>{
				// 1.랜덤한 숫자를 가지고 오는 API
				// 2.posts/랜덤숫자
				// 3.랜덤숫자 번 게시글 작성한 유저가 쓴 다른 글
			}
			const myPromise = ()=>{

			}
			const myAsyncAwait = ()=>{

			}

		</script>
	</head>
	<body>
			<button onclick={onClickCallback}>Callback 요청하기</button>
      <button onclick={onClickPromise}>Promise 요청하기</button>
      <button onclick={onClickAsyncAwait}>AsyncAwait 요청하기</button>
	</body>
</html>

위의 콜백함수 내부에는 1번 로직이완료 후 1번 로직에서 받아온 데이터를 기반으로 2번 로직을 그리고 3번로직 또한 마찬가지로 2번 로직을 완료 후 받아온 데이터를 기반으로 실행.

<!DOCTYPE html>
<html lang="en">
	<head>
		<title> 콜백과 친구들 </title>
		<script>
			const myCallback = ()=>{
				// newXMLHTTPRequest는 자바스크립트에 내장되어있는 기능입니다.
				const aa = newXMLHTTPRequest()
				aa.open("get",`http://numbersapi.com/random?min=1&max=200`)
				aa.send()
				aa.addEventListener("load",function(res){
					console.log(res)
				})
			}
			const myPromise = ()=>{

			}
			const myAsyncAwait = ()=>{

			}

		</script>
	</head>
	<body>
			<button onclick={myCallback}>Callback 요청하기</button>
      <button onclick={myPromise}>Promise 요청하기</button>
      <button onclick={myAsyncAwait}>AsyncAwait 요청하기</button>
	</body>
</html>

Callback함수 내부에서는 XMLHttpRequest() 객체를 이용해서 데이터를 읽어올 수 있다.

XMLHttpRequest는 서버와 상호 작용하기 위해 사용되는 객체로, 전체 페이지의 새로고침 없이도 URL로부터 데이터를 받아올 수 있다. Ajax 프로그래밍에 주로 사용되며, XHR이라고 줄여 부르기도 한다.

참고 링크: https://developer.mozilla.org/ko/docs/Web/API/XMLHttpRequest

XHR을 이용해서 랜덤 수 생성 API를 요청하고 해당 요청에 대한 응답을 res라는 변수에 담아 콘솔에 찍어보면 아래와 같이 나온다.


이 중 우리에게 필요한 데이터는 target 안에 들어있고 target 객체를 열어보면, 다음과 같이 response가 들어온 것을 확인할 수 있다.

가공해서 API요청 보내기

const myCallback = () => {
  const aa = 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]; // 랜덤 수 추출

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


랜덤 수에 해당하는 게시글 데이터가 response에 들어온 것을 확인할 수 있다.

하지만 응답받은 데이터는 문자열로 되어있기 때문에 가공하거나 활용하기 어렵다.

**JSON.parse()**를 이용해 **문자열을 객체로 바꿔주고,**

해당 데이터의 작성자 정보(UserId)를 이용해서 세 번째 요청을 보내보자

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));
      });
    });
  });
};


두 번째 요청 때와 마찬가지로 **JSON.parse()**를 이용해 문자열을 객체로 바꿔준다.

그렇게 만들어낸 최종 결과값을 콘솔에 찍어보면

최종 코드의 모양

이렇게 요청이 반복될수록 대각선으로 쑥 파이는 형태가 됨.

지금은 총 3회의 요청만 들어갔지만, API 요청이 2~3번 정도만 더 중첩되어도 코드의 가독성이 심각하게 떨어지게 되며 이런 현상을 **콜백 지옥 (Callback Hell)** 이라고 부른다.

Promise: 콜백 지옥 방지
콜백 지옥과 같은 현상을 막고자 나온 것이 바로 **Promise** 다.

이제 똑같은 작업을 Promise 객체를 이용해 진행해 본다.

promise 실습을 진행할 때에는 axios를 사용해본다.
(promise란 자바스크립트의 비동기 처리, 그 중에서도 특히 외부에서 많은 양의 데이터를 불러오는 작업에 사용되는 객체로 이런 promise 객체를 이용해서 만든 라이브러리가 axios. 그렇기 때문에 promise 실습에 axios를 사용.

axios.get() 위에 마우스를 올려보면
요청에 대한 반환 값이 Promise로 들어오는 것을 확인 가능.)

axios 뿐만 아니라 데이터 통신에 사용되는 현대 라이브러리들은 대부분 Promise를 기반으로 만들어져 있다. 또한 Promise 객체에는 async/await를 붙이는 것이 가능.

axios를 사용하지 않고 Promise 객체를 직접 사용하고자 할 때에는 다음과 같이 할 수 있다.

const result = await new Promise((resolve, reject) => {
	const aaa = new XMLHttpRequest();
  aaa.open("get", "http://numbersapi.com/random?min=1&max=200");
  aaa.send();
  aaa.addEventListener("load", (res: any) => {
		resolve(res)
  });
})

// resolve : 요청이 성공했을 경우
// reject : 요청이 실패했을 경우

그렇다면 async/await가 아직 만들어지기 전에는 어떻게 Promise를 사용했을까?

**promise 객체에 제공되는 .then이라는 기능**을 사용했다.

axios.then을 이용해 순수 promise 객체 만으로 데이터를 요청하는 경우를 확인해보자. 이번에는 Callback Hell같은 지옥이 일어나지 않을까?

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);
    });
};

callback에 비해 코드가 간단해졌고 콜백 지옥과 같은 현상도 일어나지 않는다.

promise를 사용할 경우 각 요청들이 체인처럼 연결되는데,

이러한 것을 **프로미스 체인(Promise chain)** 또는 **프로미스 체이닝(Promise chaining)**이라고 한다.

그런데 Promise에도 문제가 있다. 콜백 지옥은 해결했지만, 직관적이지 못하다는 것이다.

다음과 같이 호출에 대한 결과값을 상수에 담아 사용하는 것이 불가능

const onClickPromise = () => {
  const result = 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);
    });
	console.log(result)
};

위와 같은 함수를 실행하면 결과 값이 완전한 데이터가 아니라 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번입니다~");
};


async / await는 promise에만 붙일 수 있다.

axios가 promise 객체를 이용해 만들어진 라이브러리이기 때문에 async/await를 사용할 수 있는 것!!!

나만의 axios 만들어 보기

<!DOCTYPE html>
<html lang="en">
	<head>
		<title>나만의 axios 만들기!</title>
		<script>
			const myaxios = {
				get: ()=>{
					
				},
				post: ()=>{

				}
			}

			const onClickAxios = ()=>{
				// axios 요청하기
				myaxios.get("https://koreanjson.com/posts/1")
			}
		</script>
	</head>
	<body>
		<button onclick="onClickAxios()"> Axios 요청하기 </button>
	</body>
</html>

axios를 사용할 때 . 을 통해 내부에 접근했었다.

그럼 아마도 axios는 객체 형태를 띄고 있었을 것이다.
따라서객체형태의 myaxios를 생성해주도록 하자.

callback으로 만들기

// myaxios 함수
const myaxios = {
				get: (url , callbackArgu)=>{
					const qq = myaxios.get("https://koreanjson.com/posts/1")
					qq.open("get",url)
					qq.send()
					qq.addEventListener("load",()=>{
						return res.target.response
					})
				},
				post: ()=>{

				}
			}

// axios 요청함수
const onClickAxios = ()=>{
				// axios 요청하기
				myaxios.get("https://koreanjson.com/posts/1", ()=>{})
			}

그런데 지금 상태로는 promise가 아니기 때문에 axios를 요청할 때 await와 .then을 사용할 수 없다.
이런 상황에서는 위와 같이 콜백함수를 만들어 앞의 함수를 실행 후 뒤에 함수를 실행하도록 만들어 주어야 한다.
하지만, 이렇게 되면 복잡해지므로 axios와 마찬가지로 promise를 사용해주자.

// myaxios 함수
const myaxios = {
				get: (url)=>{
					return new Promise((resolve,reject) => {
					const qq = myaxios.get("https://koreanjson.com/posts/1")
					qq.open("get",url)
					qq.send()
					qq.addEventListener("load",()=>{
						return res.target.response
					})
				})
				},
				post: ()=>{}
			}

// axios 요청함수
const onClickAxios = async()=>{
		// axios 요청하기
		//1. .then()으로 받기
		myaxios.get("https://koreanjson.com/posts/1").then((res)=>{
			console.log("여기는 .then() 데이터: ",res)
		})

		// 2. await로 받기
		const result = await	myaxios.get("https://koreanjson.com/posts/1"
		console.log("여기는 .then() 데이터: ",result)
}

이렇게 myaxios 부분에 new Promise() 를 달아주면 promise를 이용할 수 있다.
또한 promise를 return해주시면 다른 곳에서 함수를 호출했을 때 return값으로 promise가 남으므로 .then이나 await를 붙일 수 있다.

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

Promise로 실행하는 함수는 태스크 큐에 들어가며 태스크 큐는 딱 하나만 있는 것이 아니라, 여러 개가 동시에 존재한다.

매크로 태스크 큐 (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>;
}

await와 마이크로큐의 관계

만일 실수로 여러번의 등록요청을 보내게 될 경우를 대비해isSubmitting을 통해 두번째 요청부터는 요청을 막아주는 역할을 하는 함수를 만들어 보자

import axoios from 'axios'

export default function IsSubmitting(){
	const [isSubmittind,setIsSubmitting] = useState(false)

	const onClickSubmit = async ()=>{
		// 등록하는 동안은 등록버튼이 작동하지 않도록
		setIsSubmitting(true)

		const result = await axios.get("https://koreanjson.com/posts/1")
		
		// 등록이 완료되었다면 다시 버튼이 동작하도록
		setIsSubmitting(false)

	}
	
	return(
		<button onClick={onClickSubmit} disabled={isSubmitting}> 등록하기 등의 API 요청버튼 </button>
	) 
}

이렇게 작성해주면, 여러번 요청을 보냈을 때 무작위로 글이 등록 되는것을 방지 할 수 있다.

이 부분은 await와 마이크로큐의 관계를 이해하고 있어야 한다.

자바스크립트는 await 를 만나게 되는 순간 async를 감싸고 있는 function 즉, bbb() 함수가 하던일을 중단하고 마이크로 큐에 들어가게 된다.

그리고 async가 감싸고 있는 함수가 마이크로 큐에 들어갈 때는 실행하던 위치를 기억한다. 따라서 마이크로 큐에서 빠져나와 실행될때는 기억한 위치부터 실행이 된다.

try~catch문에서 에러를 캐치하지 못한다?

profile
Strive for greatness

0개의 댓글