FEInterview Prep

javascript · high priority

JavaScript 이벤트 루프

Call Stack · Task · Microtask · Render — 싱글 스레드가 비동기처럼 보이는 이유

intermediate 난이도4시간토스카카오네이버당근배민
시작 전
이해도
매우 낮음

학습 개요

탄생 배경

쉬운 설명

복잡한 개념을 실생활 비유로 설명합니다.

은행 창구 한 개 + 번호표 두 종류

창구(Call Stack)는 하나라 한 번에 한 손님만 응대합니다. 일반 번호표(Task)는 한 명 끝날 때마다 한 명씩 불러오고, VIP 번호표(Microtask)는 손님 한 명 끝난 직후 VIP 가 남아있는 동안 계속 불러옵니다. 창문 청소(렌더링) 는 VIP 손님이 모두 빠진 뒤에만 합니다 — 그래서 VIP 가 끝없이 몰리면 창문이 더러운 채로 유지됩니다.

핵심 개념

자바스크립트 엔진(V8/JSC/SpiderMonkey) 은 실행 중인 함수 호출을 **Call Stack** 에 LIFO 로 쌓고, 값은 **Heap** 에 보관합니다. 엔진 자체는 비동기 개념을 모릅니다 — setTimeout, fetch, 이벤트는 엔진이 아닌 **호스트 환경(브라우저/Node)** 이 제공하는 API 입니다. 이벤트 루프는 엔진과 호스트 사이의 **스케줄러** 역할을 합니다.

"엔진"과 "런타임" 의 구분

V8 같은 **엔진** 은 ECMAScript 사양(문법, Job Queue, GC) 만 구현합니다. setTimeout, fetch, addEventListener, 이벤트 루프의 구체적 큐 구조는 모두 **호스트 환경** 책임입니다. 그래서 브라우저와 Node.js 의 이벤트 루프 구현이 세부적으로 다릅니다 (Node 는 libuv 의 phase 기반).

실무 적용

어떤 상황에서 사용하는가

상태를 setState 로 바꾼 직후 DOM 높이를 측정해 레이아웃을 조정하려는데 이전 값이 나온다.

어떻게 적용하는가

React 는 state 업데이트 후 렌더링이 커밋되는 시점이 현재 microtask 범위가 아니라 다음 렌더 프레임 전후이므로, `useLayoutEffect` (paint 이전) 또는 `useEffect` + `requestAnimationFrame` 으로 측정 시점을 미뤄야 정확한 값을 얻는다. 이벤트 루프 상 "DOM 에 반영되는 순간" 이 언제인지를 먼저 그려봐야 디버깅이 빨라진다.

흔한 실수와 안티패턴

  • `await` 뒤 코드가 바로 실행될 거라고 착각 — 실제로는 microtask 로 미뤄짐
  • `setTimeout(fn, 0)` 이 "즉시" 실행될 거라고 믿음 — HTML 명세상 중첩 타이머는 최소 4ms clamp
  • microtask 안에서 무거운 재귀 호출 → 프레임 drop 과 hang 유발
  • Node 에서 `process.nextTick` 재귀 사용 → I/O 이벤트 starvation
  • Promise 체이닝 길이가 성능에 영향 없다고 생각 — 과도한 체이닝은 같은 턴 내 microtask 폭주로 이어질 수 있음

흔한 오해

오해

"이벤트 루프는 자바스크립트 엔진의 기능이다."

교정

엔진이 아니라 호스트 환경(브라우저/Node) 이 정의·구현한다.

왜 중요

V8 같은 엔진은 ECMAScript 의 Job Queue 만 정의하고, 구체적 Task Queue 구조와 렌더 파이프라인은 HTML/libuv 명세에 있다.

오해

"setTimeout(fn, 0) 은 즉시 실행된다."

교정

다음 task 로 미뤄지며, 중첩된 타이머는 최소 4ms clamp 가 걸릴 수 있다.

왜 중요

HTML spec 이 배터리 보호와 지나친 타이머 남용 방지를 위해 clamp 를 규정하기 때문.

오해

"await 는 실행을 진짜로 멈춘다."

교정

함수 본문이 state machine 으로 변환되어 await 뒤부터 microtask 로 재개되는 것이지, 스레드가 blocking 되는 게 아니다.

왜 중요

async 함수는 컴파일 타임에 제너레이터 유사 구조로 변환되고, await 는 Promise.then 으로 desugar 된다.

면접 질문

중급토스카카오배민당근

답변 방향 힌트

Call Stack, Task Queue, Microtask Queue 를 종이에 그리면서 각 시점에 어떤 콜백이 어디로 들어가는지 따라가세요.

반드시 언급할 키워드

  • 동기 코드: 1, 4 가 먼저
  • Promise.then 은 microtask — task(setTimeout) 보다 먼저 flush
  • setTimeout 은 0ms 여도 다음 task 로 미뤄짐
  • 최종 출력: 1, 4, 3, 2
  • microtask 가 task 보다 우선한다는 점을 명시적으로 언급

예상 꼬리 질문

  • `setTimeout(fn, 0)` 이 실제로는 즉시 실행되지 않는 이유는?
  • Promise.then 을 여러 번 체이닝한 경우 microtask 몇 개가 생기나요?

자기 점검

스크롤 올리지 말고 답해보세요. Task 와 Microtask 의 차이와, 한 턴에 각각 몇 개가 실행되는지?

기대 키워드

TaskMicrotaskflushsetTimeoutPromise.then한 개 vs 전부

자주 하는 오해

Microtask 를 "Task 보다 가벼운 버전" 정도로만 이해하면 실행 개수 차이를 놓치기 쉽습니다. Task 는 한 턴당 1개, Microtask 는 큐가 빌 때까지 전부 실행됩니다.

async function 안의 `await Promise.resolve(1)` 뒤 코드는 언제 실행되나요? 다음 번 어떤 단계인가요?

기대 키워드

microtaskthen현재 task 끝난 직후suspend/resume

자주 하는 오해

"await 한 줄이면 다음 task 까지 미뤄진다" 로 오해하는 경우가 많습니다. 실제로는 같은 이벤트 루프 턴의 microtask flush 에서 재개되므로 setTimeout(fn,0) 보다 먼저 실행됩니다.

`setTimeout(fn, 0)` 이 실제로 0ms 뒤에 실행되지 않는 이유는?

기대 키워드

task queue4ms clamp중첩HTML specStack 이 비어야 실행

자주 하는 오해

"0 이면 즉시" 라고 생각하지만, HTML 명세가 5단계 이상 중첩된 timer 에 대해 최소 4ms clamp 를 규정하고, 무엇보다 현재 task + 모든 microtask 가 먼저 끝나야 실행됩니다.

학습 자료