• ABOUT
  • POSTS
  • GUESTBOOK

© 2025 BlueCool12 All rights reserved.

2026.02.05JavaScript

☕️ JS가 싱글 스레드로 동시성을 처리하는 방법 (이벤트 루프 & 블로킹)

1. 싱글 스레드의 딜레마

자바스크립트는 싱글 스레드(Single Thread) 기반의 언어이다. 쉽게 말해 한 번에 하나의 일밖에 처리를 못한다는 뜻이다.

그런데 우리는 브라우저에서 유튜브를 보면서 댓글도 달고 Node.js 서버는 수천 명의 요청을 동시에 처리하는 것처럼 보인다. 어떻게 가능한 것일까?

그 비결은 블로킹/논 블로킹(Blocking/Non-blocking) I/O와 이를 지휘하는 이벤트 루프(Event Loop)에 있다.


2. 블로킹 vs 논 블로킹 (Blocking vs Non-blocking)

이 개념을 이해하기 가장 쉬운 예시는 카페 주문이다.

[블로킹]
손님이 커피를 주문한다. 직원은 주문을 받고 커피를 만든다. 커피가 완성되어 손님에게 건네줄 때까지 직원은 꼼짝도 하지 않고(Blocked) 기다린다. 뒤에 줄 선 손님들은 앞사람의 커피가 나올 때까지 주문조차 할 수 없다.

예를 들어 파일을 다 읽을 때까지 다음 코드가 실행되지 않고 멈춰있는 상태를 의미한다. (fs.readFileSync)

[논 블로킹]
손님이 커피를 주문한다. 직원은 주문을 주방(백그라운드)에 넘기고 진동 벨을 준다. 즉시 다음 손님의 주문을 받는다. 커피가 다 되면 진동 벨이 울리고 손님은 커피를 받아 간다.

논 블로킹은 파일을 읽으라고 명령만 해두고 결과가 오든 말든 바로 다음 코드를 실행하는 상태를 의미한다. (fs.readFile)

결과적으로 직원은 쉬지 않고 주문을 받을 수 있어 효율적이다. 자바스크립트는 싱글 스레드이기 때문에 무거운 작업(파일 읽기, DB 조회)을 논 블로킹으로 처리해서 메인 스레드가 멈추지 않게 하는 것이 생명이다.


3. 이벤트 루프 (Event Loop)

그렇다면 논 블로킹으로 던져놓은 작업들이 끝났다는 건 누가 알려주고 언제 실행될까? 이때 등장하는 것이 이벤트 루프이다.

이벤트 루프의 작동 원리는 크게 3가지 요소의 협력으로 이루어진다.

1. Call Stack (호출 스택) - 현재 실행 중인 코드가 쌓이는 곳
2. Web APIs / Background - 비동기 작업(타이머, 네트워크 요청)이 대기하는 곳
3. Callback Queue (태스크 큐) - 완료된 작업의 콜백 함수가 대기하는 줄

작동 순서는 다음과 같다.

1. 코드가 실행되면 Stack에 쌓인다.
2. 비동기 작업(setTimeout, fetch 등)을 만나면 Web APIs로 보내고 다음 코드를 바로 실행한다.
3. 비동기 작업이 끝나면 그 결과(콜백)가 Queue에 줄을 선다.
4. 이벤트 루프는 Stack이 비어있는지 계속 감시한다.
5. Stack이 비어있다면 Queue에 있는 작업을 하나씩 Stack으로 올려보내 실행한다.

console.log('1. 시작');

setTimeout(() => {
console.log('2. 오래 걸리는 작업 (비동기)');
}, 0);

console.log('3. 끝');​

위 코드의 실행 결과는 1 -> 3 -> 2 순서로 실행된다. (setTimeout의 두 번째 인자가 0초라도 Queue를 거쳐야 하기 때문에 Stack이 비워진 뒤인 맨 마지막에 실행)

여기서 한 가지 정리해야 할 것은 Web APIs는 멀티 스레드로 동작하는 병렬 공간이라는 점이다. 자바스크립트 엔진 자체는 싱글 스레드이지만 자바스크립트가 구동되는 환경(브라우저, Node.js)은 멀티 스레드를 지원한다.

즉 런타임 환경은 멀티 스레드이기 때문에 동시성 처리가 가능한 것이다.



추가) 마이크로태스크 큐 (Microtask Queue)

이벤트 루프는 우선순위를 따진다.

Microtask Queue: Promise(.then/catch/finally), process.nextTick
Task Queue: setTimeout, setInterval

이벤트 루프는 마이크로태스크 큐를 우선적으로 비우고 나서야 일반 태스크 큐를 확인한다. 따라서 Promise가 setTimeout보다 더 빨리 실행된다.

이전 글
🧩 NestJS와 모듈 시스템 이해하기
다음 글
다음 글이 없습니다 ( · . ·)
장식용 로고