"이벤트 루프와 태스크 큐에 대해 설명해주세요."
프론트엔드 면접에서 자주 나오는 질문이자, JavaScript 사용자라면 알아야하는 개념이다.
제목에 '콜 스택'을 넣은 이유는 이벤트 루프를 이해하기 위해선 '콜 스택'에 관해서도 알아야 하기 때문이다.
(그리고 콜 스택을 알아두면 재귀 함수를 쉽게 익힐 수 있다..!)
이 포스팅을 읽기 전에 아래의 개념을 먼저 학습하자.
이벤트 루프(event loop) 정의는 아주 간단합니다. 이벤트 루프는 태스크가 들어오길 기다렸다가 태스크가 들어오면 이를 처리하고, 처리할 태스크가 없는 경우엔 잠드는, 끊임없이 돌아가는 자바스크립트 내 루프입니다.
콜스택(Call Stack)에서 이벤트가 순차적으로 진행되면 이어서 콜백큐(Callback Queue)에서 하나씩 동작을 Loop 시키는 것을 말합니다.
자바스크립트는 싱글 스레드 기반의 언어이고, 자바스크립트 엔진은 하나의 콜 스택(Call stack)만 사용한다. 이는 동시에 한 가지 일만 처리할 수 있음을 의미한다.
ℹ️ 스레드 - 프로세스가 할당받은 자원을 이용하는 실행 흐름의 단위
그런데 자바스크립트로 개발하다보면, 여러 작업이 동시에 처리되는 것을 느낀다. 이벤트가 일어날 때 다른 작업을 진행하기도 하고, 한 번에 여러 개의 HTTP 요청을 처리하기도 한다.
자바스크립트는 싱글 스레드 언어인데 어떻게 이런 일이 가능할까?
이건 바로 '이벤트 루프' 덕분이다.
자바스크립트는 이벤트 루프를 이용해서 비동기 방식으로 동시성을 지원합니다.
자바스크립트는 싱글 스레드 기반의 언어지만, 자바스크립트가 구동되는 환경(Node.js, 브라우저)은 여러 스레드가 사용된다.
여러 스레드가 사용되는 구동 환경이 자바스크립트 엔진과 연동하기 위해 사용되는 장치가 '이벤트 루프' 이다.
How JavaScript works in the browser
자바스크립트 런타임 개념의 시각적 표현
- 이미지 출처: mozilla - 이벤트 루프
그림에서 나온 용어를 정리해보자.
선언한 함수 또는 변수가 객체 형태로 저장된 곳이다.
힙은 콜스택이나 이벤트 루프에 영향을 끼치는 게 없으므로, 이 포스팅에서는 설명을 하지 않겠다.
자바스크립트에서 함수를 호출할 때 보이지 않는 곳에서 무슨 일이 일어나는지에 대해서 살펴보자.
거의 모든 프로그래밍 언어에는 보이지 않는 곳에서 함수 호출을 관리하는 일종의 데이터 구조가 있다.
자바스크립트의 경우에는 콜 스택(call stack)이다.
콜 스택(call stack)
요청이 들어올 때마다 순차적으로 호출 스택을 담아 처리합니다. 함수의 호출들은 "프레임" 스택을 형성한다고 말합니다. 함수가 실행되면 Call Stack에 새로운 프레임이 생기고 처리가 끝나면 없어지는 원리입니다.
function foo(a){ const b = 2; return a + b; } function bar(x){ const y = 1; return foo(x + y); } const baz = bar(1); // 4는 baz에 할당된다.
위 코드의 동작 과정을 알아보겠습니다.
1. bar()가 호출될 때, 첫 번째 프레임이 생성되어 콜스택에 쌓입니다.
2. bar() 안에 있는 foo()가 호출될 때, 콜 스택에 첫 번째 프레임 위로 두 번째 프레임이 생성되어 쌓입니다.
3. foo()가 반환되면 두 번째 프레임은 없어집니다. (이제 콜스택에는 bar 프레임만 남아 있습니다.)
4. bar()가 반환되면 첫 번째 프레임이 없어져 콜스택은 이제 비어있습니다.
Web APIs는 자바스크립트 엔진 밖에 그려져있는 것을 확인하실 수 있습니다.
즉, 자바스크립트 엔진이 아닙니다. 이는 브라우저에서 제공하는 API로 비동기인 setTimeout, Promise 등이 있습니다. Call Stack에서 실행된 비동기 함수들은 모두 Web API를 호출합니다. 그리고 Web API는 콜백 함수를 Callback Queue에 넣습니다.
위 블로그에서 나온 예시를 같이 보자.
console.log(1);
setTimeout(() => {
console.log(2);
}, 0);
console.log(3);
위 코드 콘솔에 1 -> 3 -> 2 순서대로 출력된다.
동작 과정
console.log(1)
이 콜스택에 들어가 실행된다. (타고 들어가는 함수가 없으므로 바로 실행되어 사라진다.)
setTimeout
을 만나 콜스택은 이를 Web API로 보냅니다.
console.log(3)
이 콜스택에 들어가 실행됩니다.
Web API에 있는 setTimeout
은 0ms 후에 해당 콜백을 Callback Queue에 넣는다.
콜스택이 비워지면 Callback Queue에 있는 것을 콜스택에 가져와 console.log(2)
를 실행시킨다.
여기서 중요한 점은 비동기 코드를 만나면 Web API 영역으로 빠지고 그 콜백은 바로 콜스택에 가는 것이 아닌 큐로 빠진다는 점이다.
Web APIs에서 설명했듯이 콜백 큐는 비동기적으로 실행된 콜백 함수가 보관되는 곳이다. 일종의 콜 스택에 가기 위한 '대기열'이다.
여기에 있는 콜백 함수들은 콜 스택이 비었을 때, 대기열에 들어온 순서대로 수행된다.
1. Microtask Queue
마이크로태스크는 코드를 사용해서만 만들 수 있는데, 주로 프라미스를 사용해 만듭니다. 프라미스와 함께 쓰이는 .then/catch/finally 핸들러가 마이크로태스크가 되죠. 여기에 더하여 마이크로태스크는 프라미스를 핸들링하는 또 다른 문법인 await를 사용해 만들기도 합니다.
2. Animation Frames
requestAnimationFrame
같은 코드.
3. Macrotask Queue
- 외부 스크립트
<script src="...">
가 로드될 때, 이 스크립트를 실행하는 것- 사용자가 마우스를 움직일 때
mousemove
이벤트와 이벤트 핸들러를 실행하는 것setTimeout
에서 설정한 시간이 다 된 경우, 콜백 함수를 실행하는 것- 기타 등등
- 출처: 이벤트 루프와 매크로태스크, 마이크로태스크
실행 순서
Microtask Queue ➡️ Animation Frame ➡️ Macrotask Queue
여기에서 우리는 Macrotask Queue와 Microtask Queue에 관하여 자세히 알아보자.
비동기 작업들은 태스크 큐라는 저장공간에 들어가게 된다.
태스크 큐는 발생한 순서대로 큐에 쌓이고 이벤트 루프에 의해 처리된다.
태스크큐는 매크로 태스크 큐와 마이크로 태스크 큐로 구분할 수 있다.
(매크로 태스크와 마이크로 태스크에 속하는 작업은 위에서 설명했다.)
위 gif를 보면 알 수 있듯이 항상 마이크로 태스크의 작업이 매크로 태스크의 작업보다 먼저 처리된다.
이벤트 루프는 다음을 반복한다.
콜스택이 비었는지 지속적으로 확인한다.
콜스택이 비면 제일 먼저 마이크로 태스크 큐를 확인하고 가장 오래된 태스크부터 꺼내서 콜스택으로 전달하는데, 이걸 마이크로 태스크 큐가 텅 비어있을때까지 수행한다.
모든 마이크로 태스크가 처리된 직후, 렌더링 작업이 필요하면 렌더링을 수행한다.
매크로 태스크 큐를 확인한다.
매크로 태스크 큐에서 가장 오래된 태스크 하나를 꺼내 콜스택에 전달해준다.
다시 1번 으로 돌아간다.
2번 ~ 5번을 그림으로 나타내면 다음과 같다.
⭐ 중요한 점
자바스크립트 엔진은 매크로태스크 하나를 처리할 때마다 또 다른 매크로태스크나 렌더링 작업을 하기 전에 마이크로태스크 큐에 쌓인 마이크로태스크 전부를 처리한다.
👉그래서 마이크로 태스크가 모두 처리되기 전까지는 UI렌더링이나 네트워크 요청이 절대 일어나지 않는다.
이렇게 직접 그림을 그려가며 공부했는데, 면접 때 이에 관련되어서 질문이 나오면 어떻게 답을 해야할까?
화이트보드가 있다면 그림을 그려가며 설명하면 좋을텐데....
따로 정리를 해야할 것 같다.
이론적으로 아는 것도 도움이 되겠지만 이런 스펙들이 실제 개발에서 어떻게 사용되고 인지하고 개발했는지를 설명할수 있다면 좋을것 같아요