자바스크립트 엔진을 공부하면서 Callback Queue와 Task Queue에 대해 알아본 적이 있다. 그때는 전체적인 플로우를 이해하기 위해 간단히 정리했지만, 이번에는 Callback Queue와 Task Queue의 차이점과 역할에 좀 더 집중해서 정리해보려고 한다.
처음 설명할 때는 Callback Queue와 Task Queue를 별개로 다뤘지만, 실제로는 모두 Task Queue의 일부분이다. 그리고 Task Queue는 단일 큐가 아니라, 여러 종류로 나뉘어 있다.
자바스크립트 엔진에서 Callback Queue는 Micro Task Queue라고 불리며, Promise의 .then()이나 MutationObserver 같은 비동기 작업이 담기는 큐이다.
반면, Task Queue는 Macro Task Queue를 가리키며, 여기에 setTimeout, setInterval, I/O 작업 등 시간이 지연되거나 외부와 상호작용하는 작업이 포함된다.
Task Queue 간의 우선순위
Task Queue들의 실행 순서는 어떻게 될까? Micro Task Queue와 Macro Task Queue가 충돌하는 상황에서 어떤 큐가 먼저 실행될까?
정답은 Micro Task Queue가 먼저 실행된다는 것이다.
이런 결과가 나오는 것은 Task Queue의 특징을 통해서 나타나게 되는데 :
- Micro Task Queue는 현재 실행 중인 태스크가 완료된 직후, 렌더링 전에 실행된다.
- 반면, Macro Task Queue는 현재 실행 중인 태스크가 모두 완료되고, 브라우저가 렌더링을 마친 뒤 실행된다.
이러한 특징 때문에 Micro Task Queue가 우선 순위가 더 높다. 이제 실제 코드를 보면서 어떻게 차이가 있는지 알아보자.
export default function EventLoopWithQueuePage() {
const onClickLoop = () => {
console.log("===============시작~~===============");
setTimeout(() => {
new Promise((resolve) => {
resolve("철수");
}).then((res) => {
console.log("저는 Promise! - 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>;
}
어질어질한 코드의 덩어리를 볼 수 있다.
전제 코드를 한번에 정리하지 않고 덩어리씩 정리해보자.
1. setTimeout
setTimeout(() => {
new Promise((resolve) => {
resolve("철수");
}).then((res) => {
console.log("저는 Promise! - SetTimeout!! 마이크로 큐!!");
});
console.log("저는 setTimeout! 매크로 큐!! 0초 뒤에 실행될 거예요!!!");
}, 0);
- setTimeout은 Macro Task Queue에 들어가며, 내부 코드는 바로 실행되지 않는다.
2. 첫 번째 Promise
new Promise((resolve) => {
resolve("철수");
}).then((res) => {
console.log("저는 Promise! - 1!! 마이크로 큐!! 0초 뒤에 실행될 거예요!!!");
});
- Promise는 Micro Task Queue에 들어가고, 내부 작업은 나중에 처리된다.
3. setInterval
setInterval(() => {
console.log("저는 setInterval! 매크로 큐!! 1초 마다 계속 실행될 거예요!!!");
}, 1000);
- setInterval 역시 Macro Task Queue에 들어간다.
4. for문
let sum = 0;
for (let i = 0; i <= 9000000000; i += 1) {
sum = sum + 1;
}
- for문은 Call Stack에서 바로 실행된다. 긴 연산이므로 나머지 비동기 작업들은 대기 상태에 있게 된다.
5. 두 번째 Promise
- 두 번째 Promise도 Micro Task Queue에 들어가고, 나중에 실행된다.
- 모든 코드가 실행되고, Call Stack이 비워졌기 때문에 Task Queue 작업이 하나씩 실행된다.
6. 첫 번째 Micro Task Queue
- Micro Task Queue의 우선 순위가 더 높기 때문에, Micro Task Queue의 작업이 실행된다.
7. 두 번째 Micro Task Queue
- Promise 1의 작업이 끝나게 되면 Promise 2의 작업이 바로 시작된다.
8. 첫 번째 Macro Task Queue
- Micro Task Queue에 더이상 없으면 Macro Task Queue의 작업들이 실행된다.
- 첫 번째 setTimout에는 console.log 외에도 Promise 함수가 있기 때문에 해당 함수는 Micro Task Queue에 들어간다.
9. 세 번째 Micro Task Queue
- setTimeout으로 인해서 Micro Task Queue에 새로운 작업이 들어왔기 때문에 Interval 대신 Promise 작업이 실행된다.
10. 두 번째 Macro Task Queue
- 마지막으로 Interval이 실행되면서 1초마다 반복되서 코드가 실행되는 것으로 모든 작업이 끝나게 된다.
이 코드의 결과는 예상대로 Micro Task가 먼저 실행되고, Macro Task는 이후 실행되는 것을 확인할 수 있다.
응용하기
Micro Task Queue와 Macro Task Queue에 대해서 알아봤는데, 이걸 통해서 자주 사용하고 있지만 어떤 방식으로 동작하는지 이해하지 못했던 부분을 이해할 수 있게 된다.
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>
)
}
Mutation 요청을 보낼 때, 실수로 여러번 요청하는 것을 막기 위해서 isSubmitting 또는 isLoading 같은 state를 통해서 두 번째 요청을 막아주는 기능을 구현해봤을 것이다.
실제로 위와 같이 작업하면, 여러 번 요청을 보냈을 때 글이 중복되서 등록되는 것을 막아줄 수 있다.
근데, 뭔가 이상한 점이 있다.
우리가 알기로는 setState는 함수 내부에서 여러번 사용되어도 제일 마지막에 사용된 것으로 반영한다.
그렇기 때문에 첫 번째 setIsSubmitting(true)는 무시되고 setIsSubmitting(false)만 적용되야 하는 것이다.
이러한 부분은 await와 Micro Task Queue의 관계를 이해하고 있어야 한다.
<!DOCTYPE html>
<html lang="ko">
<head>
<title>이벤트루프</title>
<script>
function onClickLoop() {
console.log("=======시작!!!!=======");
function aaa() {
console.log("aaa-시작");
bbb();
console.log("aaa-끝");
}
async function bbb() {
console.log("bbb-시작");
const friend = await "철수";
console.log(friend);
}
aaa();
console.log("=======끝!!!!=======");
}
</script>
</head>
<body>
<button onclick="onClickLoop()">시작하기</button>
</body>
</html>
위 코드를 실행하면 어떤 결과가 나올까?
=======시작!!!!=======
aaa-시작
bbb-시작
철수
aaa-끝
=======끝!!!!=======
위 결과가 맞는 것 같지만 실제 실행은 전혀 다른 결과를 나타낸다.
이런 결과가 나오는데 결정적인 역할을 하는 개념은 await이다.
콘솔을 보면 결과가 아래와 같이 나온다.
위와 같이 콘솔이 찍히는 이유는 자바스크립트에서 await을 만나게 되는 순간 async를 감싸고 있는 function 즉, bbb() 함수가 하던 일을 중단하고 Micro Task Queue에 들어가게 된다.
그리고 async가 감싸고 있는 함수가 Micro Task Queue에 들어갈 때 실행하던 위치를 기억한다. 그리고 Micro Task Queue에서 빠져나와서 실행될 때는 기억할 위치부터 실행된다.
그렇다면! 아래 코드는 어떤식으로 실행이 될까?
<!DOCTYPE html>
<html lang="ko">
<head>
<title>이벤트루프</title>
<script>
function onClickLoop() {
console.log("=======시작!!!!=======");
function aaa() {
console.log("aaa-시작");
bbb();
console.log("aaa-끝");
}
async function bbb() {
console.log("bbb-시작");
await ccc();
console.log("bbb-끝");
}
async function ccc() {
console.log("ccc-시작");
const friend = await "철수";
console.log(friend);
}
aaa();
console.log("=======끝!!!!=======");
}
</script>
</head>
<body>
<button onclick="onClickLoop()">시작하기</button>
</body>
</html>
=======시작!!!!=======
aaa-시작
bbb-시작
aaa-끝
=======끝!!!!=======
ccc-시작
철수
bbb-끝
배운 정보로는 다음과 같은 결과가 나올거라고 생각되지만
실제 결과는 아래와 같다.
=======시작!!!!=======
aaa-시작
bbb-시작
ccc-시작
aaa-끝
=======끝!!!!=======
철수
bbb-끝
await을 만나게 되면 async를 사용하고 있는 함수부터 Micro Task Queue에 들어간다고 했는데 왜 "ccc-시작"이 먼저 나오는 것일까?
그 이유는 await을 만나게되면 해당 라인은 실행을 한다.
즉, ccc 함수 자체는 실행을 하고 Micro Task Queue에 들어가는 것이다.
이러한 이유를 통해서
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>
)
}
onClickSubmit 함수의 axios.get 요청까지는 보내고 Micro Task Queue로 이동해서 API 요청은 보내지는 것이다.
이런 내용을 모르더라도 단순한 기능 개발에는 전혀 문제가 없지만 기능 개발을 하면서 발생하는 문제들을 해결하는 부분에서는 큰 차이가 나타날 것이다.
'JavaScript' 카테고리의 다른 글
JavaScript Engine (1) | 2024.10.30 |
---|---|
Scope Chain과 Closure, HOF (2) | 2024.10.16 |
RequestAnimationFrame (6) | 2024.10.12 |
Transition & Animation 이벤트 (3) | 2024.10.09 |
스크롤 위치에 따른 오브젝트 조작 (1) | 2024.09.29 |