Rounded avatar

BOBcost

Dev

자바스크립트 동작 원리와 이벤트 루프 이해하기

자바스크립트 싱글 스레드 구조와 콜스택, 이벤트 루프, 비동기 처리 방법을 정리한 글
생성일 : 2025년 08월 22일

📌 '싱글 스레드' 언어

자바스크립트는 '싱글 스레드(Single Thread)' 기반 언어입니다.

  • 스레드(Thread) 는 '작업을 처리하는 일손'에 비유할 수 있습니다.
  • 싱글 스레드라는 것은 "일손이 하나뿐이라, 한 번에 하나의 작업만 처리할 수 있다" 는 것을 의미합니다.

그렇다면 이 '일손'이 작업 목록을 관리하는 공간이 필요하겠죠. 그것이 바로 '콜 스택'입니다.


1️⃣ 콜 스택 (Call Stack)

콜 스택(Call Stack) 은 자바스크립트 코드 실행을 추적하는 '작업 목록' 입니다.

  • 함수가 호출되면 스택에 쌓이고(Push), 함수 실행이 끝나면 스택에서 제거됩니다(Pop).
  • LIFO (Last In, First Out): 가장 마지막에 들어온 작업(함수)이 가장 먼저 처리됩니다.

[실행 순서 및 스택]

  1. first() 호출 -> Stack: [first]
  2. '첫 번째' 출력
  3. second() 호출 -> Stack: [first, second]
  4. '두 번째' 출력
  5. second() 종료 -> Stack: [first]
  6. '첫 번째 종료' 출력
  7. first() 종료 -> Stack: []
1function first() {
2  console.log('첫 번째');
3  second();
4  console.log('첫 번째 종료');
5}
6function second() {
7  console.log('두 번째');
8}
9first();

💡 Point
콜 스택은 동기적(Synchronous) 으로 동작합니다. 작업이 끝나기 전까지 다음 작업은 절대 실행되지 않습니다. 만약 second() 함수가 10초 걸리는 작업이라면, 웹 페이지 전체가 10초 동안 멈추게 됩니다. 이를 블로킹(Blocking) 이라 합니다.


📌 블로킹과 논블로킹

1️⃣ 블로킹(Blocking)

  • 긴 작업이 완료될 때까지 다른 모든 작업들이 멈춰있다가 해당 작업이 끝난 후에 다시 진행

2️⃣ 논블로킹(Non-blocking)

  • 웹에서 파일도 다운로드하면서 웹 서핑도 하고 동영상도 보고 여러 가지 일을 할 수 있습니다.
  • 작업이 진행되는 동안에도 다른 작업들이 계속 진행될 수 있습니다.

📌 비동기 처리와 '이벤트 루프'

"일손이 하나인데, 10초 걸리는 작업을 만나면 웹 페이지가 10초 동안 멈추나요?"

  • 맞습니다. 이를 해결하기 위해 '이벤트 루프(Event Loop)''비동기(Asynchronous)' 개념이 필요합니다.

자바스크립트 엔진(V8 등) 자체는 싱글 스레드(콜 스택)가 맞지만, 자바스크립트가 실행되는 환경 (브라우저, Node.js)은 여러 가지 다른 기능(API)들을 제공합니다.

자바스크립트의 비동기 동작은 다음 4가지 핵심 요소의 상호작용으로 이루어집니다.

  1. 콜 스택 (Call Stack): (위에서 설명) 동기 코드 실행
  2. Web API (브라우저) / C++ API (Node.js)
    • 시간이 오래 걸리는 작업 (예: setTimeout, fetch (HTTP 요청), DOM 이벤트)을 처리하는 곳입니다.
    • 자바스크립트 엔진 바깥쪽 에 존재합니다.
  3. 태스크 큐 (Task Queue / Callback Queue)
    • Web API에서 완료된 비동기 작업의 콜백 함수 가 대기하는 '대기열' 입니다. (FIFO)
  4. 이벤트 루프 (Event Loop)
    • 콜 스택태스크 큐 를 감시하는 매니저입니다.
    • 콜 스택이 비어있으면, 태스크 큐에서 가장 오래된 작업을 콜 스택으로 옮깁니다.

📌 이벤트 루프의 구조

자바스크립트 엔진이 비동기적으로 동작하는 환경은 단순히 엔진 하나로 이루어진 것이 아닙니다. '이벤트 루프' 는 이 모든 것을 조율하는 핵심 메커니즘이며, 다음과 같은 주요 구성 요소들과 함께 동작합니다.

