머리말

자바스크립트는 싱글 스레드에서 작동한다. 즉 한 번에 하나의 작업만 처리할 수 있는데, 현대의 모던 웹 애플리케이션에서는 네트워크 요청이 발생하는 순간에도 다른 작업을 처리할 수 있다. 이는 어떻게 된 일일까?

이를 이해하려면 비동기 작업이 어떻게 처리 되는지 이해하고 비동기 처리를 도와주는 이벤트 루프를 비롯한 다양한 개념을 알고 있어야한다. 이번 포스트에서는 자바스크립트가 어떻게 여러가지 요청을 동시에 처리하고 있는지, 요청받은 테스크에 대한 우선순위는 무엇인지 파악해보자

KeyPoint

스택, 큐, 이벤트루프, 태스크 큐, 마이크로 태스크큐

런타임 환경과 함께 내용을 알아보자


Stack

함수의 호출들은 ‘스택’을 형성합니다 아래 예시코드를 들어보겠습니다

function foo(b) {
  const a = 10;
  return a + b + 11;
}

function bar(x) {
  const y = 3;
  return foo(x * y);
}

const baz = bar(7);
  1. bar(7)을 호출할 때 인수와 지역변수를 포함하는 첫 번째 프레임이 생성됩니다.
  2. bar가 foo를 호출할 때 위와같은 방식으로 두 번째 프레임이 생성됩니다.
  3. 이제 foo가 반환되면, 맨 위의 프레임 요소를 스택 밖으로 꺼냅니다.
  4. 마지막으로 남아있던 bar가 반환되며, 스택이 비어있게 됩니다.

이때 콜스택이 가득차면, 흔히보던 Uncaught RangeError: Maxium call stack size exceeded 오류를 만나볼 수 있습니다.


큐(Queue)

자바스크립트의 런타임은 메세지 큐, 즉 처리할 메세지의 대기열을 사용하는데. 각각의 메세지에는 메세지를 처리하기 위한 함수가 연결되어 있습니다.

이벤트 루프가 시작되는 시점에, 런타임은 대기열에서 가장 오래된 메세지부터 큐에서 꺼내 처리하기 시작합니다. 이를 위해 런타임은 꺼낸 메세지를 매개 변수로, 메세지에 연결된 함수를 호출합니다. 이 때 함수를 호출하면 해당 함수가 사용할 새로운 스택 프레임이 생성됩니다.

(setTimeout이 시작되는 시점에 해당 함수에 관된 내용이 스택에 쌓임)

여기서 이벤트 루프가 시작되는 시점이 대체 언제냐? 이벤트 루프를 통해 알아봅시다.


이벤트 루프

이벤트루프는 이벤트 루프만의 단일스레드 내에서 두 가지 역할을 합니다.

  1. 호출 스택이 비어있는지 확인
  2. 호출 스택 내부에 수행해야 할 작업이 있는지 확인하고, 수행해야 할 코드가 있으면 자바스크립트 엔진을 이용해 실행

코드를 통해 알아봅시다.

function foo(){
  setTimeout(() => console.log('bar'));
  console.log('foo');
}
foo()
  1. foo가 호출스택에 들어감
  2. setTimeout이 호출스택에 들어감
  3. setTimeout이 타이머 이벤트에 의해 큐로 들어가고 스택에서 제거됨.
  4. console.log(‘foo’)가 스택에 들어간 후 실행되고 스택에서 빠짐.
  5. 이벤트 루프가 호출 스택이 비워져있다는 것을 확인
  6. 이벤트 루프가 큐를 확인하여 수행해야 할 작업이 있나 확인 후 있다면 스택에 넣음
  7. 스택 실행 console.log(‘bar’) 실행

 

여기서 궁금할 수 있는 점으로는 setTimeout과 같이 n초를 기다리거나 fetch를 기반으로 실행되는 네트워크 요청은 누가 보내고 응답 받는것일까? 입니다.

 이러한 작업들은 전부 자바스크립트 메인 스레드가 아닌 태스크 큐가 할당되는 별도의 스레드에서 실행됩니다. 바로 브라우저나 Node.js에서 말입니다!(Web API등 외부에서 실행되고 큐로 들어감)

