JavaScript - Event Loop

이소라·2022년 11월 28일
0

JavaScript

목록 보기
13/22

Event Loop

Event Loop & Concurrency

  • 브라우저는 single-thread에서 event-driven 방식으로 동작함
    • thread가 하나이므로, 하나의 작업(task)만 처리할 수 있음
      • 실제 동작하는 웹 어플리케이션은 많은 작업을 동시에 처리되는 것처럼 동작함 : 동시성(Concurrency)
    • Event Loop가 JavaScript의 동시성을 지원함

  • 브라우저의 환경을 그림으로 표현하면 아래와 같음

JavaScript 엔진

  • JavaScript 엔진은 단순히 작업이 요청되면 Call Stack을 사용하여 작업을 순차적으로 실행함
  • JavaScript 엔진은 크게 2개의 영역으로 나뉨
    • Call Stack
      • 작업이 요청되면(함수가 호출되면) 요청된 작업이 순차적으로 Call Stack에 쌓임
      • JavaScript는 단 하나의 Call Stack을 사용하므로, 해당 task가 종료되기 전까지는 다른 task를 수행할 수 없음
    • Heap
      • 동적으로 생성된 객체 인스턴스가 할당되는 영역

Event Queue(Task Queue) & Event Loop

  • 동시성을 지원하기 위해 필요한 비동기 요청(이벤트 포함) 처리는 JavaScript 엔진을 구동하는 환경 즉 브라우저(또는 Node.js)가 담당함
    • Event Queue(Task Queue)
      • 비동기 처리 함수의 콜백함수, 비동기식 이벤트 핸들러, Timer 함수(setTimeout, setInterval)의 콜백함수가 보관되는 영역
      • Event Queue에 보관된 콜백함수들은 Call Stack에 비어졌을 때 Event Loop에 의해 순차적으로 Call Stack으로 이동되어 실행됨
    • Event Loop
      • Call Stack 내에서 현재 실행중인 작업이 있는지, Event Queue에 작업이 있는지 반복하여 확인함
      • Call Stack이 비어있다면 Event Queue 안의 작업이 Call Stack으로 이동하고 실행됨

Code Examples

// Example
function func1() {
  console.log('func1');
  func2();
}

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

  func3();
}

function func3() {
  console.log('func3');
}

func1(); // func1 => func3 => func2
  • 위 예제 설명
    1. 함수 func1이 호출되면, 함수 func1이 Call Stack에 쌓임
    2. 함수 func1이 함수 func2를 호출하므로, 함수 func2가 Call Stack에 쌓임
    3. 함수 func2가 호출되면, setTimeout이 호출됨
      • setTimeout의 콜백함수는 즉시 실행되지 않고 지정 대기 시간만큼 기다리다가 'tick' 이벤트가 발생하면 Task Queue로 이동함
      • Task Queue로 이동한 콜백함수는 Call Stack이 비어졌을 때 Call Stack으로 이동하여 실행됨
    4. 함수 func2가 함수 func3을 호출하므로, 함수 func3이 Call Stack에 쌓임
    5. 함수 func3이 호출되고 종료되면, Call Stack에서 함수 func3이 제거됨
    6. 함수 func2가 종료되면, Call Stack에서 함수 func2이 제거됨
    7. 함수 func1가 종료되면, Call Stack에서 함수 func1이 제거됨
    8. Call Stack이 비어졌으므로, setTimeout의 콜백함수가 Call Stack으로 이동하여 실행됨

  • DOM 이벤트 핸들러도 setTimeout의 콜백함수와 비슷하게 동작함
function func1() {
  console.log('func1');
  func2();
}

function func2() {
  // <button class="foo">foo</button>
  const elem = document.querySelector('.foo');

  elem.addEventListener('click', function () {
    this.style.backgroundColor = 'indigo';
    console.log('func2');
  });

  func3();
}

function func3() {
  console.log('func3');
}

