싱글 스레드인데 어떻게 동시에 처리하나
아래 코드에서 bar가 먼저 실행되고 3초 뒤 foo가 실행된다는 건 놀랍지 않다.
function foo() { console.log('foo'); }
function bar() { console.log('bar'); }
setTimeout(foo, 3 * 1000);
bar(); // bar → (3초 후) foo내가 걸린 지점은 이거였다. 자바스크립트는 싱글 스레드라서 한 번에 한 가지 일만 한다고 했다. 그런데 bar가 실행되는 동안, 그리고 그 뒤로 3초 동안 — 타이머는 누가 재고 있었을까? 엔진이 한 번에 하나만 할 수 있다면 시간을 재는 일과 코드 실행을 동시에 할 수 없어야 한다. 모순처럼 보였다.
참고 : 이 파트는 정말 잘 정리된 모던 자바스크립트의 이벤트 루프 파트를 읽어보면 좋다.
답: 자바스크립트는 시간을 재지 않는다
모순이 풀리는 지점은 의외였다. 타이머를 재는 건 자바스크립트 엔진이 아니라 브라우저다.
엔진이 가진 건 실행 컨텍스트 스택, 즉 콜 스택 하나뿐이다. "싱글 스레드"의 정확한 의미가 이것이다 — 콜 스택이 하나라서 실행 중인 코드가 끝나야 다음 코드를 실행할 수 있다. 여기까지만 보면 3초를 기다리는 동안 아무것도 못 해야 맞다.
그런데 setTimeout은 사실 자바스크립트 언어(ECMAScript)의 함수가 아니다. 브라우저가 제공하는 호스트 API다. setTimeout(foo, 3000)을 호출하면 엔진은 "3초 뒤에 이 함수를 부탁해"라고 브라우저에 외주를 주고 즉시 다음 줄로 넘어간다. 시간은 브라우저가 엔진 바깥에서 잰다. 브라우저 자체는 멀티 스레드라서 타이머를 재면서 렌더링도 하고 네트워크 요청도 받는다.
3초가 지나면 브라우저는 foo를 콜 스택에 바로 밀어 넣을까? 그러면 실행 중이던 코드 한복판에 끼어들게 되니 안 된다. 대신 태스크 큐라는 대기줄에 등록한다. 그리고 이벤트 루프가 단순한 규칙을 무한 반복한다 — 콜 스택이 비어 있나? 큐에 대기 중인 콜백이 있나? 있으면 스택으로 올린다.
그래서 이벤트 루프와 태스크 큐는 ECMAScript 스펙을 뒤져도 안 나온다. HTML 스펙에 정의되어 있고, 브라우저(Node.js라면 libuv)가 제공한다. "자바스크립트는 싱글 스레드"라는 말의 실체는 이렇다 — 엔진의 콜 스택이 하나일 뿐, 동시성은 브라우저와의 협업으로 만들어진다.
그럼 3초는 보장되나?
아니다. 여기서 이벤트 루프 이해가 실전과 만난다. 규칙을 다시 보면 콜백은 "3초 뒤"가 아니라 3초 뒤 + 콜 스택이 빈 다음에 실행된다. 스택이 안 비면?
setTimeout(() => console.log('1초 뒤에 실행되길 바랐다'), 1000);
const start = Date.now();
while (Date.now() - start < 3000); // 3초 동안 콜 스택을 점유
// 콜백은 1초가 아니라 3초 후에야 실행된다타이머는 정확히 1초 뒤에 만료되어 콜백을 큐에 넣었다. 하지만 while 루프가 스택을 점유하고 있는 동안 이벤트 루프는 손을 못 댄다. setTimeout의 delay는 보장 시간이 아니라 최소 지연 시간이다.
이게 브라우저에서 벌어지면 사용자가 직접 겪는다. 클릭 이벤트의 콜백도 같은 큐에서 기다리기 때문에, 무거운 동기 코드가 스택을 점유하는 동안 버튼은 눌리지 않고 화면은 멈춘다. 프론트엔드에서 "메인 스레드를 막지 마라"는 조언이 전부 이 구조에서 나온다. 해법의 방향도 구조가 알려준다 — 작업을 잘게 쪼개 큐에 나눠 넣거나(스택을 자주 비워주거나), 아예 다른 스레드(Web Worker)로 보내는 것이다. 렌더링이 정확히 무엇을 하고 언제 끼어드는지는 브라우저 렌더링 글에서 다룬다.
자바스크립트가 동시에 여러 일을 하는 게 아니었다. 엔진은 한 번에 하나만 실행하고, 기다리는 일은 전부 브라우저에 외주를 준다. 이벤트 루프는 그 외주 결과를 돌려받는 유일한 창구다.
그런데 이 대기줄, 사실 하나가 아니다. Promise의 콜백은 setTimeout과 다른 줄에 서고, 그 줄은 언제나 먼저 처리된다. 다음 글에서 새치기의 규칙을 다룬다. → Promise.then과 setTimeout, 누가 먼저인가