이벤트 루프

MyeonghoonNam·2021년 8월 15일
1

자바스크립트의 호출 스택과 이벤트 루프

자바스크립트는 싱글 스레드 기반의 언어입니다. 즉, 자바스크립트는 하나의 호출 스택을 가집니다. 하나의 호출 스택을 가진 단일 스레드로 동작하는 자바스크립트에서 어떻게 동시성을 지원할까요? 답은 이벤트 루프입니다. 자바스크립트는 이벤트 루프 기반의 비동기 방식으로 Non-Blocking IO를 지원합니다.

Node.js의 특징으로 자주 소개되는 내용입니다. Node.js가 V8(자바스크립트 엔진)으로 빌드 된 이벤트 기반 자바스크립트 런타임이기 때문에 브라우저에서의(크롬 V8) 자바스크립트와 동일한 특징을 가지고 있습니다.

Node.js는 확장성 있는 네트워크 애플리케이션(특히 서버 사이드) 개발에 사용되는 소프트웨어 플랫폼이다. 작성 언어로 자바스크립트를 활용하며 Non-blocking I/O와 단일 스레드 이벤트 루프를 통한 높은 처리 성능을 가지고 있다.

크롬 V8는 웹 브라우저를 만드는 데 기반을 제공하는 오픈소스 자바스크립트 엔진이다. 구글 크롬 브라우저와 안드로이드 브라우저, Node.js 등에서 사용된다.

Non-blocking I/O(Asynchronous I/O 혹은 Non-sequential I/O): Non-blocking I/O란, 입출력 처리는 시작만 해둔 채 완료되지 않은 상태에서 다른 처리 작업을 계속 진행할 수 있도록 멈추지 않고 입출력 처리를 기다리는 방법을 말한다


자바스크립트 엔진의 구성요소

아래 그림은 자바스크립트 엔진의 모습입니다.

자바스크립트 엔진은 Heap과 Call Stack으로 이루어져 있습니다.

  • Heap : 메모리 할당이 일어나는 영역
  • Call Stack : 코드 실행에 따라 호출 스택이 쌓이는 영역

런타임 환경

위 그림에서도 알 수 있지만 중요한 사실은 자바스크립트에는 이벤트 루프가 없습니다. V8과 같은 자바스크립트 엔진은 단일 호출 스택을 사용하고, 요청이 들어오면 그 순서에 따라 순차적으로 처리할 뿐입니다.

그러면 비동기 요청은 어떻게 처리할까요?

자바스크립트 엔진을 구동하는 런타임 환경(브라우저나 Node.js)이 담당합니다.

런타임 환경이 제공하는 것

  • Web APIs

    • DOM(document)

    • AJAX(XMLHttpRequest)

    • Timeout(setTimeout)

  • Event Loop

이렇게 런타임 환경과 자바스크립트 엔진이 만나서 익숙한 이벤트 루프 그림이 완성됩니다.

Alt Javascript EventLoop

위 그림과 같이 우리가 비동기 호출을 위해 사용하는 Web API, Event Loop, Task Queue는 자바스크립트 엔진 외부에 런타임 환경에 구현이 되어있습니다.


단일 호출 스택(single-thread)의 단점

브라우저에서 호출 스택에 실행해야 하는 함수가 쌓여있는 동안 다른 일을 할 수 없습니다. 이 상태를 blocked라 합니다. 이 상태에서 브라우저는 렌더링을 할 수도 없고, 다른 코드를 실행할 수도 없습니다.

  • 브라우저 자체가 동작을 하지 않음.

  • 매끄러운 화면 UI를 제공하지 못하게 됨.

  • 프로그램 동작 중에 이러한 현상은 사용자 경험을 완전히 망가뜨리는 일이다.

Alt StackOverFlow

위 그림들과 같은 예제로 만약 호출 스택이 계속해서 호출이 무한정으로 쌓인다면 브라우저는 위 처럼 제 기능을 못하고 멈춰버리는 현상이 발생하는데 이러한 오류를 Stack Overflow 현상이라 부릅니다.


동시성과 이벤트 루프

