모든 글

싱글 스레드인데 왜 안 멈출까 — 이벤트 루프와 이벤트 위임

JS는 한 번에 하나만 처리하는데 어떻게 여러 일이 동시에 도는 것처럼 보일까. 콜 스택·마이크로태스크·매크로태스크로 이벤트 루프를 정리하고, 리스너를 부모 하나에 다는 이벤트 위임까지 퍼블리셔 시선으로 엮는다.

JavaScript는 한 번에 하나만 처리해요.

싱글 스레드죠.

그런데도 setTimeout, fetch, 클릭 핸들러가 동시에 도는 것처럼 보여요.

이 마술의 정체가 이벤트 루프예요.

JS는 오래 걸리는 작업(네트워크 요청, 타이머)을 비동기로 넘겨두고, 끝나면 결과를 큐에 넣어요.

그리고 지금 하던 일(콜 스택)이 다 비면, 큐에서 하나씩 꺼내 실행하고요.

순서엔 규칙이 있어요.

콜 스택이 비워짐
  ↓ ① 마이크로태스크 큐(Promise) 먼저 처리
  ↓ ② 매크로태스크 큐(setTimeout 등) 처리

Promise 콜백(마이크로태스크)이 setTimeout(매크로태스크)보다 먼저 실행돼요.

그래서 이런 출력이 나와요.

console.log('1');
setTimeout(() => console.log('2'), 0);   // 매크로태스크
Promise.resolve().then(() => console.log('3'));   // 마이크로태스크
console.log('4');
// 출력: 1 → 4 → 3 → 2

setTimeout(…, 0)이라도 “지금 당장”이 아니라 “콜 스택 비고, 마이크로태스크 다 처리한 다음”이에요.

이 순서를 알면 “왜 내 로그가 예상과 다른 순서로 찍히지?”가 풀려요.

비동기 문법 자체는 콜백 → Promise → async/await로 진화해 왔고, 지금은 이렇게 읽기 좋게 써요.

const res = await fetch('/api/users');
const data = await res.json();

await는 마법이 아니에요.

그 지점에서 함수를 잠시 멈추고, 결과가 큐에 올라오면 이어서 실행하는 — 이벤트 루프 위의 문법 설탕이죠.

짝으로 알아두면 좋은 게 이벤트 위임이에요.

DOM 이벤트는 자식에서 부모로 전파돼요(버블링). 이걸 이용하면 자식마다 리스너를 달지 않고 부모 하나에만 달아 처리할 수 있어요.

list.addEventListener('click', (e) => {
  const item = e.target.closest('li');
  if (item) select(item.dataset.id);
});   // li마다 리스너를 달지 않아도 된다

목록이 수백 개여도 리스너는 하나.

나중에 li를 동적으로 추가해도 부모가 듣고 있으니 자동으로 처리되고요.

퍼블리셔라면 jQuery 시절 .on('click', 'li', …) 위임을 써봤을 텐데, 그 원리가 이거예요.

그리고 React의 onClick이 내부적으로 이벤트 위임으로 동작해요.

버블링·캡처링을 이해하면 “모달 바깥 클릭 시 닫기”, 드롭다운 같은 UI를 정확히 만들 수 있고요.

이벤트 루프를 알면 비동기 코드의 실행 순서가 보여요 — await 남발로 느려지는 지점, Promise가 왜 먼저 도는지.

이벤트 위임을 알면 동적으로 늘어나는 목록에 리스너를 어떻게 달지, React의 이벤트가 어떻게 도는지가 이해되고요.

싱글 스레드인데 안 멈추는 이유는 결국 하나예요.

오래 걸리는 건 넘기고, 큐에서 순서대로 꺼낸다.

N
NACRE
Writing on less · Seoul
다른 글 보기

댓글

  1. 불러오는 중…