자 이제 우리는 이벤트루프를 통해 스택과 큐가 어떻게 작동 하는지 알게 되었습니다. 이제 마지막 챕터인 마이크로 태스크 큐로 넘어가 봅시다.


큐는 사실 FIFO가 아닌 set형태를 띄고 있다. 선택된 중 ‘실행 가능한’ 가장 오래된 태스크를 가져와야하기 때문이다.

 이때까지 큐라고 불렀던 공간은 흔히 태스크 큐(task queue) 또는 매크로 태스크 큐(macrotask que)라고들 합니다. 이번에는 이 태스크 큐보다 우선순위가 높은 마이크로 태스크 큐에대해 알아봅시다.

 

마이크로 태스크 큐는 대표적으로 Promise가 있다. 즉 setTImeout, 클릭 이벤트 등은 Promise보다 늦게 실행이 됩니다.

명세에 따르면 마이크로 태스크 큐가 빌 때까지는 기존 태스크 큐의 실행은 뒤로 미루어집니다.

  • 태스크 큐: 이벤트 핸들러, setTImeout, setInterval, promise를 사용하지않은 AJAX 요청
  • 마이크로 태스크 큐: Promise, queueMicrotask 함수, MutationObserver

이때 중요한 점으로 브라우저 렌더링 작업은 주로 태스크 큐에 속하고 있으며, Dom 변경에 대한 콜백이 Promise의 then() 또는 MutationObserver와 같은 API를 통해 등록된 경우가 아닌 이상 마이크로 태스크 큐태스크 큐 사이에 렌더링이 일어납니다.

 


 끝까지 읽어주셔서 감사합니다.! 이번 글을 통해 조금이라도 도움이 되셨다면 좋겠습니다. 마무리삼아 퀴즈를 하나 내드리겠습니다.

아래 코드는 어떤 순서대로 실행이 될까요??

const quiz = () => {
  console.log('1');

  Promise.resolve().then(() => {
    console.log(2);
  });

  setTimeout(() => console.log(3));

  window.requestAnimationFrame(() => console.log(4));
};

quiz();

'개발' 카테고리의 다른 글

[nextjs 톺아보기] <head>  (0) 2025.04.08
Vue Ref 톺아보기  (0) 2025.03.26
빌드시스템은 왜 필요할까?  (0) 2025.03.26
번들사이즈 최적화  (0) 2024.09.30

호이안

해당 포스트는 하루에 하나씩 FE 개발자 면접 문제 오픈 채팅방에서 매일 출제되는 퀴즈를 토대로 학습 및 토론내용을 정리한 게시글입니다.

 

1. Quiz 및 답변

퀴즈

안녕하세요~ 좋은 하루입니다! 오늘의 FE의 질문은!!

Q1. 자바스크립트의 event loop와 call stack의 동작 방식을 설명하고, microtask와 macrotask 큐의 차이점을 설명해 주세요. 이를 통해 비동기 코드가 실행되는 순서에 대해 설명해 주세요.

답변

 싱글스레드인 자바스크립트의 실행 방식을 보완하기 위하여 브라우전 엔진 및 노드등은 이벤트 루프와 같은 설계 방식을 선택하였습니다!
이벤트 루프는 자바스크립트의 콜스택이 비게되면 태스크큐에 있는 작업을 순차적으로 실행시켜 자바스크립트에서 비동기 로직을 가능하게 합니다. 이때 큐 하나로는 비동기 작업의 우선순위 및 효율성을 지키기 어려워 **매크로 태스크 큐****마이크로 태스크 큐** 두 가지 큐를 통하여 비동기 처리의 일관성을 유지합니다.
마이크로 태스크큐는 주로 프로미스나 뮤테이션 옵져버가 들어가며, 매크로 태스크 큐는 그 이외의 대부분 비동기 작업이 들어갑니다.

2. 토론

 대부분의 답변들이 콜스택과 태스크 큐를 강조하며, 태스크 큐와 마이크로 태스크 큐의 차이점, 그리고 각 큐에는 어떤 작업들이 들어가게 되는지를 설명해 주었습니다. 이 중 이슈가 되는 답변과 그에 대한 저의 생각들을 나열해보겠습니다.

 

