Programming Language/JavaScript

JavaScript는 싱글스레드인데 어떻게 동시에 여러 일을 할까?

dev-power 2026. 2. 4. 21:30
JavaScript는 싱글스레드입니다.

 

JavaScript를 사용하는 사람들이라면 분명히 들어본 말입니다. 그런데 이상하지 않나요?

 

fetch('https://api.example.com/data')  // 서버에서 데이터 가져오는 중
console.log('로딩중...')  // 이건 바로 실행됨
updateAnimation()  // 애니메이션도 돌아감

 

분명 API 호출하고, 로그 찍고, 애니메이션 돌리고... 동시에 여러 일이 일어나는데요? 스레드가 하나면 한 번에 한 가지만 할 수 있는 거 아닌가요?

 

"비동기 처리를 하니까 가능하다"는 건 알겠는데, 그게 대체 어떻게 가능한 건지 이해가 잘 안 됩니다. 비동기는 말 그대로 동시에 여러 작업을 하는 거잖아요. 싱글스레드가 어떻게 동시에 여러 일을 하죠?

 

정답부터 말하면: 실제로는 동시에 안 합니다.

 

자바스크립트 혼자서는 비동기 처리가 불가능합니다. 브라우저나 Node.js 같은 런타임 환경이 멀티스레드로 일을 대신 처리해 주는 겁니다. 이 글에서는 그 원리를 최대한 쉽게 풀어보겠습니다.

 

 

 

핵심: JavaScript는 실제로 동시에 안 합니다


"비동기 = 동시에 여러 일"이라고 생각하기 쉬운데, 자바스크립트 입장에서는 아닙니다. 엄청 빠르게 번갈아가면서 할 뿐입니다.

 

회전초밥집에 비유해볼까요? 회전초밥집 주방장(JavaScript)은 한 명 뿐입니다. 그런데 손님 여러 명이 주문을 합니다.

  • 테이블1: "연어초밥 3개요!"
  • 테이블2: "우동 하나요!"
  • 테이블3: "새우튀김이요!"

주방장은 한 번에 한 가지만 만들 수 있습니다. 하지만 이렇게 합니다:

  1. 연어초밥 재료를 도마에 올려놓고 -> "잠깐, 이건 좀 시간 걸리겠는데?"
  2. 우동 물을 올려놓고 -> "끓는 동안 다른거 하자"
  3. 새우튀김 기름에 넣고 -> "튀기는 동안 또 다른 거"
  4. 연어초밥으로 돌아와서 마무리

기다리는 시간 동안 다른 일을 하는 겁니다. 동시에 하는 게 아니라, 기다리는 시간을 낭비하지 않는 거죠. 그런데 여기서 의문이 생깁니다. "우동 물이 끓는지 어떻게 알아요? 주방장은 새우튀김하느라 바쁜데요?". 바로 이겁니다. 주방장(JavaScript)이 직접 물 끓는 걸 보는 게 아닙니다. 가스레인지(브라우저 Web API 또는 Node.js libuv)가 알아서 끓여주고, 다 끓으면 "주방장님! 물 끓었어요!" 하고 알려주는 겁니다.

 

 

 

Web API와 libuv


JavaScript 혼자서는 비동기 처리를 못 합니다. 도와주는 친구들이 있습니다.

 

브라우저 환경: Web API

브라우저에서 돌아가는 JavaScript는 브라우저가 제공하는 Web API를 씁니다.

// setTimeout, fetch, DOM 이벤트 등은 Web API가 처리
setTimeout(() => console.log('완료'), 1000)
fetch('https://api.example.com/data')
document.querySelector('#btn').addEventListener('click', handleClick)

 

이런 작업들은 JavaScript 엔진이 직접 하는 게 아니라 브라우저한테 맡깁니다. 브라우저는 멀티스레드로 돌아가기 때문에 백그라운드에서 처리할 수 있죠.

 

예를 들어 fetchf를 호출하면:

  1. JavaScript: "브라우저야, 이 URL에서 데이터 좀 가져와줘"
  2. 브라우저: "알았어, 내가 별도 스레드로 처리할게" (JavaScript는 다른 일 계속함)
  3. 브라우저: "다 가져왔어!" (완료되면 JavaScript한테 알림)
  4. JavaScript: "오케이, 이제 콜백함수 실행할게"

 

Node.js 환경: libuv

Node.js에서는 libuv라는 C로 만든 라이브러리가 비동기 작업을 담당합니다.

// 파일 읽기, 네트워크 요청 등은 libuv가 처리
const fs = require('fs')
fs.readFile('data.txt', (err, data) => {
  console.log(data)
})

 

libuv는 운영체제의 멀티스레드 기능을 활용해서 파일 I/O, 네트워크 같은 걸 처리합니다. 그리고 끝나면 JavaScript한테 "다 했어요!" 하고 알려주죠.

 

정리하면:

  • JavaScript 엔진 자체는 싱글스레드 (한 번에 한 가지만 처리)
  • 하지만 브라우저(Web API)나 Node.js(libuv)는 멀티스레드
  • 시간 걸리는 작업은 이 친구들한테 맡기고, JavaScript는 다른 일 계속 함
  • 작업 완료되면 알림 받아서 콜백 실행

 

 

 

이벤트 루프: 할 일 목록 관리자


그럼 "작업이 끝났다는 걸 어떻게 알고, 언제 콜백을 실행하는가?" 이게 바로 이벤트 루프의 역할입니다.

 

출처: geeksforgeeks.org

 

모식도를 외우려고 하지 말고, 이렇게 생각해봅시다.

 

할 일 목록이 세 개 있습니다:

  1. 콜 스택(Call Stack): 지금 당장 하고 있는 일
  2. 웹 API / libuv: 시간 걸리는 일 맡기는 곳 (타이머, 네트워크 요청 등)
  3. 콜백 큐(Callback Queue): 끝난 (시간 걸리는) 일들이 대기하는 줄

이벤트 루프는 교통경찰입니다. 계속 이렇게 확인합니다:

  • "콜 스택이 비었나?"
  • "비었으면 콜백 큐에서 하나 꺼내서 콜 스택으로!"
console.log('1. 시작')

setTimeout(() => {
  console.log('2. 타이머 끝!')
}, 0)  // 0초 후에 실행

console.log('3. 끝')
```

**출력 결과:**
```
1. 시작
3. 끝
2. 타이머 끝!

 

"어? 0초인데 왜 제일 나중에 나와요?"라고 생각하셨다면 정상입니다.

 

진행 과정:

  1. console.log('1. 시작') -> 콜 스택에 쌓임 -> 바로 실행 -> "1. 시작" 출력
  2. setTimeout 만남 -> "아, 이건 시간 걸리는 작업이네" -> 웹 API한테 넘김
  3. console.log('3. 끝') -> 콜 스택에 쌓임 -> 바로 실행 -> "3. 끝" 출력
  4. 콜 스택이 비었음! -> 이벤트 루프가 콜백 큐 확인 -> setTimeout 콜백 발견
  5. 콜백을 콜 스택으로 올림 -> "2. 타이머 끝!" 출력

0초여도 일단 Web API를 거쳐야 하기 때문에 콜백 큐에 들어갔다가 나오는 시간이 필요합니다. 그래서 제일 나중에 실행되는 거죠.

'Programming Language > JavaScript' 카테고리의 다른 글

호이스팅? 생각보다 쉽습니다  (0) 2025.12.31