자바스크립트를 사용하면 가끔씩 코드가 내가 생각했던 흐름대로 흐르지 않고 뒤죽박죽으로 실행되는 경우가 있다.
이는 자바스크립트의 이벤트 루프라는 개념때문이다. 이벤트 루프를 이해하지 못하는 것은 자바스크립트를 다루기 힘들정도로 중요한 개념이며 이벤트 루프에 대해 살펴보자.
자바스크립트는 단일(싱글) 스레드 언어이다. 이 말은 한 번에 하나의 작업만 처리할 수 있다는 뜻이다.
자바스크립트를 이용하다보면 여러가지 이벤트와 동시에 처리해아하는 경우가 많다. 동시성 문제를 해결하기 위해 자바스크립트는 이벤트 루프와 함께 비동기 프로그래밍 모델을 사용한다.
먼저 JS라고 적힌 네모 부분이 자바스크립트 코드를 실행하는 엔진 부분이며 오른쪽 그림 중 Web APIs는 JS가 아닌 멀티스레드의 브라우저, Node.js 환경에서 처리하는 비동기 함수다. 이러한 환경에서 이벤트 루프가 어떻게 동작하는지 살펴보자
Heap : 힙은 메모리가 할당되는 곳이며 선언한 변수, 함수의 값이나 객체 참조 값이 담겨져 있다.
Call Stack : 함수 호출 시 이곳에 저장되며 스택의 특성 LIFO(Last In First Out)의 특성을 지니고 있다.
함수를 여러번 호출하면 맨 마지막에 있는 함수를 하나씩 처리해 나간다. JS는 단일 스레드로서 반드시 한 번에 하나의 작업을 수행한다.
Web APIs : Web API는 JS가 아닌 브라우저에서 제공하는 API로 DOM, Ajax, TimeOut의 비동기 함수가 있다. Call Stack에서 Web APIs 함수들이 있으면 Call Stack이 아닌 Web APIs 영역에서 처리를 한다.
이때 Web APIs는 JS가 아닌 멀티 스레드의 브라우저에서 실행하기에 JS의 단일 스레드 구조와 별개로 동작한다. 이 말은 브라우저의 Web APIs 비동기 함수가 처리되면서 동시에 JS의 Call Stack도 다른 작업을 계속해서 처리할 수 있다는 뜻이다.
Callback Queue : 콜백 큐에서는 Web APIs의 비동기 처리가 끝난 함수의 콜백 함수를 순서대로 저장한다.
큐의 특성상 FIFO(First In First Out) 먼저 들어온 콜백 함수를 제일 먼저 처리한다.
Event Loop : 이벤트 루트는 만약 JS의 Call Stack이 비어있다면 그제서야 Callback Queue에 존재하는 콜백 함수를 순서대로 Call Stack에 옮겨 작업을 처리하는 역할을 한다.
Callback Queue의 함수들은 바로 실행되지 않는다. 기다렸다가 반드시 Call Stack이 비어있을때에 Callback Queue 함수들이 순서대로 실행된다.
Call Stack에서 오래걸리는 작업을 하게된다면 Callback Queue의 함수들이 계속해서 기다리게 된다. 그래서 JS를 이용한다면 시간이 오래걸리는 연산은 피하는게 좋다.
function main(){
console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
console.log('3');
}
main();
이 코드를 만약 Node.js 환경에서 실행하면 실행 순서가 어떻게 될까?
Call Stack | Web APIs | Callback Queue |
---|---|---|
main() |
Call Stack | Web APIs | Callback Queue |
---|---|---|
console.log('1') | ||
main() |
Call Stack | Web APIs | Callback Queue |
---|---|---|
main() |
Call Stack | Web APIs | Callback Queue |
---|---|---|
setTimeout(() => { console.log('2'); }, 0); | ||
main() |
Call Stack | Web APIs | Callback Queue |
---|---|---|
main() | setTimeout(() => { console.log('2'); |
Call Stack | Web APIs | Callback Queue |
---|---|---|
console.log('3') | ||
main() | setTimeout(() => { console.log('2'); }, 0); |
Call Stack | Web APIs | Callback Queue |
---|---|---|
main() | console.log('2') |
Call Stack | Web APIs | Callback Queue |
---|---|---|
console.log('2') |
Call Stack | Web APIs | Callback Queue |
---|---|---|
console.log('2') |
Call Stack | Web APIs | Callback Queue |
---|---|---|
큐의 종류는 Task Queue, MicroTask Queue, Animation Frames 총 3가지가 있다. 이벤트 루프에서 여러 Queue들에 우선순위를 부여해 어떤 Queue를 먼저 수행할지 결정한다.
Task Queue(Macrotask Queue) : Task Queue는 Macrotask Queue라고도 불리며 setTimeout(), setInterval(), setImmediate()와 같은 작업을 넘겨받는다.
Microtask Queue : Microtask Queue는 Promise, async/await, process.nextTick, Object.observe, MutationObserver과 같은 비동기 호출을 넘겨받는다. 이는 Task Queue보다 우선순위가 높으며 먼저 콜 스택에서 처리된다.
Animation Frames : Animation Frames는 requestAnimationFrame과 같이 브라우저 렌더링과 관련된 작업을 넘겨받는다. 우선순위는 Microtask보다는 낮고 Task Queue보다는 높다.
정리하자면 3가지 종류의 큐가 있고 각 큐마다 넘겨받는 작업의 종류가 다르다. 각 큐는 우선순위를 가지며 순위가 높은 순서대로 콜 스택에서 처리된다.
Microtask Queue > Animation Frames > Task Queue