본문 바로가기

JavaScript

Event Loop와 Task Queue의 동작 시각화

 

setTimeout의 실행 순서에 대해 이해하기가 쉽지 않았습니다. 또, Event Loop의 구조 전체를 시각적으로 파악하고 싶었지만, 자료를 찾기 쉽지 않았습니다. 저와 같이 Event Loop가 머릿속에 그려지지 않는 분들에게 이 글이 조금이라도 도움이 되었으면 합니다.

 

Event Loop


JavaScript는 초기에 한 번에 하나의 작업만 처리하도록 설계되었습니다. 하지만 시간이 지나고, 여러 가지 작업을 동시에 처리할 일들이 생기기 시작했습니다. 이벤트루프는 이런 비동기 처리를 지원합니다.

 

간단히 말하면, Task Queue에 있는 작업을 Call Stack으로 이동시키는 역할을 합니다.

 

그리고 좀 더 상세하게는 다음과 같습니다.

  1. Task Queue에 작업이 있으면, Call Stack이 비었는지 계속 체크합니다.
  2. Macrotask Queue가 있으면 Call Stack으로 이동시켜 처리합니다. 만약 Microtask가 있으면 3번으로 넘어갑니다.
  3. Microtask Queue가 있으면 Call Stack으로 이동시켜 전부 처리합니다.
  4. UI를 렌더링 합니다.다시 2번으로 돌아갑니다. (브라우저 환경만 해당됩니다.)

Event Loop 시각화

Call Stack
현재 실행 중인 코드를 후입선출(LIFO)로 관리합니다. JavaScript 메인 스레드가 동기식으로 실행합니다.
WEB APIs
브라우저가 별도의 스레드로 비동기 작업을 처리합니다. 완료되면 Task Queue에 할당합니다.
ex) 이벤트 핸들러, setTimeout, HTTP requests,...

Task Queue

  Microtask Queue Macrotask Queue
공통 앞으로 실행되야 될 콜백 함수, 이벤트를 관리합니다.
Queue에 들어온 순서대로 처리하는 선입선출(FIFO) 대기열이지만, 자료구조는 set입니다.
처리 방식 큐에 있는 태스크를 전부 처리합니다. 큐에 있는 태스크를 하나씩 처리합니다.
예시 Promise.then/catch/finally, await, queueMicrotask(func), ... Web APIs(setTimeout, HTTP requests, DOM api, ...)

 

 

 

Event Loop 실행 순서


동작

아래 코드는 버튼을 클릭해서 숫자를 더하는 리액트 앱입니다. Task Queue의 동작을 이해하기 위해, setTimeout과 Promise를 handleClick 함수에 넣었습니다. 이제 버튼을 클릭하면 각각의 작업이 어떤 순서로 일어나는지 알 수 있습니다. 마지막으로, 렌더링 타이밍 확인을 위해 렌더링을 할 때마다 로그를 찍었습니다.

카운트 앱

 

// 버튼 클릭 리액트 코드
function App() {
	const [clickCount, setClickCount] = useState(0);
	const renderCounter = useRef(0);

	const handleClick = () => {
		// Macro task: setTimeout
		setTimeout(() => {
			setClickCount((prev) => prev + 10);
			console.log("called setTimeout");
		}, 0);

		// Micro task: Promise
		Promise.resolve().then(() => {
			setClickCount((prev) => prev + 1);
			console.log("called Promise");
		});

		// 즉시 실행 함수: end
		(function end() {
			console.log("called end");            
		})();
	};

	useEffect(() => {
		console.log(`Component re-rendered ${++renderCounter.current} times`);
	});

	return (
		<div>
			<p>Count: {clickCount}</p>
			<button onClick={handleClick}>Increase Count</button>
		</div>
	);
}

 

버튼 클릭 Event Loop

 

 

간단히 설명하면,

setTimeout 메서드는 Macrotask Queue로, Promise는 Microtask Queue로 이동하고, end 함수는 즉시 실행됩니다.

그리고 Queue에 남아있는 태스크들은 Microtask => Macrotask 순서로 Call Stack으로 이동하고 실행됩니다.

 

좀 더 상세하게 설명하면 다음과 같습니다.

  1. setTimeout 함수
    1. 함수가 Call Stack에 들어옵니다.
    2. Macrotask Queue로 이동해서 대기합니다.
  2. Promise 함수
    1. 함수가 Call Stack에 들어옵니다.
    2. Microtask Queue로 이동해서 대기합니다.
  3. end 함수
    1. 함수가 Call Stack에 들어옵니다.
    2. Call Stack에서 즉시 처리됩니다.
  4. Promise 함수
    1. Event Loop는 Call Stack이 빈 것을 확인하고,
      우선순위가 높은 Microtask Queue의 태스크를 Call Stack에 올립니다.
    2. 함수가 Call Stack에 들어옵니다.
    3. Call Stack에서 즉시 처리됩니다.
  5. setTimeout 함수
    1. Event Loop는 더 이상 Microtask Queue의 태스크가 없는 것을 확인하고,
      Macrotask Queue의 태스크를 Call Stack에 올립니다.
    2. 함수가 Call Stack에 들어옵니다.
    3. Call Stack에서 즉시 처리됩니다.
  6. Call Stack과 Task Queue에 함수가 더 이상 남아있지 않아, Event Loop는 대기 상태에 들어갑니다.

 

실행 결과

함수 호출 로그

 

콘솔 로그를 살펴보면, 초기 렌더링 이후에 동작은 다음과 같습니다.

  1. (setTimeout과 Promise는 Task Queue로 이동했습니다.)
  2. 즉시 실행 함수 end가 호출 됐습니다.
  3. Microtask Queue의 Promise가 호출 됐습니다.
  4. 렌더링이 일어났습니다.
  5. Macrotask Queue의 setTimeout 메서드가 호출 됐습니다.
  6. 렌더링이 일어났습니다.

 

 

마지막으로

저도 처음에 문서 한 두 개를 봐서는 구조를 정확히 이해하기가 쉽지 않았습니다.

계속 자료를 찾아보며, 내용을 글과 이미지로 정리하니 좀 더 명확히 그려지기 시작했습니다.


이벤트 루프를 이해하면, 비동기 처리가 작동하는 걸 이해하는데 큰 도움이 됩니다.

이 과정이 초보 개발자를 한 걸음 벗어나는 순간이라 생각합니다.

이해가 안 되고 어려울 때는, 글이나 그림으로 정리를 해보는 걸 추천해 드립니다.

 

 

출처

JavaScript Visualizer 9000

Modern JavaScript Tutorial

JavaScript Visualized: Event Loop

모던 리액트 Deep Dive

자바스크립트의 이벤트 루프: 동시성 관리 이해하기

'JavaScript' 카테고리의 다른 글

Failed to load module script - ViteJS Typescript  (0) 2023.07.25