function delay() {
for (var i = 0; i < 100000; i++);
}
function foo() {
delay();
bar();
console.log("foo!");
}
function bar() {
delay();
console.log("bar!");
}
function baz() {
console.log("baz!");
}
setTimeout(baz, 10);
foo();
- 자바스크립트의 가장 큰 특징 중 하나 -> 단일 스레드 기반의 언어
- 단일 스레드이지만, 자바스크립트가 사용 되는 환경을 생각해보면 많은 작업이 동시에 처리되고 있다.
- 웹 브라우저 = 애니메이션 효과를 보여주면서 + 마우스 입력을 받아서 처리
- Node.js 기반의 웹서버 = 동시에 여러개의 HTTP 요청을 처리
-> 자바스크립트는 어떻게 동시성을 지원하는 것일까?
동시성
- 하나의 시스템이 여러 작업을 동시에 처리하는 것처럼 보이게 하는 것이다.
- 실질적으로 한번에 하나의 작업만을 처리한다.
- 멀티태스킹과 유사할 수 있으나, 멀티태스킹은 하나의 컴퓨터에서 여러 개의 프로그램이 동시에 실행되는 것.
- 동시성은 하나의 작업 내에서 여러개의 서브 태스크를 동시에 처리하는 것.
- 하드웨어적인 측면으로 보았을 때 멀티태스킹이 동시성보다 상위의 인자로 위치함.
- 병렬성: 여러 작업을 실제로 동시에 처리하는 것, 여러 CPU 또는 코어를 사용하여 병렬로 처리할 수 있음.
ECMAscript에는 이벤트 루프가 없다
- 실제로 V8과 같은 엔진은 단일 호출 스택(Call Stack)을 사용.
- 요청이 들어올 때마다 해당 요청을 순차적으로 호출 스택에 담아 처리.
그러면 비동기 요청은 어떻게 이뤄지며, 동시성의 대한 처리는 누가 하는 걸까?
- 자바스크립트 엔진을 구동하는 환경(브라우저, Node.js)

- 비동기 호출을 위해 사용하는 setTimeout, XMLHttpRequest 와 같은 함수들은 Javascript 엔진이 아닌 Web API 영역에 따로 정의되어 있다.
- ES6 이후 부터는 조금 달라졌지만, ECMAScript 스펙에 이벤트 루프 관련 내용이 없는 이유
또한 이벤트 루프, 태스크 큐 와 같은 장치 또한 Javascript 엔진 외부에 구현

- Node.js는 비동기 I/O 를 지원하기 위해 libuv 라이브러리 사용.
- libuv 가 이벤트 루프를 제공함.
- Javascript 엔진은 비동기 작업을 위해 Node.js의 API를 호출
- 이때 넘겨진 콜백은 libuv 의 이벤트 루프를 통해 스케쥴되고 실행됨.
자바스크립트가 '단일 스레드' 기반의 언어라는 말은, Javascript 엔진이 단일 호출 스택 을 사용한다는 관점에서만 사실이다.
실제 Javascript가 구동되는 환경(Node.js 등) 에서는 주로 여러 개의 스레드가 사용되며, 이러한 구동 환경이 단일 호출 스택을 사용하는 Javascript 엔진과 상호 연동하기 위해 사용되는 장치가 Event Loop.
단일 호출 스택과 Run-to-Completion
- 하나의 함수가 실행되면 이 함수의 실행이 끝날 때까지는 다른 어떤 작업도 중간에 끼어들지 못한다는 의미.
functiondelay() {
for (var i = 0; i < 100000; i++);
}
functionfoo() {
delay();
bar();
console.log('foo!');
}
functionbar() {
delay();
console.log('bar!');
}
functionbaz() {
console.log('baz!');
}
setTimeout(baz, 10);
foo();

