티스토리 뷰

JavaScript

JS - 동작원리와 비동기처리

김관장 2022. 11. 11. 00:14

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

 

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

  • 호출 스케줄링(scheduling a call)은 일정 시간이 지난 후에 원하는 함수를 예약 실행(호출)할 수 있게 하는 것을 의미합니다.

  • 매크로태스크큐와 마이크로태스크큐는 서로 다른 별로의 큐 입니다.
  • 매크로태스크 큐(MacroTask Queue) : 기존의 태스크 큐
  • 마이크로태스크(MicroTask Queue 혹은 Job Queue) : ES6에서 Promise와 함께 소개된 개념으로 매크로태스크 큐보다 처리 우선순위가 높습니다.

📌 자바스크립트 엔진

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

    • Memory Heap

      • 메모리 할당이 일어나는 곳, 콜 스택의 요소인 실행 컨텍스트는 힙에 저장된 객체를 참조합니다.
      • 객체는 원시값과 달리 크기가 정해져 있지 않기 때문에 할당해야 할 메모리 공간의 크기는 동적 할당 됩니다.
    • Call Stack

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

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

 

아래와 같은 코드를 실행하면

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

 

먼저 first 함수가 호출(push)되고, 그안의 second함수가 호출되고, 마지막으로 그 안의 third 함수가 호출됩니다. 

  • main() 함수는 처음 실행시 전역 컨텍스트(함수가 호출 되었을때 생성되는 환경)입니다.
  • 함수의 실행이 완료되면 콜 스택에서 지워집니다. third, second, first, main 순으로 pop되기 때문에 "세번째, 두번째, 첫번째" 순으로 콘솔에 찍히며 최종적으로 호출 스택이 지워이 비워집니다.

 

📌 자바스크립트 런타임

  • 자바스크립트 런타임(환경)이란 자바스크립트가 구동되는 환경을 말합니다. 자바스크립트 런타임의 종류로는 웹 브라우저(크롬, 파이어폭스 등..)와 Node.js라는 프로그램이 있습니다.
  • 자바스크립트 런타임은 자바스크립트 엔진(Memory heap, Call stack), Web apis, 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) 구조 입니다.

 

📌 자바스크립트 비동기 처리의 필요성

// 비동기 처리 전
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를 통해 Data를 받아오는 과정, Data를 서버에 업데이트하는 과정, HTML요소로 만든 애니메이션 효과 등은 모두 비동기적으로 처리가 됩니다.

 

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

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

 

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

console.log('시작');

setTimeout(middleFn, 5 * 1000);

console.log('끝');

// 시작 끝 중간 순으로 콘솔에 찍힘.
  • 더 자세하게 위 코드를 통해서 어떻게 비동기가 처리되는지 알아보겠습니다.
    (이미지가 많아 접은 글로 작성하였습니다. 아래 더보기를 클릭해주세요.)
더보기
  • 1. 먼저 전역 컨텍스트 main() 함수가 콜 스택에 쌓이고 그 다음으로 console.log('시작')이 콜 스택에 쌓입니다.

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

 

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

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

 

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

    * 호출 스케줄링(scheduling a call)이란? 일정 시간이 지난 후에 원하는 함수를 예약 실행(호출)할 수 있게 하는 것

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

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

 

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

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

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

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


    현재 실행 컨텍스트 : log('끝')

 

  • 5. 그리고 마지막으로 남아있던 전역 컨텍스트 main()이 종료되고 콜 스택에서 pop됩니다. 따라서 현재 콜 스택에는 아무런 실행 컨텍도 남아있지 않게 됩니다.그리고 setTimeout에서 설정한 지연 시간에 따라 4 ~ 5번 과정 진행 중 혹은 진행 완료 후에 콜백 함수가 태스크 큐에 푸시되어 대기 중 입니다.
    (위 예시에서는 대기 시간을 5초 설정했기때문에 main()까지 pop된 상태에서 콜백함수가 태스크 큐에 푸시되었을 것 입니다.)

    현재 실행 컨텍스트 : 없음

 

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

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

 

  • 7. 이후 콜백함수가 종료되어 콜 스택에서 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 생성, 메인 스레드를 구성하는 JS 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 후에 실행될까?

자바스크립트의 비동기 처리와 이벤트루프, 마이크로태스크 큐에 대해 설명합니다.

velog.io

 

[JS] Javascript 동작 원리와 비동기처리

자바스크립트는 싱글스레드이다. 어떻게 자바스크립트는 싱글스레드이면서 비동기인 것일까? 자바스크립트가 어떻게 동작하는지 내부 원리에 대해 알아보자. 📌 Contents 자바스크립트 엔진 자

ingg.dev

 

⭐️🎀 JavaScript Visualized: Promises & Async/Await

Ever had to deal with JS code that just... didn't run the way you expected it to? Maybe it seemed lik...

dev.to

 

이벤트 루프와 매크로태스크, 마이크로태스크

 

ko.javascript.info

 

이벤트 루프와 태스크 큐 (마이크로 태스크, 매크로 태스크)

자바스크립트는 싱글 스레드 기반의 언어이고, 자바스크립트 엔진은 하나의 호출 스택만을 사용한다. 이는 요청이 동기적으로 처리되어, 한 번에 한 가지 일만 처리할 수 있음을 의미한다. 만약

velog.io

댓글