Q.1 '🤔 자바스크립트가 동기적으로 처리한다는 말이 약간 애매하지않을까요 ?'

 

 저는 질문의 의도를 아래와 같이 생각하였습니다.

 

[사고의 맥락]
'자바스크립트는 비동기도 되니깐 동기라고 하기에는 조금 애매한거 같다' 라고 생각을 하시고 있으신가?. 라고 생각하였으며, 그에 대해 저는 '자바스크립트 자체는 싱글 스레드로 동기가 맞으며, 비동기를 가능하게 한 것 은 런타임 에서 이벤트루프와 같은 설계를 도입하여 싱글스레드를 보완 한 것이다' 와 같이 생각하였습니다.

 

 

[당시 답변]

이와 관련하여 '동기적인데 이벤트루프, 큐로 인해서 비동기처럼 보이는거 뿐일거로 알고 있어요' 와 같은 답변들도 있었으며, 해당 질문을 통하여 토론방에서 '자바스크립트는 어떻게 비동기를 실행할까' 에 대한 이상적인 토론이 오고갔습니다.


Q.2 console.log 랑 value값이 어떻게 찍힐지 예상해보세요~!

감사하게도 이와 관련된 재미있는 문제를 올려주신분이 있습니다.

 let value = 100;

const delay = () => {
  return new Promise((resolve, reject) => {
    console.log(0, value);
    setTimeout(() => {
      console.log(1, value);

      value = 200;
      console.log(2, value);
      resolve(value);
    });
  });
};

(async () => {
  const output = await delay();
  console.log("3", output);
})();

value = 300;

 

[사고의 맥락]

1️⃣ console.log(0, value); → 0 100

  • delay() 함수가 실행되면, 동기적으로 console.log(0, value);가 실행됩니다.
  • 이 시점에서 value는 100입니다.

2️⃣ value = 300;

  • 비동기 함수인 **await delay()**는 Promise가 처리될 때까지 일시 중단됩니다.
  • value는 300으로 변경됩니다.

3️⃣ setTimeout 콜백 실행 → 1 300

  • 이벤트 루프에 의해 **setTimeout()**이 실행되면, 변경된 **value = 300**이 출력됩니다.

4️⃣ value = 200; → 2 200

  • setTimeout 내부에서 **value**가 200으로 업데이트되고, **2 200**이 출력됩니다.

5️⃣ resolve(value);

  • Promise가 fulfilled 상태가 되어, await 다음 줄이 실행될 준비가 됩니다.
  • 이때 value는 200입니다.

6️⃣ console.log("3", output); → 3 200

  • **output**에는 **resolve(value)**로 전달된 200이 저장되어 출력됩니다.

[당시 답변]

0 100

1 300

2 200

3 200

 

 

[정답]

 

 

[공유]

Node.js와 V8의 이벤트 루프 차이

V8 엔진 (브라우저 환경) Node.js의 이벤트 루프
V8 엔진에서는 이벤트 루프가 매크로태스크(예: setTimeout)보다 항상 마이크로태스크(예: Promise, process.nextTick)를 우선 처리합니다. 이는 이벤트 루프의 각 반복에서 콜 스택이 비워질 때마다 마이크로태스크 큐를 우선적으로 비우는 구조입니다. Node.js는 V8 엔진 위에 구축된 환경이지만, 이벤트 루프의 동작 방식은 libuv 라이브러리에 의해 제어됩니다. Node.js 이벤트 루프는 여러 단계로 나뉘며, 각 단계가 끝날 때마다 마이크로태스크 큐를 비우지 않습니다. 대신, 특정 단계에서만 마이크로태스크(예: Promise의 후속 작업)가 처리됩니다.

3. 후기

이전에 블로그 포스팅을 했던 내용이라 꽤 관심있게 토론을 진행하였습니다. 다시금 자바스크립트 동작원리에 대해 공부할 수 있어 좋았고 다음에 더 흥미로운 주제로 찾아 뵙겠습니다!!

+ Recent posts

  1. 머리말
  2. Stack
  3. 큐(Queue)
  4. 이벤트 루프