- 전역 환경에서 실행되는 코드는 한 단위의 코드블록으로써 가상의 익명함수로 감싸져 있다고 생각하는것이 좋다. -> 전역 실행 컨텍스트인듯.
- setTimeout 함수는 브라우저에게 타이머 이벤트를 요청한 후 스택에서 제거된다.
- 결론적으로 setTimeout에 의해 등록된 baz 콜백 함수는 지정된 10ms보다 더 늦게 실행됨.
태스크 큐, 이벤트 루프
setTimeout 함수를 통해 넘긴 baz 콜백 함수는 어떻게 적절한 시점에 실행될 수 있을까?
바로 이 역할을 하는 것이 태스크 큐와 이벤트 루프이다.
- 태스크 큐: 콜백 함수들이 대기하고 있는 큐
- 이벤트 루프: 호출 스택이 비워질 때마다 큐에서 콜백 함수를 꺼내와서 실행하는 역할
- 코드가 처음 실행되면 이 코드는 현재 실행중인 태스크가 된다.
- 코드를 실행하는 도중 10ms가 지나면 브라우저의 타이머가 baz 콜백 함수를 바로 실행하지 않고 태스크 큐에 추가한다.
- 이벤트 루프는 현재 실행중인 태스크가 종료되자 마자 태스크 큐에 대기중인 태스크를 호출 스택에 적재한다.
- foo 가 실행을 마치고 호출 스택이 비워지면, 현재 실행중인 태스크는 종료 -> 이벤트 루프가 태스크 큐의 대기중인 태스크 baz 를 호출 스택에 추가
MDN의 이벤트 루프 가상코드
while(queue.waitForMessage()) {
queue.processNextMessage();
}
- waitForMessage() 메서드는 현재 실행중인 태스크가 없을 때 다음 태스크가 큐에 추가될 때 까지 대기하는 역할.
- 모든 비동기 API들은 작업이 완료되면 콜백 함수를 태스크 큐에 추가한다.
- 이벤트 루프는 '현재 실행중인 태스크가 없을 때'(주로 호출 스택이 비워질 때) 태스크 큐의 첫 번째 태스크를 꺼내와 실행한다.
function delay() {
for (var i = 0; i < 100000; i++);
}
function foo() {
delay();
console.log('foo!');
}
function bar() {
delay();
console.log('bar!');
}
function baz() {
delay();
console.log('baz!');
}
setTimeout(foo, 10);
setTimeout(bar, 10);
setTimeout(baz, 10);
- 아무런 지연 없이 setTimeout 함수가 세 번 호출된 이후 실행을 마치고 호출 스택은 비워진다.
- 약 10ms가 지나는 순간 콜백 함수가 태스크 큐에 추가된다.
- 이벤트 루프는 태스크 큐를 소비시킨다.

