헷갈릴 수 있는 용어들에 대한 간략한 설명입니다.

  • 블로킹

느린 동작이 스택에 남아있는 것을 의미합니다.

  • 호출 스케줄링(scheduling a call)

일정 시간이 지난 후에 원하는 함수를 예약 실행(호출)할 수 있게 하는 것을 의미합니다.

  • 매크로태스크큐와 마이크로태스크큐는 서로 다른 별도의 큐 입니다.

    • 매크로태스크 큐(MacroTask Queue) : 기존의 태스크 큐

    • 마이크로태스크 큐(MicroTask Queue) : ES6에서 Promise와 함께 소개된 개념으로 매크로태스크큐보다 처리 우선순위가 높습니다.


📌 JavaScript 엔진


  • 자바스크립트 엔진은 자바스크립트 코드를 해석하고, 실행하는 인터프리터입니다. (크롬과 Node.js에서 사용되는 V8엔진이 대표적)

    • Memory Heap

      • 메모리 할당이 일어나는 곳, 콜 스택의 요소인 실행 컨텍스트는 힙에 저장된 객체를 참조합니다.

      • 객체는 원시값과 달리 크기가 정해져있지 않기 때문에 할당해야 할 메모리 공간의 크기는 동적으로 할당 됩니다.

    • Call Stack

      • 함수를 호출하면 함수 실행 컨텍스트가 순차적으로 콜 스택에 푸시되어 동기적으로 실행됩니다.

자바스크립트는 싱글스레드 언어입니다. 이는 하나의 싱글 호출 스택(Call Stack)만 가지고 있다는 뜻이며, 자바스크립트의 엔진은 한 번에 하나의 task만 실행할 수 있는 동기 프로그램 언어입니다. 하나의 작업이 끝나면 pop하고 바로 아래의 함수나 코드를 실행합니다. 작업을 차례대로 실행하므로 하나의 작업이 끝날 때까지 또 다른 작업을 실행하지 않습니다.

예를들어 아래와 같은 코드를 실행해보면 먼저 first함수가 호출(push)되고, 그 안의 second함수가 호출되고, 마지막으로 그 안의 third함수가 호출됩니다.

function first(){
    second();
    console.log("첫번째");
}
function second(){
    third();
    console.log("두번째");
}
function third(){
    console.log("세번째");
}
first(); // 세번째 두번째 첫번째

  • main() 함수는 처음 실행시 전역 컨텍스트(함수가 호출 되었을때 생성되는 환경)입니다.

  • 함수의 실행이 완료되면 콜 스택에서 지워집니다. third, second, first, main 순으로 pop되기 때문에 "세번째, 두번째, 첫번째" 순으로 콘솔에 찍히고 최종적으로 호출 스택이 비워집니다.



📌 JavaScript 런타임


