티스토리 뷰
JavaScript의 런타임 모델은 코드의 실행, 이벤트의 수집과 처리, 큐에 대기 중인 하위 작업을 처리하는 이벤트 루프에 기반하고 있다.
이는 C 또는 Java 등 다른 언어가 가진 모델과는 다르다.
이벤트 루프 (Event Loop)
: Call Stack 내에서 현재 실행중인 task가 있는지 그리고 Event Queue에 task가 있는지 반복하여 확인한다. 만약 Call Stack이 비어있다면 Event Queue 내의 task가 Call Stack으로 이동하고 실행된다.
브라우저는 단일 스레드에서 이벤트 드리븐(event-driven)방식으로 동작한다.
자바스크립트는 단일 스레드 기반의 언어이다.
(💡 자바스크립트가 단일 스레드 기반의 언어라는 말은 '자바스크립트 엔진이 단일 호출 스택을 사용한다'는 관점에서만 사실이다. 실제 자바스크립트가 구동되는 환경(브라우저, Node.js등)에서는 주로 여러 개의 스레드가 사용되며, 이러한 구동 환경이 단일 호출 스택을 사용하는 자바스크립트 엔진과 상호 연동하기 위해 사용하는 장치가 '이벤트 루프'이다.)
하지만 실제 웹 애플리케이션은 여러개의 task가 동시에 처리되는 것처럼 느껴지는데
자바스크립트는 이벤트 루프를 이용하여 비동기 방식으로 동시성(Concurrency)을 지원한다.
아래 그림과 같이 비동기 호출을 위해 사용하는 setTimeout이나 XMLHttpRequest와 같은 함수들은 자바스크립트 엔진이 아닌 Web API 영역에 따로 정의되어 있다. 또한 이벤트 루프와 태스크 큐와 같은 장치도 자바스크립트 엔진 외부에 구현되어있다.
V8엔진을 비롯한 대부분의 자바스크립트 엔진은 크게 Call Stack, Heap이라는 2개의 영역으로 나뉜다.
- Call Stack : 작업이 요청되면(함수 호출) 요청된 작업은 순차적으로 Call Stack에 쌓이게 되고 순차적으로 실행된다. 자바스크립트는 단 하나의 Call Stack을 사용하기 때문에 해당 task가 종료하기 전까지는 다른 어떤 task도 수행될 수 없다.
- Heap : 동적으로 생성된 객체 인스턴스가 할당되는 영역이다. 단순히 메모리의 큰 (그리고 대부분 구조화되지 않은) 영역을 지칭하는 용어이다.
자바스크립트 엔진은 작업이 요청되면 Call Stack을 사용하여 요청된 작업을 순차적으로 실행할 뿐 (Run to Completion), 동시성을 지원하기 위해 필요한 비동기 요청 처리는 자바스크립트 엔진을 구동하는 환경(브라우저 또는 node.js)가 담당한다.
- Event Queue (Task Queue) : 비동기 처리 함수의 콜백 함수, 비동기식 이벤트 핸들러, Timer 함수(setTimeout(), setInterval())의 콜백 함수가 보관되는 영역으로 이벤트 루프(Event Loop)에 의해 특정 시점(Call Stack이 비어졌을 때)에 순차적으로 Call Stack으로 이동되어 실행된다.
→ 콜백 함수들이 대기하는 큐(FIFO) 형태의 배열
아래 코드의 동작 과정을 보면,
function func1() {
console.log('func1');
func2();
}
function func2() {
setTimeout(function () {
console.log('func2');
}, 0);
func3();
}
function func3() {
console.log('func3');
}
func1();
1) 함수 func1이 호출되면 함수 func1은 Call Stack에 쌓인다.
2) 함수 func1은 함수 func2을 호출하므로 함수 func2가 Call Stack에 쌓이고 setTimeout이 호출된다.
3) setTimeout의 콜백함수는 즉시 실행되지 않고 지정 대기 시간만큼 기다리다가 “tick” 이벤트가 발생하면 태스크 큐로 이동한 후 Call Stack이 비어졌을 때 Call Stack으로 이동되어 실행된다.
태스크 큐에 다른 task가 없고, Call Stack이 비어있다면 setTimeout의 콜백함수는 딜레이 직후 처리되지만 다른 task가 있다면 앞선 task들의 처리를 기다려야 하므로 setTimeout의 두번째 인자인 시간값은 "최소 지연 시간"이다.
const s = new Date().getSeconds();
setTimeout(function() {
console.log((new Date().getSeconds() - s) + "초 후 실행됨"); // "2"를 출력, 즉 500밀리초가 지난 후 즉시 실행된 것이 아니라는 것
}, 500)
while (true) {
if (new Date().getSeconds() - s >= 2) {
console.log("좋아요, 2초간 반복했습니다.")
break;
}
}
따라서, 지연시간을 0초로 지정한다고 0밀리초 후에 호출한다는 뜻이 아니다.
(function() {
console.log('시작');
setTimeout(function cb() {
console.log('콜백 1: 콜백 메시지');
}); // has a default time value of 0
console.log('평범한 메시지');
setTimeout(function cb1() {
console.log('콜백 2: 콜백 메시지');
}, 0);
console.log('종료');
})();
// "시작"
// "평범한 메시지"
// "종료"
// "콜백 1: 콜백 메시지"
// "콜백 2: 콜백 메시지"
DOM 이벤트 핸들러도 마찬가지로 동작한다.
function func1() {
console.log('func1');
func2();
}
function func2() {
// <button class="foo">foo</button>
const elem = document.querySelector('.foo');
elem.addEventListener('click', function () {
this.style.backgroundColor = 'indigo';
console.log('func2');
});
func3();
}
function func3() {
console.log('func3');
}
func1();
1) 함수 func1이 호출되면 함수 func1은 Call Stack에 쌓인다.
2) 함수 func1은 함수 func2을 호출하므로 함수 func2가 Call Stack에 쌓이고 addEventListener가 호출된다.
3) addEventListener의 콜백함수는 foo 버튼이 클릭되어 click 이벤트가 발생하면 태스크 큐로 이동한 후 Call Stack이 비어졌을 때 Call Stack으로 이동되어 실행된다.
위 예제들을 정리해보면,
→ 모든 비동기 API들은 작업이 완료되면 콜백 함수를 태스크 큐에 추가한다.
→ 이벤트 루프는 '현재 실행중인 태스크가 없는지'와 '태스크 큐에 태스크가 있는지'를 반복적으로 확인한다.
→ 이벤트 루프는 '현재 실행중인 태스크가 없을 때'(주로 호출 스택이 비워졌을 때) 태스크 큐의 첫 번째 태스크를 꺼내와 실행한다.
참고 자료
🔗 [NHN Cloud Meetup] 자바스크립트와 이벤트 루프
🔗 [javascript.info] 이벤트 루프와 매크로태스크, 마이크로태스크
'개발공부 > 🟨 JavaScript' 카테고리의 다른 글
[JS] 자바스크립트 비동기 처리 (0) | 2023.01.29 |
---|---|
[JS] 호이스팅 (Hoisting) (0) | 2023.01.18 |
[JS] 클로저 (Closure) (0) | 2023.01.09 |
[JS] 실행 컨텍스트 (Execute context) (0) | 2023.01.04 |
[JS] 내가 놓쳤던 자바스크립트 기초문법 정리 (0) | 2022.08.15 |
프론트엔드 개발자 삐롱히의 개발 & 공부 기록 블로그