안녕하세요. 프론트 개발자 진돌입니다.
처음 자바스크립트를 접하였을 때, 이해하기 어려웠던 내용이 있었습니다.
자바스크립트는 싱글 스레드 언어이며, 비동기 함수로 작업을 처리하여 오래걸리는 작업의 블로킹 현상을 막을 수 있습니다.
자바스크립트는 싱글 스레드인데 비동기 작업은 어디서 해주며 어떻게 관리를 해주는 것일까라는 의문이 있었습니다.
그 정답 중 하나는 이번 주제인 이벤트 루프입니다.
이벤트 루프 동작에 대해서 이해하기 앞서서 이벤트 루프가 관리하는 각각의 요소에 대해서 이해할 필요가 있습니다. (이미지 출처)
Call Stack은 자바스크립트 프로그램 실행 context를 관리합니다.
어떤 함수가 실행되면 Call Stack에 하나씩 쌓이며 실행 context가 만들어집니다.
console.log() 등 일반적인 자바스크립트 함수가 쌓이고 소모되는 곳입니다.
브라우저에서 활용하는 기능과 상호 작용하는 데 사용되는 브라우저에서 제공해주는 API들 입니다. 일부는 백그라운드에서 비동기 작업을 제공해주고 비동기 작업은 브라우저 자체에서 실행됩니다.
오늘은 이벤트 루프 설명에 필요한 비동기 Web APIs를 좀 더 자세히 살펴보겠습니다.
Web APIs 비동기 함수들은 크게 두가지 방식이 존재합니다.
대표적으로 setTimeout 함수가 있습니다.
setTimeout(() => {
console.log('2000ms');
},100);
setTimeout함수를 예로 보면 100ms 시간을 브라우저에서 비동기로 관리한 후 100ms 지나면 call back을 자바스크립트 엔진에서 실행시키도록 옮겨줄 것입니다.
자바스크립트 엔진에서 실행시키려면 Call Stack에 들어가야 되는데, 이 때 비동기 함수들이 Call Stack에 바로바로 계속 들어가면 충돌이 생길 것입니다.
이를 방지하기 위해 Task Queue라는 것이 있고 setTimeout에 넣어준 callback은 100ms가 지나면 Task Queue로 이동하는 방식으로 구성됩니다.
Task Queue는 뒤에서 설명드리도록 하겠습니다.
최신 Web API들은 대부분 Promise를 반환하는 방식을 제공해줍니다.
대표적으로는 fetch API가 있습니다.
fetch("...")
.then(data => console.log(data))
.catch(error => console.error(error));
해당 Web API의 경우는 브라우저에서 함수가 실행된 후 then, catch 등을 자바스크립트 엔진에서 실행될 수 있도록 옮겨줍니다.
Promise는 callback 방식의 Web API와는 조금 다르게 Microtask Queue로 이동하는 방식으로 구성됩니다.
Web API 콜백 및 이벤트 핸들러가 보관되는 Queue입니다.
(Web API의 callback을 넣어놓기에 Callback Queue 라고 불리기도 함)
Call Stack이 비워지면 이벤트 루프가 Task Queue에 있는 Task를 Call Stack으로 이동하여 실행하도록 하는 목적입니다.
(setTimeout, setInterval 등)
Promise Callback, async await, MutationObserver 등의 callback이 보관되는 Queue입니다.
마이크로 태스크큐는 이벤트 루프가 Call Stack이 비어져있으면 Task Queue 보다 먼저 소모하는 우선순위가 가장 높은 Queue입니다. (렌더링보다도 먼저 실행)
(Promise.then, catch, finally, fetch 등)
이제 본론으로 돌아와서 이벤트 루프에 대해서 살펴보겠습니다.
이벤트 루프는 위에서 설명한 Queue들의 작업을 CallStack으로 옮겨주는 역할을 합니다. (함수의 실행 처리는 자바스크립트 엔진, 브라우저가 처리)
이벤트 루프는 Call Stack이 비어있는지 확인하는 루프를 계속 돌고 있습니다.
루프중 CallStack이 비게 되면 현재 진행중인 작업이 없다 판단하고 Microtask Queue나 Task Queue(Microtask Queue가 우선순위가 높음)에서 대기중인 작업 중 실행가능한 가장 오래된 함수를 Call Stack으로 옮겨줍니다.
이벤트 루프는 위의 과정을 지속적으로 반복합니다.
아래 코드는 이해를 돕기 위해 javascript를 활용하여 야매로 다음 과정을 작성해보았습니다.
while(true){
if(!콜스택이_비어있는가){
return;
}
if(마이크로_태스크큐_대기중인_작업이_있는가){
콜스택에_마이크로_태스크큐_작업_넣기();
return;
}
render()
if(태스크큐_대기중인_작업이_있는가){
콜스택에_태스크큐_작업_넣기();
return;
}
}
Callback 기반의 비동기 Web API는 Task Queue에서 관리한다고 하였습니다.
setTimeout 코드를 통해 해당 과정을 조금 더 자세히 살펴보도록 하겠습니다.
setTimeout(() => {
console.log('2000ms');
},2000);
setTimeout(() => {
console.log('100ms');
},100);
console.log('end');
위에서 설명한 Event loop에 따르면 다음과 같은 과정을 진행할 것입니다.
해당 과정을 설명하는 좋은 gif가 있어서 소개드립니다(출처)
MicroTask Queue도 Task Queue와 동일하게 동작을 하게 됩니다.
다른 점은 위에서 설명했던 것처럼 이벤트 루프가 가장 높은 우선순위로 처리를 하게 됩니다.
이번에도 해당 과정을 설명하는 좋은 gif가 있어서 소개드립니다 (출처)
주의해야할 점은 렌더링보다 우선순위에 있는 작업이라 microtask Queue에 작업 자체가 너무 많이 쌓이게 되면 렌더링 자체가 멈추는 현상이 생길 수 있습니다.
React 개발자에게 친숙한 React 코드를 통해서 이벤트 루프 동작을 살펴보겠습니다.
다음 코드는 x축으로 스크롤되는 리스트와, 리스트에 아이템을 추가하고 추가된 아이템으로 scroll 해주는 button으로 구성되어 있습니다.
const [listData, setListData] = React.useState(data);
const containerRef = React.useRef<HTMLDivElement>(null);
const handleAddItem = () => {
setListData((prev) => [...prev, item]);
containerRef.current?.scrollTo({
left: ref.current?.scrollWidth,
behavior: "smooth",
});
};
return (
<>
<div
ref={containerRef}
style={{
display: "flex",
flexDirection: "row",
gap: 10,
width: "100%",
overflowX: "scroll",
}}
>
{listData.map((item) => (
<div
key={item}
style={{
backgroundColor: "tomato",
width: 300,
height: 300,
}}
>
<div style={{ width: 300 }}>{item}</div>
</div>
))}
</div>
<button
type="button"
onClick={handleAddItem}
>
+
</button>
</>
);
해당 코드는 의도대로 동작하지 않습니다.
왜 동작안하는걸까요?
위에서 살펴본 Event Loop의 동작과정을 통해서 이해해보겠습니다.
위에서 살펴본 Evnet Loop 과정으로 하나씩 짚어보니 어떤 문제가 있는지 알겠네요.
React에서 batch update 처리를 하면서 Microtask Queue에서 상태업데이틀 하였기 때문입니다.
이벤트 루프에서 Microtask Queue는 항상 먼저 실행되는 Queue라고 하였습니다.
의도대로 동작시키기 위해서 이벤트 루프의 동작 원리를 활용해보겠습니다.
...
const handleAddItem = () => {
setListData((prev) => [...prev, item]);
setTimeout(() => {
ref.current?.scrollTo({
left: ref.current?.scrollWidth,
behavior: "smooth",
});
}, 0);
};
...
scrollTo 함수를 setTimeout Callback에 넣어주었습니다.
좋아요! 의도대로 아주 잘 동작하네요.
이번에도 이벤트 루프 동작 과정을 살펴보겠습니다.
여기까지 이벤트 루프의 동작을 통해서 보니 왜 동작하는지 명확히 알 수 있었습니다.
여기까지 자바스크립트 이벤트 루프 동작에 대해서 자세히 살펴보았습니다.
틀린 부분이 있으면 댓글로 자유롭게 피드백 부탁드리겠습니다.
감사합니다.
MDN
lydiahallie님의 Event Loop 설명
인파님의 Event Loop 설명
React Batch Update