[TIL] 자바스크립트 비동기 처리 방식 (feat. 이벤트 루프)

📌 들어가며

자바스크립트는 흔히 싱글 스레드 언어라고 한다. 즉, 한 번에 하나의 작업만 수행할 수 있는 구조처럼 보인다.
그런데 우리는 실제로 웹사이트에서 네트워크 요청을 보내고, UI가 끊김 없이 렌더링되고, 이벤트도 정상적으로 처리되는 경험을 한다.

 

어떻게 싱글 스레드가 멀티 스레드 처럼 동시에 여러 일을 하는 것처럼 보일까?
오늘은 그 비밀인 자바스크립트의 비동기 처리 모델을 정리했다.


 

📌 자바스크립트의 싱글 스레드

자바스크립트는 원래 브라우저의 DOM 조작을 안전하게 하기 위해 싱글 스레드로 설계되었다.

여러 스레드가 동시에 DOM을 만지면 Race Condition이 발생할 수 있기 때문이다.

 

그래서 JS 엔진은 하나의 콜 스택(Call Stack) 만 두고 코드를 순차적으로 실행한다.

콜 스택이란?

  • JS가 실행할 작업을 관리하는 스택 자료구조
  • 동기 코드가 오래 걸리면 콜 스택을 점유(pop 하지 못함)해서 브라우저가 멈춘다
    JS에서 비동기 모델이 중요한 이유

 

 즉, JS 자체는 절대 동시에 두 작업을 처리하지 않는다.

그런데도 동시에 처리되는 것처럼 보이는 이유가 바로 다음 설명할 구조때문이다.


📌 비동기 처리의 비밀

자바스크립트 엔진은 오직 콜 스택(Call Stack) 내에서의 코드 실행만 담당한다.
즉, JS 엔진 자체는 비동기 작업을 직접 수행하지 않는다.

그 외의 시간이 오래 걸리거나 무거운 작업들은 브라우저(Web API) 또는 Node.js 런타임이 대신 처리한다.

✅ 런타임에 따라 달라지는 환경

  • 자바스크립트는 어디에서 실행되는지에 따라 런타임 환경이 달라진다.
  • 브라우저에서 실행될 때 → Web API 환경
    • Web Worker를 통해 멀티스레드를 구현
    • 타이머, DOM 이벤트, fetch 등은 Web API에서 처리됨
  • Node.js에서 실행될 때 → Node 런타임 환경
    • Libuv 라이브러리의 스레드 풀을 기반으로 멀티스레드를 구현
    • 파일 I/O, 네트워크 작업 등을 Node 런타임이 처리함

✅ 대표적인 비동기 작업 예시

  • setTimeout (Web API, Node)
  • fetch (Web API, Node)
  • DOM 이벤트 (Web API)
  • File I/O (Node)

이런 API들은 브라우저 또는 Node.js 자체가 제공하는 멀티스레드 환경에서 동작한다.

즉, JS가 직접 위의 작업들을 처리하지 않고 외부의 환경(Web API or Node.js)에 작업을 맡기는 것이다.

✅ 비동기 처리 과정 요약

  1. JS는 비동기 작업을 직접 수행하지 않는다.
  2. 콜스택에는 비동기가 완료된 작업만 올라온다.
  3. 작업을 런타임에 맡겨두고, 완료되었을 때 콜백이나 Promise를 통해 다시 JS 엔진에게 알려주는 구조

📌 이벤트 루프(Event Loop) & 태스크 큐(Task Queue)

위의 비동기 처리 과정에서 가장 중요한 역할을 하는 것이 이벤트 루프(Event Loop) 이다.

태스크 큐는 비동기 처리가 완료된 작업이 들어가 대기하고 있는 저장소 역할이다.

 

이벤트 루프가 콜스택과 태스크 큐 사이에서 어떤 역할을 하고, 어떤 방식으로 동작하는지 자세하게 알아보자.

✅ 이벤트 루프(Event Loop)의 핵심 역할

  1. 콜 스택이 비었는지 확인한다.
  2. 비어 있다면 → 태스크 큐(Task Queue) 에서 작업을 하나 꺼낸다.
  3. 그 작업을 콜 스택으로 옮겨 실행한다.
  4. 다시 스택이 비면 → 큐를 확인한다.
  5. 이 과정을 무한 반복한다.

"콜 스택이 비면 큐에서 작업을 가져와 실행한다." 라는 규칙 하나로 비동기 코드가 동작하게 된다.

✅ 태스크 큐(Task Queue)란?

비동기 작업이 완료되면, 그 콜백 함수는 바로 실행되지 않는다. 콜 스택이 바쁘기 때문에, 에 잠시 대기시켜 둔다.

이 큐가 바로 태스크 큐(Task Queue) 이다.

 

즉, "비동기 작업의 콜백이 모여 있는 대기열" 이라고 생각하면 된다.

✅ 마이크로태스크 큐 (Microtask Queue)

태스크 큐 외에도 우선순위가 더 높은 마이크로태스크 큐가 존재한다.

태스크 큐보다 콜 스택에 먼저 들어가야 하는 작업들은 마이크로태스크 큐에 적재된다.

여기에는 다음 작업들이 들어간다

  • Promise.then / catch / finally
  • queueMicrotask
  • MutationObserver

📌 비동기 실행 순서

  1. 콜 스택 실행
  2. Microtask Queue 전체 처리
  3. Task Queue에서 한 개 실행
  4. 다시 Microtask Queue 확인
  5. 반복…
console.log(1);
setTimeout(() => console.log(2), 0); // 태스크 큐 작업
Promise.resolve().then(() => console.log(3)); // 마이크로태스크 큐 작업
console.log(4);
 

위 코드의 출력은 1 → 4 → 3 → 2 이다.


 

📚 오늘의 배움

  • 싱글스레드의 단점을 런타임 구조를 통해 해결해 낸다는 점이 인상적이었다.
  • 특히 Event Loop와 Task Queue에 대해 정확히 이해했을때 비동기 로직이 들어간 코드의 흐름을 정확하게 읽을 수 있을 것 같다고 생각했다.
  • Task Queue 보다 우선 순위로 콜스택에 적재되는 Microtask Queue의 존재에 대해 알게 되었다.
  • 앞으로는 이벤트 루프 흐름을 정확하게 읽고,실제 프로젝트에서 비동기 흐름에 따른 버그 요소를 찾아내는 성과를 거두고 싶다.