비동기 API와 try-catch
- setTimeout 뿐만 아니라 브라우저의 다른 비동기 함수들(addEventListener, XMLHttpRequest), Node.js의 I/O 관련 함수 등 모든 비동기 방식의 API들은 이벤트 루프를 통해 콜백 함수가 실행된다.
$('.btn').click(function() {
$.getJSON('/api/members',function (res) {
});
}catch (e) {
console.log('Error : ' + e.message);
}
});
- getJSON 함수는 브라우저의 XMLHttpRequest API를 통해 서버로 비동기 요청을 보낸 후에 바로 실행을 마치고 호출 스택에서 제거된다.
- 이후 서버 응답을 받은 브라우저는 콜백 B를 태스크 큐에 추가, 이벤트 루프에 의해 실행되어 호출 스택에 추가된다.
- 해당 시점에 A 버튼 클릭에 의핸 콜백 컨텍스트는 이미 호출 스택에서 비워진 상태이기 때문에 B 컨텍스트에서 발생한 에러가 catch로 감지되지 못한다.
$('.btn').click(function() {
$.getJSON('/api/members',function (res) {
}catch (e) {
console.log('Error : ' + e.message);
}
});
});
setTimeout(fn, 0)
setTimeout(function() {
console.log('A');
}, 0);
console.log('B');
- setTimeout 함수는 콜백 함수를 바로 실행하지 않고(호출 스택이 아닌) 태스크 큐에 추가한다.
$('.btn').click(function() {
showWaitingMessage();
longTakingProcess();
hideWaitingMessage();
showResult();
});
showWatingMessage가 호출되고, 태스크 큐에 추가되었을 때 호출 스택에는 순차적으로 각 실행될 태스크들이 쌓인다.
longTakingProcess 가 오래 걸리는 작업이기 때문에 그 전에 showWatingMessage를 호출해서 로딩 메시지를 표기하려 한다.
다만 실제 동작은
1. showWaitingMessage 함수 실행이 끝나고,
2. 렌더링 엔진이 렌더링 요청을 보내도 해당 요청은 태스크 큐에서 이미 실행중인 태스크가 끝나기를 기다리고 있기 때문에 지연 된다.
3. 실행중인 태스크가 끝나는 시점 -> 호출 스택이 비워지는 시점 -> showResult 까지 실행이 끝남
$('.btn').click(function() {
showWaitingMessage();
setTimeout(function() {
longTakingProcess();
hideWaitingMessage();
showResult();
}, 0);
});
- longTakingProcess가 바로 호출 스택으로 쌓이지 않고, 태스크 큐에 추가된다.
- 꼭 렌더링 관련이 아니라도, 실행이 너무 오래 걸리는 코드를 setTimeout 을 사용하여 적절하게 다른 Task로 나누어 주면 전체 애플리케이션이 멈추거나 스크립트가 너무 느리다는 등 예방 가능하다.
setTimeout(callback, 0) 에서 '0' 이라는 숫자는 '즉시'를 의미하지 않는다.
- 브라우저는 내부적으로 타이머의 최소단위(Tick)을 정하여 관리.
- 실제로 그 최소단위만큼 지난 후 태스크 큐에 추가된다.
-> 브라우저 별로 상이함, 크롬: 4ms
- 이를 해결하기 위해 setTmmediate 라는 API 존재하나 표준 반열에 오르지 못함.
-> 실제로 이 메서드는 setTimeout 과 같은 최소단위 지연 없이 바로 태스크 큐에 해당 콜백을 추가한다.
Promise와 이벤트 루프
setTimeout(function() {
console.log('A');
}, 0);
Promise.resolve().then(function() {
console.log('B');
}).then(function() {
console.log('C');
});
- Promise도 비동기로 실행된다고 할 수 있으니 태스크 큐에 추가되어 순서대로 A -> B -> C
- Promise는 setTimeout 처럼 최소단위 지연이 없으니 B -> C -> A
- 체인 형태로 연속하여 호출된 then()함수는 어떤식으로 동작할까?
= B -> C -> A
이유는 Promise가 일반 태스크 큐가 아닌, 마이크로 태스크를 사용하기 때문이다.
마이크로 태스크
- 일반 태스크보다 더 높은 우선순위를 갖는 태스크
- 태스크 큐에 대기중인 태스크 < 마이크로 태스크
- setTimeout() 함수는 콜백 A를 태스크 큐에 추가하고
- Promise의 then() 메소드는 콜백 B를 태스크 큐가 아닌 별도의 '마이크로 태스크 큐'에 추가한다.
위의 코드의 실행이 끝나면 태스크 이벤트 루프는 태스크 큐 대신 마이크로 태스크 큐가 비어 있는지 먼저 확인한 뒤, 태스크 큐에 있는 콜백 B를 실행한다.
마이크로 태스크는 태스크 큐보다 우선적으로 실행되며, 마이크로 태스크가 실행되는 동안 마이크로 태스크 큐에 추가로 추가될 수 있다.
-> 마이크로 태스크가 다른 마이크로태스크를 재귀적으로 대기열에 넣으면 다음 마이크로태스크가 처리될 때까지 시간이 오래 걸릴 수 있음.
-> 애플리케이션에서 차단된 UI 또는 일부 완료된 I/O 유휴 상태로 끝날 수 있음.
매크로 태스크
- setTimeout, setInterval, setImmediate, requestAnmationFrame
마이크로태스크
- DOM의 변화를 감지할 수 있게 해주는 클래스
- es6-promise와 같은 폴리필에서 마이크로 태스크를 구현하기 위해 사용.
출처
자바스크립트와 이벤트 루프