위 예제는 극단적으로 재귀 호출을 하는 예제이지만, 재귀 호출이 아니더라도 비슷한 현상은 충분히 발생할 수 있습니다. 호출 스택에 처리 시간이 엄청나게 오래 걸리는 함수가 실행된다면 위 예제와 같이 함수가 실행되는 동안 브라우저는 아무 작업도 하지 못하는 blocked 상태가 되고 대기 상태가 됩니다.

함수의 내부 호출 스택이 일청 수치까지 늘어난다면 Stack Overflow 가 발생할 수도 있고요.

이러한 문제를 해결하기 위해 이벤트 루프를 통한 동시성 확보를 해야 합니다.

적절하게 task를 쪼개서 비동기 호출을 하고, 또 중간중간 렌더링 등 UI 갱신이 이루어질 수 있도록 호출 스택이 빈 상태가 되도록 해주어야 한다.

이벤트 루프

이벤트 루프는 하나의 단순한 동작만을 수행합니다. 호출 스택과 Task Queue를 감시하면서, 만약 호출 스택이 비어있다면 이벤트 루프는 큐에서 첫 번째 Task를 호출 스택에 넣고 해당 Task가 수행됩니다.

이러한 반복을 이벤트 루프에서는 tick이라고 부릅니다.

task queue는 이벤트 루프의 메시지를 기다리고 메시지가 들어오면 task queue에 task를 추가합니다.

그리고 이벤트 루프는 가장 오래된 메시지부터 시작해서 메시지를 처리합니다(FIFO : 큐 자료구조의 특징). 메시지를 처리한다는 것은 함수를 실행해서 호출 스택에 올린다는 뜻입니다. 메시지가 처리되면 또 호출 스택이 빌 때까지 기다렸다가 메시지를 처리하고 하는 동작(tick)을 반복합니다.

그런데 이벤트 루프가 다발적으로 발생한 메시지들을 큐에 쌓고 실행을 해주면서 동시성을 확보하는 것이지 실제로 동시에 동작이 수행되는 것은 아닙니다.

실제 실행 자체는 호출 스택에 올라가서 수행이 됩니다.


Task Queue vs Microtask Queue vs Animation Frames

앞에 까지는 모든 비동기 동작이 Task Queue에 쌓이는 것처럼 설명을 했는데, 실제로는 여러 Queue가 존재합니다.

ES6에 들어오면서 새로운 컨셉인 Microtask Queue가 도입되었습니다. Microtask Queue는 Task Queue와 동일한 계층에 존재하고 프로미스의 비동기 호출 시 Microtask Queue에 쌓이게 됩니다.

이것에 대해 이해하기 위해서는 브라우저의 이벤트 루프가 taskmicrotask를 어떻게 다루는지 알아야 합니다.

각각의 큐에 들어가는 함수들

  • Microtask Queue

    Promise, process.nextTick, Object.observe, MutationObserver

  • Task Queue

    setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI 렌더링


브라우저의 이벤트 루프 우선순위

  • 이벤트 루프는 실행 순서를 보장하는 여러 queue에서 어떤 task를 꺼내서 실행시킬지 결정한다.
  • 이를 통해 브라우저는 우선순위가 높은 task를 먼저 실행하도록 할 수 있다.
  • microtask는 일반 task보다 높은 우선순위를 가지고 있다.

Microtask 외에도 Queue는 또 있습니다. 바로 requestAnimationFrame에 의해 등록되는 Animation Frames입니다.

Animation frames 작업이 완료되면 브라우저 렌더링 작업이 이루어집니다.

정리하자면 자바스크립트는 비동기 작업을 수행할 때 Web APIs를 통해 여러 queue에 등록된 작업들을 우선순위에 따라 꺼내서 처리합니다.

이벤트 루프의 우선순위

  1. 호출 스택의 작업을 처리한다.

  2. 호출 스택이 비어있다면 microtask queue를 확인하고 작업이 있다면 microtask queue의 task를 작업을 호출 스택으로 넣고 처리한다.

  3. 만약 microtask가 비어있다면 Animation Frames를 확인하고 브라우저 렌더링이 발생한다.

  4. 1, 2, 3번 작업이 완료되었다면 task queue를 확인하고 작업이 있다면 task queue의 작업을 호출 스택으로 넣고 처리한다.


참고자료

profile
꾸준히 성장하는 개발자를 목표로 합니다.

0개의 댓글