한양과학기술고등학교

에듀테크 프로젝트 대시보드

이벤트 루프

자바스크립트는 스레드가 하나인데 어떻게 안 멈출까?

JS 엔진은 콜 스택 1개로 한 번에 한 줄만 실행한다. 시간이 걸리는 일(타이머·네트워크)은 브라우저(Web API)에 맡기고, 끝나면 콜백을 큐에 넣는다. 이벤트 루프는 "콜 스택이 비었을 때" 큐에서 콜백을 하나씩 꺼내 올린다. 아래에서 한 단계씩 눌러보며 실행 순서를 눈으로 확인해 보자.

console.log('1: start'); setTimeout(() => console.log('2: timeout'), 0); Promise.resolve().then(() => console.log('3: promise')); console.log('4: end');
단계 0 / 0

콜 스택 Call Stack · LIFO

Web API 브라우저가 처리

이벤트 루프 — 시작하려면 다음 단계를 누르세요.

마이크로태스크 큐 Promise · 우선

태스크 큐 setTimeout · 이벤트

콘솔 출력

콜 스택

지금 실행 중인 함수가 쌓이는 곳. 여기가 비어야만 이벤트 루프가 다음 콜백을 올릴 수 있다. 무한 루프를 돌리면 스택이 안 비워져 화면이 멈춘다.

마이크로태스크가 먼저

콜 스택이 빌 때마다 이벤트 루프는 마이크로태스크 큐를 전부 비운 뒤에야 태스크 큐에서 딱 하나를 꺼낸다. 그래서 Promise.then이 setTimeout보다 항상 먼저.

setTimeout(fn, 0)

"0초 뒤"가 아니라 "지금 하던 일과 대기 중인 마이크로태스크가 모두 끝난 뒤 최대한 빨리"라는 뜻. 최소 지연·큐 대기 때문에 실제로는 몇 ms 뒤에 실행된다.

최종 출력 순서: 1 → 4 → 3 → 2. 1: start와 4: end는 전역 코드라 그 자리에서 바로 실행된다. 3: promise는 마이크로태스크, 2: timeout은 태스크라서 전역 코드가 끝난 뒤 마이크로 → 태스크 순으로 처리된다. Node.js도 큰 틀은 같지만 process.nextTick, 타이머 단계 등 세부 규칙이 조금 다르다.

💡 "비동기"인데 왜 병렬 실행이 아닐까?

JS 코드 자체는 항상 한 스레드에서 한 번에 하나만 돈다. 다만 파일 읽기·네트워크 요청·타이머 같은 느린 작업을 브라우저/OS가 백그라운드에서 대신 해주고, 완료되면 콜백을 큐에 넣어줄 뿐이다. 덕분에 기다리는 동안 스크롤·클릭 같은 다른 일을 처리할 수 있어 "안 멈추는 것처럼" 보인다.

💡 오래 걸리는 계산을 하면 화면이 멈추는 이유

무거운 for 반복이 콜 스택을 오래 차지하면, 그동안 이벤트 루프는 클릭·렌더링 콜백을 큐에서 꺼내지 못한다. 해결책은 작업을 잘게 쪼개 setTimeout으로 나누거나, Web Worker라는 별도 스레드로 계산을 넘기는 것.