노트

이벤트 루프

Event Loop

프런트엔드#js#browser · 연결된 개념 14개

쉽게 말하면

이벤트 루프는 혼자 일하는 바리스타가 주문을 하나씩 꺼내 끝까지 만드는 방식이에요. 한 잔을 다 만들기 전엔 다음 주문을 못 받으니, 오래 걸리는 잔 하나가 클릭 반응과 화면 갱신까지 멈춰 세워요.

비유가 깨지는 곳 주문 줄이 하나만 있는 건 아니에요. Promise 같은 마이크로태스크는 태스크 하나가 끝날 때마다 전부 비워지고, 화면 그리기는 매 바퀴가 아니라 그릴 때가 됐을 때만 끼어들어요.

자바스크립트를 실행하는 하나의 스레드가 할 일을 큐에서 하나씩 꺼내 처리하는 반복 구조. JS 엔진 자체에는 타이머도 네트워크도 없고, 브라우저(또는 Node.js)가 이 루프를 돌리면서 콜백을 엔진에 넘겨준다.

언어 쪽에서 본 모습

  • JS는 콜 스택(Call Stack) 하나로 동기 코드를 실행한다(스택). 함수 하나가 시작되면 끝날 때까지 다른 코드가 끼어들지 못한다(run-to-completion)
  • 그래서 무거운 동기 루프가 1초 걸리면, 10ms 뒤로 예약한 setTimeout도 1초 넘게 기다린다. setTimeout이 보장하는 것은 "지정한 시간 뒤에 큐에 넣는다"까지이고, 실행 시점은 보장하지 않는다
  • 비동기 작업의 결과는 콜백 형태로 큐에 쌓였다가 스택이 비면 실행된다. Promise와 await은 일반 태스크 큐가 아닌 마이크로태스크(Microtask) 큐를 쓴다

브라우저 쪽에서 본 모습

루프 한 바퀴는 대략 이렇다.

while (true) {
  const task = taskQueue.shift()     // 1. 태스크 하나
  run(task)
  while (microtaskQueue.length)      // 2. 마이크로태스크 전부
    run(microtaskQueue.shift())
  if (isRenderingOpportunity()) {    // 3. 그릴 차례일 때만
    runAnimationFrameCallbacks()     //    rAF 콜백
    render()                         //    스타일·레이아웃·페인트
  }
}
  • 태스크(Task, 매크로태스크): setTimeout·setInterval, 클릭 같은 DOM 이벤트, postMessage, 네트워크 콜백. 스펙상으로는 출처별로 여러 큐가 있고 브라우저가 어느 큐에서 꺼낼지 고른다. 한 바퀴에 하나만 처리한다
  • 마이크로태스크: 큐가 하나뿐이고, 태스크가 끝날 때마다 빌 때까지 전부 처리한다
  • 렌더링: 매 바퀴가 아니라 화면을 갱신할 때(60Hz면 약 16.7ms마다)만 온다. 그 직전에 requestAnimationFrame 콜백이 실행되고 이어서 렌더링 파이프라인이 돈다
console.log('1')
setTimeout(() => console.log('timeout'), 0)
Promise.resolve().then(() => console.log('promise'))
console.log('2')
// 1, 2, promise, timeout

주의

  • "마이크로 → rAF(requestAnimationFrame) → 매크로" 순서로 우선순위가 고정돼 있다는 설명이 흔한데 틀렸다. rAF는 렌더링 차례가 왔을 때만 실행되므로 setTimeout(fn, 0)과의 선후는 프레임 타이밍에 따라 달라진다. 확실한 것은 "마이크로태스크가 다음 태스크보다 먼저 전부 처리된다"뿐이다
  • 태스크 하나가 오래 걸리면(Long Task, 50ms 초과) 그동안 클릭에 반응도, 화면 갱신도 못 한다. INP(Interaction to Next Paint)가 나빠지는 가장 흔한 원인이다. React의 동시성 렌더링은 렌더 작업을 잘게 쪼개 이 문제를 줄인다

출처: HTML Standard — Event loops · MDN — JavaScript execution model: Job queue and event loop

연결된 개념

이 노트를 가리키는 문서

뜻이 가까운 노트

  • 에이전트 루프

    LLM이 도구 호출을 요청하면 실행해 결과를 돌려주고, 도구 호출 없이 답할 때까지 반복하는 구조. 코딩 에이전트도 챗봇형 에이전트도 이 반복 위에 서 있다.

  • 이벤트 버블링·캡처링과 위임

    DOM 이벤트는 대상 요소에서만 끝나지 않고 트리를 따라 전파된다. 루트에서 대상까지 내려가는 캡처링(Capturing Phase), 대상에서 처리되는 타깃 단계(Target Phase), 대상에서 루트로 올라가는 버블링(Bubbling Phase) 순서다.

  • 커넥션 드레이닝과 무중단 재시작

    배포 중 서버를 재시작하는 몇 초 동안 로드밸런서가 그 서버로 요청을 보내면 502가 난다. 먼저 로드밸런서에서 빼고(드레이닝), 진행 중인 요청을 마친 뒤 재시작하고, 준비되면 다시 넣는다.

  • use()로 Promise 읽기

    use()는 렌더 중에 Promise나 Context 같은 리소스의 값을 읽는 React 19 API(Application Programming Interface)다. Promise가 아직 pending이면 컴포넌트가 suspend되어 가장 가까운 suspense의 fallback이 보이고, 끝나면 그 값으로 다시 렌더된다. reject되면 가장 가까운 에러 경계(Error Boundary)로 던져진다.

  • 스트리밍 SSR

    스트리밍 SSR(Streaming Server-Side Rendering)은 서버가 페이지 전체를 다 만들 때까지 기다리지 않고, 준비된 부분의 HTML(HyperText Markup Language)부터 먼저 보내고 느린 부분은 준비되는 대로 같은 응답에 이어서 흘려보내는 렌더링 방식이다. suspense 경계가 나눔의 단위다.

보기 옵션