1️⃣ 메모리 힙 (Memory Heap)

  • 메모리 힙 은 컴퓨터가 정보를 저장하는 공간입니다.
  • 자바스크립트 관점에서 우리가 선언하는 변수, 객체, 배열, 함수 등 대부분의 데이터가 이곳에 동적으로 할당되고 저장됩니다.

2️⃣ 콜 스택 (Call Stack)

  • 콜 스택 은 자바스크립트의 '할 일 목록' 이라고 생각할 수 있습니다.
  • 함수가 호출되면, 해당 함수의 정보 (실행 컨텍스트) 가 콜 스택에 순서대로 쌓이게 됩니다.
  • FILO (First In Last Out) 구조, 즉 "먼저 들어간 것이 마지막에 나온다" (또는 LIFO)는 규칙을 따릅니다.

💡 실행 컨텍스트란?
​실행 컨텍스트(Execution Context)는 scope, hoisting, this, function, closure 등의 동작원리를 담고 있는
자바스크립트의 핵심원리

3️⃣ 웹 API (Web APIs)

  • 웹 API는 자바스크립트 엔진 외부 에서 관리되며, 브라우저나 Node.js와 같은 런타임 환경이 제공하는 기능입니다.
  • 예를 들어 setTimeout, setInterval, fetch (HTTP 요청), DOM 이벤트 등이 여기에 속합니다.
  • 이 작업들은 비동기적 으로 처리되므로, 즉시 완료되지 않고 백그라운드에서 실행됩니다.

4️⃣ 콜백 큐 (Callback Queue)

  • 콜백 큐 는 Web API에서 완료된 비동기 작업의 콜백 함수 가 대기하는 공간입니다.
  • FIFO (First In First Out), 즉 "먼저 들어간 작업이 먼저 나간다"는 구조입니다.
  • 이 큐는 작업의 종류와 우선순위에 따라 세 가지 유형으로 나눌 수 있습니다.

1. 태스크 큐 (Task Queue / Macrotask Queue)

  • 일반적으로 '매크로태스크 큐' 라고도 부릅니다.
  • setTimeout, setInterval, I/O 작업, DOM 이벤트 등의 콜백 함수가 이곳으로 들어갑니다.

2. 마이크로태스크 큐 (Microtask Queue)

  • 매크로태스크보다 높은 우선순위 를 갖는 큐입니다.
  • Promise.then(), .catch(), .finally() 콜백이나 async/await 구문이 이곳에 들어갑니다.

3. 애니메이션 프레임 (Animation Frames)

  • 브라우저 환경에서만 해당하며, 화면을 업데이트(리페인트)하는 작업과 관련된 큐입니다.
  • requestAnimationFrame 의 콜백 함수가 여기에 해당됩니다.
마이크로태스크 큐(Microtask Queue) > 애니메이션 프레임(Animation Frames) > 태스크 큐(Task Queue)
Microtask Queue가 가장 먼저 실행 되고 Task Queue가 가장 늦게 실행 되는 우선순위 를 가지고 있습니다.

📌 이벤트 루프의 동작 과정

이벤트 루프는 프로그램이 종료될 때까지 다음과 같은 과정을 계속 반복 하며 자바스크립트의 동시성을 관리합니다.

예시 이미지예시 이미지

  1. 1. 콜 스택(Call Stack) 확인

    • 이벤트 루프는 가장 먼저 콜 스택 이 비어 있는지 확인합니다.
    • 만약 콜 스택에 아직 처리되지 않은 함수(작업)가 있다면, 해당 함수가 완전히 실행되어 콜 스택이 빌 때까지 기다립니다.
  2. 2. 콜백 큐(Callback Queue) 확인

    • 콜 스택 이 비어 있는 것이 확인되면, 콜백 큐 (태스크 큐, 마이크로태스크 큐)를 확인합니다.
    • 큐에 대기 중인 콜백 함수가 있는지 살펴봅니다.
  3. 3. 함수 이동 (큐 → 스택)

    • 콜백 큐에 대기 중인 함수가 있다면, 큐에서 가장 오래된(먼저 들어온) 함수를 하나 꺼내어 콜 스택 으로 옮깁니다.
    • (참고: 실제로는 마이크로태스크 큐가 항상 우선권을 가지며, 큐가 빌 때까지 모든 작업을 옮깁니다.)
  4. 4. 함수 실행

    • 콜 스택 으로 옮겨진 함수가 즉시 실행됩니다.
    • 함수 실행이 완료되면, 해당 함수는 콜 스택 에서 제거(Pop)됩니다.