자바스크립트 런타인(환경)이란 자바스크립트가 구동되는 환경을 의마합니다. 자바스크립트 런타임의 종류는 웹 브라우저(크롬, 파이어폭스 등..)와 Node.js라는 프로그램이 있습니다.

  • 자바스크립트 런타임은 자바스크립트 엔진(Memory Heap + Call Stack), Web Api, Task Queue, Event Loop를 갖습니다.

    • Web API (https://developer.mozilla.org/en-US/docs/Web/API)

      • Web API는 브라우저에서 제공되는 API입니다.
      • 자바스크립트 엔진에서 정의되지 않았던 setTimeout, HTTP 요청 메서드(Ajax), DOM 이벤트 등의 메서드를 지원합니다.

    • Task Queue

      • setTimeout, setInterval과 같은 비동기 함수들의 콜백 함수 또는 이벤트 핸들러가 일시적으로 보관되는 영역입니다.
      • 태스크 큐에 일시적으로 보관된 함수들은 비동기 처리 방식으로 동작합니다.
      • 이벤트 루프가 정한 순서대로 줄을 서 있으므로 콜백 큐(Callback Queue)라고도 합니다.

    • Event Loop

      • 콜 스택에 현재 실행 중인 실행 컨텍스트가 있는지, 태스크 큐에 대기 중인 함수가 있는지 반복해서 확인합니다.

      • 만약 콜 스택이 비어있고 태스크 큐에 대기 중인 함수가 있다면 이벤트 루프는 순차적으로 태스크 큐에 대기 중인 함수를 콜 스택으로 이동시킵니다.

      • 큐(Queue) - FIFO(First In First Out) 구조 입니다.



📌 JavaScript 비동기 처리의 필요성


// 비동기 처리 전
const sleep = (func, delay) => {
  const delayUntil = Date.now() + delay;
  while (Date.now() < delayUntil);
  func();
};

const str = () => console.log("시작");
const end = () => console.log("끝");

sleep(str, 5 * 1000);
end();

ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ

// 비동기 처리 후 : setTimeout
const str = () => console.log('시작');
const end = () => console.log('끝');

setTimeout(str, 5 * 1000);
end();

극단적인 예를 들어보겠습니다.

  • 비동기 처리 전 스크립트를 실행하면 5초 뒤 "시작" "끝"이 콘솔로 찍히는 것을 확인할 수 있습니다.
    이런 로직들은 사용자들이 사용자가 사이트가 느리다고 생각하는 주요 원인이 될 것입니다.

  • 비동기 처리 후 스크립트를 실행하면 콘솔에 "끝"이 찍히고 몇초 뒤에 "시작"이 콘솔로 찍힙니다.

  • 이처럼 오래 걸리는 태스크를 처리할 때 발생하는 블로킹을 막고 싱글스레드 언어인 자바스크립트의 동기적인 실행 컨텍스트 스택을 효율적으로 스케줄링하기 위해 비동기 처리를 사용합니다.

setTimeout, setInterval, HTTP request, DOM Event 모두 비동기 처리 방식으로 동작합니다.

즉, API를 통해 데이터를 받아오는 과정, 데이터를 업데이트하는 과정, HTML요소로 만든 애니메이션 효과 등은 모두 비동기적으로 처리가 됩니다.



📌 JavaScript 비동기 처리


🔎 자바스크립트의 비동기 처리 과정

아래 코드를 통해서 어떻게 비동기가 처리되는지 알아보겠습니다.

const middleFn = () => console.log('중간');

console.log('시작');

setTimeout(middleFn, 5 * 1000);

console.log('끝');

// 시작 끝 중간 순으로 콘솔에 찍힘.
  1. 먼저 전역 컨텍스트 main()함수가 콜 스택에 쌓이고 그 다음으로 console.log('시작')이 쌓입니다.

    현재 실행 컨텍스트 : console.log("시작")

  2. "시작"이 콘솔에 찍히고 종료되며 콜 스택에서 pop됩니다. 그리고 다음으로 setTimeout함수가 실행되면서 콜 스택에 쌓입니다.

    현재 실행 컨텍스트 : setTimeout()

  3. setTimeout() 함수가 실행되면 자바스크립트 엔진이 처리하지 않고 Web API가 처리하므로 콜백 함수를 전달하고 setTimeout 작업을 요청합니다. (콜백함수의 호출 스케줄링) 그리고 종료되어 콜 스택에서 pop합니다.

    이때 호출 스케줄링, 즉 타이머 설정과 타이머가 만료되면 콜백 함수를 태스크 큐에 푸시하는 것은 브라우저의 역할 입니다.

    현재 실행 컨텍스트 : -

  4. 다음으로 console.log("끝")이 콜 스택에 쌓입니다.

브라우저에서 수행하는 setTimeout 콜백 작업자바스크립트 엔진이 수행하는 console.log("끝")이 병렬로 처리됩니다.

(📌) 브라우저에서 수행되는 작업
브라우저는 타이머(setTimeout에서 설정한)를 설정하고 타이머의 만료를 기다립니다. 이후 타이머가 만료되면 콜백 함수 middleFn이 태스크 큐에 푸시됩니다. 그리고 콜 스택이 빈 상태가 될 때까지 대기하다가 호출됩니다.

(📌) 자바스크립트 엔진에서 수행되는 작업
console.log("끝")이 호출되어 실행 컨텍스트가 생성되고 콜 스택에 푸시되어 현재 실행 중인 컨텍스트가 됩니다. 이후 종료되면 콜 스택에서 pop됩니다.

현재 실행 컨텍스트 : console.log("끝")

  1. 마지막으로 남아있던 전역 컨텍스트 main()이 종료되고 콜 스택에서 pop됩니다. 따라서 현재 콜 스택에는 아무런 실행 컨텍도 남아있지 않게 됩니다. 그리고 setTimeout에서 설정한 지연시간에 따라 4~5번 과정이 진행 중이거나 진행 완료가 되었다면 콜백 함수가 태스크 큐에 푸시되어 대기 중입니다.

    현재 실행 컨텍스트 : -

  2. 이벤트 루프에 의해 콜 스택이 비어있음이 감지되고 태스크 큐에 대기 중인 콜백함수 middleFn이 콜 스택에 푸시됩니다. 따라서 middleFn의 실행 컨텍스트가 생성되고 콜 스택에 푸시되어 현재 실행 중인 컨텍스트가 됩니다.

    현재 실행 컨텍스트 : setTimeout의 콜백함수인 middleFn()

  3. 이후 콜백함수가 종료되고 콜 스택에서 pop되며 최종 종료됩니다.


🔎 자바스크립트의 비동기 처리 과정 - Promise와 이벤트 루프

위 과정을 통해 setTimeout, setInterval과 같은 비동기 함수들의 콜백 함수 또는 이벤트 핸들러는 태스크 큐에 푸시해두었다가 콜 스택이 비었을 때 이벤트 루프에 의해서 콜 스택에 푸시되어 실행된다는 것을 알았습니다.

그렇다면 ES6에서 비동기 처리를 위해 추가된 Promise는 어떻게 처리가 되는지 확인해보겠습니다.

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

Promise.resolve()
  .then(function() {
    console.log('promise1');
  })
  .then(function() {
    console.log('promise2');
  });

console.log('script end');

/* 콘솔에 찍히는 순서
script start
script end
promise1
promise2
setTimeout
*/

setTimeout의 대기시간을 0으로 주었음에도 왜 Promise 보다 느리게 호출이 된 걸까? 라는 의문이 생깁니다. 이는 마이크로태스크큐가 기존의 태스크큐인 매크로태스크 큐보다 처리 우선 순위가 높기 때문입니다.

⚙️ ES6 MicroTask Queue

  • MicroTask Queue(혹은 Job Queue)는 ES6에서 Promise와 함께 소개된 개념으로 마이크로태스크 큐를 사용하는 대표적인 함수가 Promise입니다.

  • 마이크로태스크 큐와 기존의 태스크 큐(매크로태스크 큐=MacroTask Queue)는 서로 다른 별도의 큐 입니다.

  • ⭐ 마이크로태스크 큐는 기존의 태스크 큐보다 우선 순위가 높습니다. 따라서 이벤트 루프는 콜 스택이 비었을 때 먼저 마이크로태스트 큐에 대기하고 있던 함수를 가져와서 실행하고, 없다면 기존의 태스크 큐에서 대기하고 있던 함수를 가져와서 실행합니다.


⚙️ MacroTask Queue vs MicroTask Queue

  • Macrotask queue

    • 이벤트 루프는 매크로태스크 큐에 있는 것을 실행시키기 시작할 때 매크로 태스크만 실행시킵니다. 매크로태스크가 추가한 매크로태스크는 다음 이벤트 루프가 실행될 때까지 실행되지 않습니다.

    • HTML 파싱, DOM 이벤트, 메인 스레드를 구성하는 JavaScript Code, 페이지 로드나 네트워크 이벤트, 타이머와 같은 여러 이벤트를 포함합니다.

    • 지연 시간이 0초인 setTimeout()을 통해 새로운 매크로태스크를 스케줄링 할 수 있습니다.

  • Microtask queue

    • 마이크로태스크들은 실행하면서 새로운 마이크로태스크를 큐에 추가할 수도 있습니다. 새롭게 추가된 마이크로태스크도 큐가 빌 때까지 계속해서 체크합니다.

    • Promise의 후속 처리 메서드의 콜백 함수를 처리합니다.

    • 다른 이벤트 핸들링, 렌더링, 매크로태스크가 실행되기 전에 완료됩니다.

    • 마이크로태스크는 바로 다음 마이크로태스크를 실행하기때문에 그 사이에 UI 혹은 네트워크 변화가 없습니다.

    • 브라우저가 리렌더링 되기 전에 실행되기때문에 마이크로태스크 큐의 작업이 늦게 처리되면 브라우저의 UI 렌더링이 지연 될 수 있습니다.

  • Macrotask queue를 이용하는 함수
    : setTimeout(), setInterval(), setImmediate(), requestAnimationFrame, I/O, UI 렌더링 등등..

  • Microtask queue를 이용하는 함수
    process.nextTick(), Promise, queueMicrotask 등등..


🔗 참고한 글

setTimeout(foo, 0)에서 foo는 정말 0ms 후에 실행될까?
[JS] Javascript 동작 원리와 비동기처리
JavaScript Visualized: Promises & Async/Await
이벤트 루프와 매크로태스크, 마이크로태스크
이벤트 루프와 태스크 큐 (마이크로 태스크, 매크로 태스크)

profile
FE 개발자

0개의 댓글