Callback과 비동기 처리
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 + "입니다")
}
비동기 실습( callback-promise-async/await )
Callback 실습
<!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번 로직을 완료 후 받아온 데이터를 기반으로 실행한다.
XMLHttpRequest 란?
XMLHttpRequest는 **서버와 상호 작용하기 위해 사용되는 객체**입니다. 전체 페이지의 새로고침 없이도 URL로부터 데이터를 받아올 수 있다.
Ajax 프로그래밍에 주로 사용되며, XHR이라고 줄여 부르기도 한다.
MDN : https://developer.mozilla.org/ko/docs/Web/API/XMLHttpRequest
XHR을 이용해서 랜덤 수 생성 API를 요청할 때 요청에 대한 응답을 res라는 변수에 담아 콘솔에 찍어보면
아래와 같이 나온다.
이 중 우리에게 필요한 데이터는 target 안에 들어있다.
target 객체를 열어보면, 다음과 같이 response가 들어온 것을 확인할 수 있다.
이 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 실습을 진행할 때에는 axios를 사용할 것이다.
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 객체에 제공되는 .then이라는 기능을 사용했다.
axios와 .then을 이용해 순수 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);
});
};
callback에 비해 코드가 간단해진 것을 확인할 수 있으며 또한 콜백 지옥과 같은 현상도 일어나지 않았다.
promise를 사용할 경우 각 요청들이 체인처럼 연결되는데,
이러한 것을 **프로미스 체인(Promise chain)** 또는 **프로미스 체이닝(Promise chaining)**이라고 부른다.
그런데 Promise에도 문제가 있는데 콜백 지옥은 해결했지만, 직관적이지 못하다.
1. 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의 형태로 들어오는 것을 확인할 수 있다.
2. 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 실습
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를 사용할 수 있는 것.
매크로 태스크 큐 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>;
}