이벤트 루프는 1~4번의 과정을 프로그램이 종료될 때까지 계속 반복합니다.


가장 고전적인 예제를 통해 비동기 동작을 이해해 봅시다.
예제1 코드
console.log('시작'); // (A)
 
setTimeout(() => { // (B)
  console.log('1초 뒤 실행');
}, 1000);
 
console.log('끝'); // (C)

[실행 순서]

  1. console.log('시작')이 콜스택 에 추가되고, 실행된 후 제거됩니다. (출력: '시작')

  2. setTimeout이 콜스택 에 추가됩니다.
    setTimeout은 Web API 작업이므로, 브라우저에게 "1초 뒤에 이 콜백 함수 실행해 줘"라고 요청하고 즉시 콜스택에서 제거 됩니다.

  3. console.log('끝')이 콜스택 에 추가되고, 실행된 후 제거됩니다. (출력: '끝')

  4. Web API는 1초가 지나자 콜백 함수(() => console.log('1초 뒤 실행'))를 태스크 큐 로 보냅니다.

  5. 이벤트 루프 가 "콜스택이 비어있음"을 확인합니다.

  6. 이벤트 루프가 태스크 큐에 있던 콜백 함수를 콜스택 으로 이동시킵니다.

  7. 콜백 함수가 실행되고, console.log('1초 뒤 실행')이 실행됩니다. (출력: '1초 뒤 실행')

이번에는 GIF 이미지 예제를 보면서 이해해보자!
예제2 코드
function greet() {
  return 'Hello!';
}
 
function respond() {
  return setTimeout(() => {
    return 'Hey!';
  }, 1000);
}
 
greet();
respond();

예제2 이미지1예제2 이미지1

  1. greet 함수를 만나서 호출한다.
  2. 콜스택에 실행 중인 작업이 없기 때문에 콜스택에 추가 된다.
  3. greet 함수를 실행 시켜서 "Hello"를 출력하고 콜스택에서 사라진다.
  4. 다음 respond 함수를 콜스택에 추가 한다.

예제2 이미지2예제2 이미지2

  1. setTimeout 함수가 콜 스택 에 추가(Push)됩니다.
  2. setTimeout은 브라우저(Web API)에게 "이 콜백 함수를 1000ms 뒤에 실행해 줘"라고 등록 요청을 보냅니다.
  3. 요청을 마친 setTimeout 함수 자체는 콜 스택 에서 즉시 제거(Pop)됩니다. (다른 동기 코드들은 계속 실행됩니다.)
  4. 등록된 콜백 함수는 Web API 환경으로 이동하여 타이머(1000ms)가 시작됩니다.

예제2 이미지3예제2 이미지3

  1. Web API에서 1000ms 타이머가 완료됩니다.
  2. 타이머가 완료된 콜백 함수는 콜 스택 으로 바로 들어가는 것이 아니라 , 콜백 큐 (Callback Queue) 로 이동하여 대기합니다.
  3. (더 자세히는 setTimeout의 콜백이므로 태스크 큐 (Task Queue) 로 들어간 것입니다.)

예제2 이미지4예제2 이미지4

  1. 이벤트 루프콜 스택 이 완전히 비어있는지 계속 확인합니다.
  2. 콜 스택이 비워진 것을 확인한 이벤트 루프는 콜백 큐 (태스크 큐)를 확인합니다.
  3. 큐에 대기 중인 콜백 함수를 발견하고, 해당 함수를 콜 스택 으로 이동시킵니다.

예제2 이미지5예제2 이미지5

  1. 콜 스택에 들어온 함수는 즉시 실행됩니다. (예: 내부의 Hey가 실행됨)
  2. 실행이 완료된 콜백 함수는 콜 스택 에서 최종적으로 제거(Pop)됩니다.

📌 예상 면접 질문 & 답변

Q1: 자바스크립트는 싱글 스레드인데 어떻게 비동기 처리가 가능한가요?

  • 포인트: 자바스크립트 엔진과 실행 환경(브라우저)의 분리, 이벤트 루프
  • 답변 예시:
    "자바스크립트 엔진 자체는 싱글 스레드로 콜 스택 하나만 가지고 있지만, 실행 환경인 브라우저(또는 Node.js)가 Web API 같은 별도의 기능을 제공합니다. setTimeout이나 fetch 같은 비동기 작업은 Web API가 처리하고, 완료된 작업의 콜백 함수는 '태스크 큐'로 보내집니다. '이벤트 루프'가 콜 스택이 비었을 때 태스크 큐의 작업을 콜 스택으로 가져와 실행함으로써, 싱글 스레드임에도 논블로킹(Non-blocking) 비동기 처리가 가능해집니다."

