(자바스크립트는 코드 실행, 이벤트 수집과 처리, 큐에 놓인 하위 작업들을 담당하는 이벤트 루프에 기반한 동시성 모델을 가지고 있다. )
- 스레드(thread) : 프로세스 내 실행 단위. 하나일 경우 싱글 스레드(Single thread), N개일 경우 멀티 스레드(Multi thread)라고 한다.
- 동시성(Concurrency) : 여러 작업이 마치 동시에 일어나는 것처첨 보이는 것.
- 병행성(Parallelism) : 여러 작업이 동시에 일어나는 것.
- 브라우저 엔진 : 크롬과 같은 웹 브라우저 엔진으로 다양한 작업을 수행.
- 호출 스택(Call Stack) : 자바스크립트에서는 수행해야 할 함수를 순차적으로 호출 스택에 담아 처리함.
- Stack : 자료구조 중 하나로 선입후출(LIFO, Last In First Out)의 룰을 따른다.
- 콜백 큐(Callback Queue) : 자바스크립트 런타임 환경에서 처리해야 하는 명령어를 임시로 저장하는 대기 큐. ( = Task Queue = Event Queue )
- Queue : 컴퓨터의 기본적인 자료구조의 한가지로, 먼저 집어 넣은 데이터가 먼저 나오는 FIFO(First In First Out) 구조로 저장하는 형식.
- Web API : 웹 브라우저에서 제공하는 API로, Browser API라고도 한다.
위 그림을 살펴보자.
1. 함수를 동기 호출하게 되면 Call Stack에 차곡차곡 쌓여 순차적으로 실행된다.
2. 이때 AJAX나 setTimeout, DOM event 함수를 실행하면 Call Stack에서 Web APIs로 보내진다.
3. 정해진 시간 혹은 이벤트가 발생한 순간에 순차적으로 Callback Queue에 적재한다.
4. Callback Queue에 줄을 선 함수들은 Call Stack에 쌓여있던 것들이 모두 제거되어 깨끗해지면 차례대로 stack에 쌓여 실행된다.
자바스크립트 엔진(V8 엔진: 오픈소스, 구글에서 개발, C++로 작성됨)은 ✔️Memory Heap과 Call Stack으로 구성되어 있다. 자바스크립트는 단일 스레그(Single thread) 프로그래밍 언어로, Call Stack이 하나이다.
✔️Memory Heap
프로그램이 실행될 때 메로리는 기계어 코드가 들어있는 코드 섹션(Code Section)과 데이터를 저장하는 데이터 섹션(Data Section)으로 나뉜다.
데이터 섹션은 다시 3부분으로 나눠지며, 전역 메모리(Global Memory), 스택 메모리(Stack Memory), 힙 메모리(Heap Memory)로 나뉜다.전역 메모리에는 모든 지역 함수 바깥에 선언된 변수를 할당한다. 스택 메모리는 함수 내에 선언된 지역 변수들을 위한 공간이다. 스택 메모리는 해당 함수의 실행이 끝나면 자동으로 해제된다.
힙 메모리(Heap Memory)는 프로그램 실행시 일정량의 힙 메모리를 할당하게 되며, 프로그램 실행 중 필요에 의한 동적 메모리 할당을 위한 공간이 힙 메모리이다.
그림의 우측에 있는 Web API는 자바스크립트 엔진 밖에 위치하고 있다. 즉, 자바스크립트 엔진이 아니라 브라우저에서 제공하는 API이다. Call Stack에서 실행된 비동기 함수는 Web API를 호출하고, Web API는 콜백함수를 Callback Queue에 밀어 넣는다.
비동기적으로 실행된 콜백함수가 보관되는 영역이다.
예를 들어, setTimeout
에서 타이머 완료 후 실행되는 함수, addEbentListener
에서 click 이벤트가 발생했을 때 실행되는 함수 등이 보관되는 것이다.
Call Stack과 Callback Queue의 상태를 반복적으로 체크해, Call Stack이 빈 상태가 되면 Callback Queue의 첫 번째 콜백을 Call Stack으로 밀어 넣는다. (이러한 반복적인 행동을 틱(tick)이라 한다.)
코드가 실행되면 Call Stack에 쌓인다. Stack의 선입후출 룰에 따라 제일 마지막에 들어온 함수가 먼저 실행되며, Stack에 쌓인 함수들이 모두 실행된다.
만약 비동기 함수가 실행될 경우 Web API가 호출된다. Web API는 비동기함수의 콜백함수를 Callback Queue에 밀어넣는다.
EventLoop는 Call Stack이 빈 상태가 되면 Callback Queue에 있는 첫 번째 콜백을 Call Stack으로 이동시킨다.
이러한 반복적인 행동을 통해 Event Loop가 행해지는 것이다.
자바스크립트는 단일 스레드 프로그래밍 언어이기 때문에 한 번에 하나의 테스크만 실행된다. 하지만 Web API, Callback Queue, Event Loop를 통해 멀티 스레드처럼 보여지는 것이다.
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
위 코드의 실행 순서는 아래와 같다.
script start
script end
promise1
promise2
setTimeout
왜 Promise가 먼저 실행된 후에 setTimeout이 실행되는 걸까?
Microtask Queue 개념을 살펴보면 된다.
EventLoop는 우선적으로 Microtask Queue를 확인한다. 만약 Microtask Queue에 콜백이 있다면, 이를 먼저 Call Stack에 담는다. 그리고 Microtask Queue에 더이상 처리해야할 콜백이 없다면 Task Queue를 확인 후 처리한다.
Promise의 then()
콜백은 Task Queue가 아닌 Microtask Queue에 담긴다. 따라서 위 코드에서는 우선순위가 높은 Microtask Queue부터 처리되기 때문에, Promise의 then()
콜백이 실행된 후에 setTimeout
콜백이 실행되는 것이다.
requestAnimationFrame API가 실행되면 콜백이 Animation Frames로 담긴다. (setTimeout이 실행되면 타이머 완료 후 콜백이 Task Queue에 담기는 것과 비슷하다고 생각하면 된다.)
console.log("script start");
setTimeout(function() {
console.log("setTimeout");
}, 0);
Promise.resolve().then(function() {
console.log("promise1");
}).then(function() {
console.log("promise2");
});
requestAnimationFrame(function() {
console.log("requestAnimationFrame");
})
console.log("script end");
위 코드를 실행하면 아래와 같은 순서로 진행된다.
script start
script end
promise1
promise2
requestAnimationFrame
setTimeout
Microtask Queue ➝ Animation Frames ➝ Task Queue 순으로 실행됨을 볼 수 있다.
(이 실행 순서는 크롬 기준으로, 브라우저마다 다를 수 있다.)
코드가 실행되면 Call Stack에 쌓이고, Stack에서는 선입후출 룰에 따라 테스크가 실행된다.
만약 비동기 함수가 실행될 경우, Web API가 호출된다.
Web API는 비동기 함수의 콜백 함수를 Callback Queue에 밀어넣는다. Promise는 Microtask Queue로, setTimeout은 Task Queue로, RequestAnimationFrame은 Animation Frame으로 콜백함수를 밀어넣는다.Event Loop는 Call Stack이 빈 상태인지 확인 후, 콜백을 Call Stack으로 이동시켜 테스크를 처리한다. 이때, 콜백 이동 우선순위는 Microtask Queue, Animation Frames, Task Queue 순으로 이동된다.
💡 요약
자바스크립트는 싱글 스레드 언어로 한 번에 하나의 테스크만 처리가 가능하다. 하나의 함수가 실행되면 이 함수의 실행이 끝날 때까지 어떤 작업도 중간에 끼어들지 못한다. 자바스크립트 엔진은 하나의 호출 스택을 사용하며, 현재 스택에 쌓여있는 모든 함수들이 실행을 마치고 스택에서 제거되기 전까지는 다른 어떠한 함수도 실행될 수 없는 것이다.하지만 이벤트 루프를 통해 비동기 방식으로 동시성을 지원한다.
함수를 호출하면 Call Stack에 차곡차곡 쌓여 순차적으로 실행된다. 이때 AJAX나 setTimeout, DOM event 함수 등 비동기 함수를 실행하면 Web API를 호출한다. Web API는 콜백함수의 정해진 시간이나 이벤트가 발생한 순간에 순차적으로 Callback Queue에 밀어넣으며, Promise는 Microtask Queue에 밀어넣는다.
( Callback Queue 외에도 Microtask Queue가 존재한다. 콜백 큐와 마이크로태스크 큐 모두 콜백함수가 들어간다는 점에서는 동일하지만 어떤 함수를 실행하느냐에 따라 달라진다.
Task Queue(=콜백 큐)에는 setTimeout, setInterval, setImmediate, requestAnimationFrame 등이 적재되며, Microtask Queue에는 Promise, process.nextTick 등이 적재된다. )
이후 Event Loop는 Call Stack이 빈 상태인지 확인 후, 콜백을 Call Stack으로 이동시켜 테스크를 순차적으로 처리한다.
이때 콜백 이동 우선순위는 Microtask Queue가 먼저 이동된 후 Task Queue가 이동된다.
이벤트 루프는 감시자 역할이라고 이해하자.
: 일반적으로 자바스크립트 코드의 성능을 측정할 때, 스택 안에 있는 함수는 성능을 느리게도 빠르게도 만든다. 만약 console.log()한줄만 있다면 코드의 실행은 빠르게 될것이다. 하지만 코드가 길어진다면 느려지게 될것이고, 그 코드들은 스택을 계속 차지하고 있을 것이다. 이러한 상황을 'Blocking Script' 라 부른다.
결론적으로 이벤트루프를 통해서 자바스크립트를 멀티 스레드처럼 작동하게 하는 것이다.