func1();
  • 위 예제 설명
    1. 함수 func1이 호출되면, 함수 func1은 Call Stack에 쌓임

    2. 함수 func1이 함수 func2를 호출하므로, 함수 func2가 Call Stack에 쌓임

    3. 함수 func2가 호출되면, addEventListener가 호출됨

      • addEventListener의 콜백함수는 핸들러가 등록된 DOM 요소에서 특정한 이벤트가 발생했을 때 Task Queue로 이동함
      • Task Queue로 이동한 addEventListener의 콜백함수는 Call Stack이 비어졌을 때 Call Stack으로 이동하여 실행됨



Task

  • macrotask

    • 외부 스크립트 <script src="...">가 로드될 때, 이 스크립트를 실행하는 것
    • 사용자가 마우스를 움직일 때 mousemove 이벤트와 이벤트 핸들러를 실행하는 것
    • setTimeout에서 설정한 시간이 다 된 경우, 콜백 함수를 실행하는 것
  • microtask

    • 주로 promise를 사용해 만듬
      • promise.then/catch/finally 핸들러가 microtask가 됨
    • await를 사용해 만들기도 함
  • JavaScript 엔진은 macrotask 하나를 처리할 때마다 또 다른 macrotask나 렌더링 작업을 하기 전에 microtask queue에 쌓인 microtask 전부를 처리함

    • 처리 순서
      • macrotask 1개 => 전체 microtasks => 렌더링 => 다음 macrotask 1개
    • 처리 순서가 중요한 이유
      • 모든 microtask를 동일한 환경에서 처리할 수 있음
        • 렌더링이나 네트워크 요청 등의 작업들이 모든 microtask가 처리된 직후 처리되므로, 이벤트나 네트워크 요청에 영향을 받지 않음
        • microtask 전체가 처리되는 동안 UI 변화나 네트워크 이벤트 핸들링이 일어나지 않음


  • setTimeout과 promise를 같이 사용한 코드 예제
setTimeout(function() { // (A)
    console.log('A');
}, 0);
Promise.resolve().then(function() { // (B)
    console.log('B');
}).then(function() { // (C)
    console.log('C');
});
  • 위 예제 설명
    1. setTimeout 함수는 콜백함수 A를 Task Queue(Macrotask Queue)에 추가함
    2. then 메서드는 콜백함수 B를 Microtask Queue에 추가함
    3. Event Loop가 Task Queue 대신 Microtask Queue가 비어있는지 먼저 확인하고, 그 queue에 있는 콜백함수 B를 실행함
    4. then 메서드가 콜백함수 C를 Microtask Queue에 추가함
    5. Event Loop는 다시 Microtask Queue를 확인하고, 그 queue에 있는 콜백함수 C를 실행함
    6. Event Look는 Microtask Queue가 비어있음을 확인 한 후 Task Queue에 있는 콜백함수 A를 실행함

Scheduling macrotask & microtask

  • 새로운 macrotask를 스케쥴링하는 방법
    • 지연시간이 0인 setTimeout(f)를 사용하기
      • 이 방법을 사용하면 계산이 복잡한 큰 작업을 여러 작업으로 쪼갤 수 있음
      • 이벤트가 완전히 처리되고 난 후(버블링이 끝난 후)에 특정 작업을 수행하도록 스케쥴링할 때도 사용됨

setTimout(f,0)의 '0'

  • 브라우저는 내부적으로 타이머의 최소단위(Tick)를 정하여 관리하기 때문에, 실제로는 즉시가 아니라 그 최소단위만큼 지난 후에 태스크 큐에 추가되게 됨
    • 이 최소단위는 브라우저별로 조금씩 다름
      • 예) 크롬 브라우저의 경우, 최소단위로 4ms 사용
  • 새로운 microtask를 스케쥴링하는 방법
    • queueMicrotask(f) 사용하기;
    • promise.then/catch/finally 핸들러 사용하기



참고

0개의 댓글