Q2: 마이크로태스크 큐와 매크로태스크 큐는 무엇이며, 어떤 순서로 실행되나요?

  • 포인트: 우선순위, Promise vs setTimeout
  • 답변 예시:
    "태스크 큐는 두 종류가 있습니다. 매크로태스크 큐setTimeout이나 DOM 이벤트 콜백이 들어가고, 마이크로태스크 큐Promise.then이나 async/await의 콜백이 들어갑니다. 이벤트 루프는 마이크로태스크 큐를 항상 우선합니다. 즉, 콜 스택이 비어서 하나의 매크로태스크를 실행한 직후, 마이크로태스크 큐에 쌓인 모든 작업을 전부 실행하고 나서야 다음 매크로태스크를 확인하러 갑니다."

Q3: setTimeout(..., 0)은 왜 즉시 실행되지 않나요?

  • 포인트: Web API, 태스크 큐, 이벤트 루프 순서
  • 답변 예시:
    "setTimeout(..., 0)은 "0초 뒤에 실행"이 아니라, "0초 뒤에 태스크 큐로 보내라"는 의미입니다. 콜 스택에 현재 실행 중인 동기 코드들이 모두 처리되어 콜 스택이 비워진 후에야, 이벤트 루프가 태스크 큐를 확인하고 해당 콜백을 콜 스택으로 가져옵니다. 따라서 console.log 같은 동기 코드보다 항상 늦게 실행됩니다."

Q4: '블로킹(Blocking)'이란 무엇이며, 왜 이것이 싱글 스레드 모델에서 문제가 되나요?

  • 포인트: 콜 스택 점유, UI 멈춤, 이벤트 처리 지연
  • 답변 예시:
    "블로킹은 하나의 작업이 콜 스택을 오래 점유하여 다음 작업이 실행되지 못하고 멈춰있는 현상을 말합니다. 자바스크립트는 싱글 스레드이므로, 만약 동기적으로 무거운 작업을 실행하면 그 작업이 끝날 때까지 콜 스택이 막힙니다. 이로 인해 브라우저의 경우 UI 렌더링이나 사용자 클릭 같은 이벤트 처리가 모두 지연되어, 사용자가 '웹페이지가 멈췄다'고 느끼게 됩니다. 따라서 시간이 오래 걸리는 작업은 비동기 API를 통해 논블로킹으로 처리해야 합니다."

Q5: setTimeout(fn, 0)Promise.resolve().then(fn)은 어떤 순서로 실행되며, 그 이유는 무엇인가요?

  • 포인트: 마이크로태스크, 매크로태스크, 우선순위
  • 답변 예시:
    "Promise.resolve().then(fn)이 항상 먼저 실행됩니다. 두 함수 모두 비동기적으로 실행되지만, Promise의 콜백은 마이크로태스크 큐로 들어가고 setTimeout의 콜백은 매크로태스크 큐로 들어가기 때문입니다. 이벤트 루프는 콜 스택이 비워지면, 매크로태스크 큐를 확인하기 전에 마이크로태스크 큐를 먼저 확인하고 그 안의 모든 작업을 비웁니다. 따라서 마이크로태스크인 Promise가 매크로태스크인 setTimeout보다 항상 먼저 실행됩니다."

Q6: 이벤트 루프(Event Loop)의 정확한 동작 순서를 설명해주세요.

  • 포인트: 콜 스택, 마이크로태스크 큐, 매크로태스크 큐, 렌더링
  • 답변 예시:
    1. 우선 콜 스택 에 실행 중인 동기 코드가 있는지 확인하고, 있다면 모두 실행하여 콜 스택을 비웁니다.
    2. 콜 스택이 비워지면, 마이크로태스크 큐 를 확인하여 큐에 있는 모든(ALL) 작업을 꺼내와 콜 스택에서 순서대로 실행합니다. (이 과정에서 새로운 마이크로태스크가 추가되면 그것까지 모두 실행합니다.)
    3. 마이크로태스크 큐가 완전히 비워지면, (브라우저 환경의 경우) 렌더링이 필요한 시점인지 확인하고 필요시 렌더링을 수행합니다.
    4. 그다음 매크로태스크 큐 를 확인하여 큐에서 가장 오래된 작업 단 하나(ONE) 만 꺼내어 콜 스택으로 옮겨 실행합니다.
    5. 다시 1번 단계(콜 스택 확인 후 마이크로태스크 실행)로 돌아가 이 과정을 반복합니다.