[ Javascript ] Event Loop

dayglow·2023년 12월 7일
0

Javascript

목록 보기
7/7

자바스크립트 특징

자바스크립트는 멀티 스레드를 지원하는 Java와 python과 다르게 싱글 스레드 언어입니다.
싱글 스레드는 이름 처럼 한번에 하나의 작업만 수행만 가능한데,
웹 애플리케이션에서는 네트워크 요청이나 이벤트 처리, 타이머와 같은 작업을 멀티로 처리해야하는 경우가 많습니다.
만일 싱글 스레드로 브라우저 동작이 한번에 하나씩 수행하게 된다면, 우리는 파일을 다운로드 하는 동안 웹 서핑도 못하고 대기해야 할 것 입니다.
그래서 이렇게 처리가 오래 걸리고 반복적인 작업들은 자바스크립트 엔진이 아닌 브라우저 내부의 멀티 스레트인 Web APIs에서 비동기 + 논블로킹으로 처리됩니다.
비동기 + 논블로킹(Async + Non blocking)은 메인 스레드가 작업을 다른 곳에 요청하여 대신 실행하고, 그 작업이 완료되면 이벤트나 콜백 함수를 받아 결과를 실행하는 방식입니다.

즉, 비동기로 동작하는 핵심요소는 자바스크립트 언어가 아니라 브라우저라는 소프트웨어가 가지고 있다라고 보면 좋을 것 같습니다. ( Node.js 는 libuv 내장 라이브러리가 처리합니다. )

브라우저 동작을 제어하는 이벤트 루프

싱슬 스레드인 자바 스크립트의 작업을 멀티 스레드로 돌려 작업을 동시에 처리시키거나 여러 작업 중 어떤 작업을 우선으로 동작시킬 것인지 결정하는 세심한 컨트롤을 하기 위해 존재하는 것이 바로 이벤트 루프(Event Loop) 입니다.
이벤트 루프는 브라우저 내부의 Call Stack, Callback Queue, Web APIs등의 요소들을 모니터링하면서 비동기적으로 실행되는 작업들을 관리하고, 이를 순서대로 처리하여 프로그램의 실행 흐름을 제어하는 관리자입니다.

이벤트 루프의 동작 과정을 간단히 살펴보면, 자바스크립트의 setTimeout이나 fetch와 같은 비동기 자바스크립트 코드를 브라우저 Web APIs에게 맡기고 백그라운드 작업이 끝난 결과를 콜백 함수 형태로 Queue에 넣고 처리 준비가 되면 Call Stack에 넣어 마무리 작업을 진행합니다

이러한 이벤트 루프를 이용한 프로그램 방식을 이벤트 기반 ( Event Driven ) 프로그래밍이라고 합니다.
이벤트 기반 프로그래밍은 프로그램의 흐름이 이벤트에 의해 결정되는 방식입니다.
예를 들면, 사용자의 클릭이나 키보드 입력과 같은 이벤트가 발생하면, 그에 맞는 콜백 함수가 실행됩니다.
대표적으로 가장 많이 사용하는 addEventListener(이벤트명, 콜백함수) 가 있겠네요.

이벤트 기반 프로그래밍으로 비동기 작업을 쉽게 처리할 수 있고, 멀티 스레드 언어에 비해 단순하고 직관적인 코드 작성을 가능하게 하며, 브라우저와 같은 환경에서도 안정적인 실행을 가능하게 하여 사용자와의 상호작용을 높일 수 있습니다. 따라서 이를 이해하고 적절한 방식으로 비동기 작업을 처리하는 것은, 자바스크립트를 이용한 웹 개발에 있어서 매우 중요합니다.


브라우저의 기본 구조

  • Heap: 동적으로 생성된 자바스크립트 객체가 저장되는 공간입니다.
  • Call Stack: 자바스크립트 엔진이 코드 실행을 위해 사용하는 메모리 구조입니다.
  • Web APIs: 브라우저에서 제공하는 API 모음으로, 비동기적으로 실행되는 작업들을 전담하여 처리합니다. (AJAX 호출, 타이머 함수, DOM 조작 등)
  • Callback Queue: 비동기적 작업이 완료되면 실행되는 함수들이 대기하는 공간입니다.
  • Event Loop: 비동기 함수들을 적절한 시점에 실행시키는 관리자입니다.
  • Event Table: 특정 이벤트(timeout, click, mouse 등)가 발생했을 때 어떤 callback 함수가 호출되야 하는지를 알고 있는 자료구조입니다.

Web APIs의 종류

Web APIs는 타이머, 네트워크 요청, 파일 입출력, 이벤트 처리 등 브라우저에서 제공하는 다양한 API를 포괄하는 총칭입니다. Web API는 브라우저에서 멀티 스레드로 구현되어 있습니다.
그래서 브라우저는 비동기 작업에 대해 메인 스레드를 차단하지 않고 다른 스레드를 사용하여 동시에 처리할 수 있습니다.

예를 들어 setTimout 비동기 작업은 Web APIs의 한 종류인 Timer API 에서 타이머 스레드를 사용하여 타이머를 수행합니다.
XMLHttpRequest, fetch와 같은 네트워크 관련 API는 네트워크 스레드를 사용하여 네트워크 요청과 응답을 처리합니다.

Web APIs의 대표적인 종류로는 다음과 같습니다.

  • DOM : HTML 문서의 구조와 내용을 표현하고 조작할 수 있는 객체입니다.
  • XMLHttpRequest : 서버와 비동기적으로 데이터를 교환할 수 있는 객체입니다.
  • Timer API : 일정한 시간 간격으로 함수를 실행하거나 지연시키는 메소드들을 제공합니다.
  • Console. API : 개발자 도구에서 콘솔 기능을 제공합니다.
  • Canvas API : <canvas> 요소를 통해 그래픽을 그리거나 애니메이션을 만들 수 있는 메소드들을 제공합니다.
  • Geolocation API : 웹 브라우저에서 사용자의 현재 위치 정보를 얻을 수 있는 메소드들을 제공합니다.

여기서 오해하지 않아야 하는 것은 모든 Web API들이 비동기로 동작되는 것이 아닙니다.
Web API에는 동기적으로 처리되는 것과 비동기적으로 처리되는 것이 모두 있습니다.
예를 들면 DOM API나 Console API는 동기적으로 처리되고 XMLHttpRequest, Timer API는 비동기적으로 처리됩니다.

Callback Queue의 종류

Web APIs가 여러 API들을 묶어 말하듯이, Callback Queue도 여러가지 종류의 Queue를 묶어 총칭하는 개념입니다.. Callback Queue에는 (macro)task queue와 microtast queue 두 가지 종류가 있습니다.

  • Task Queue : setTimeout, setInterval, fetch, addEvenListener 와 같이 비동기로 처리되는 함수들의 콜백 함수가 들어가는 큐입니다. ( macrotask queue 는 보통 task queue라고 부릅니다. )

  • Microtask Queue : promise.then, process.nextTick, MutationObserver 와 같이 우선적으로 비동기로 처리되는 함수들의 콜백 함수가 들어가는 큐입니다. (처리 우선순위가 높습니다.)

Callback Queue의 종류에 따라 이벤트 루프가 콜 스택으로 옮기는 순서가 달라집니다.
일반적으로 microtask queue가 가장 우선순위가 높아서 먼저 처리후 비운 다음 task queue의 콜백을 처리합니다.

Promise.then 결과가 setTimeout 보다 우선 된다는 것을 알고 계신다면 왜 프로미스가 먼저 처리되는지에 대한 이유가 이벤트 류프의 동작 원리와 관련이 있다는 걸 알 수 있을 것입니다.

그리고 같은 queue안에 적재되는 콜백이라도 어떠한 비동기 작업이냐에 따라 우선순위가 다른 태스크들이 있을 수 있습니다. 예를 들어 Microtask Queue에 적재되는 Promise와 MutationObserver 콜백 중 MutationObserver가 먼저 처리됩니다.

AnimationFrame Queue

브라우저의 큐는 콜백 큐 뿐만 아니라 브라우저 애니메이션 작업에 대한 처리를 담당하는 AnimationFrame Queue도 있습니다.
자바스크립트 애니메이션 동작을 제어하는 requestAnimationFrame메소드를 통해 콜백을 등록하면, 이 큐에 적재되어 브라우저가 repaint 직전에 AnimationFrame Queue에 있는 작업들을 전부 처리합니다.
따라서 자바스크립트 스타일 관련 코드들을 Animation Frame Queue에 비동기로 처리하도록 구성하면 브라우저가 애니메이션의 타이밍을 관리하고, 적절한 프레임 속도를 유지하고, 타른 탭이나 창에 있을 때 애니메이션을 중지함으로써 브라우저의 애니메이션 동작의 성능과 품질을 향상 시킬 수 있습니다.

관심이 있다면 해당 블로그를 추천드립니다.

이벤트 루프 동작 과정

다시 정리하면, 싱글 스레드인 자바스크립트에서도 작업의 동시 처리를 지원할 수 있는 비결에는 이벤트 루프가 자바스크립트 엔진과 브라우저의 웹 API를 연결하여 비동기적인 일 처리가 가능하기 때문입니다.
다만 모든 자바스크립트 코드를 비동기로 처리할 수 있는 것은 아닙니다.
자바스크립트에는 비동기로 동작하는 비동기 전용 함수가 있으며, 대표적으로 setTimeout이나 fetch, addEventListener 가 있습니다.

이벤트 루프는 비동기 한수 작업을 Web API에 옮기는 역할을 하고 작업이 완료되면 콜백을 큐에 적재했다가 다시 자바스크립트 엔진에 적재해 수행시키는 '작업을 옮기는 역할' 만 합니다.
작업을 처리하는 주체는 자바스크립트 엔진과 Web API입니다.
그래서 이벤트 루프는 Call Stack에 현재 실행 중인 작업이 있는지 그리고 Task Queue에 대기 중인 작업이 있는지 반복적으로 확인하는 일종의 무한 루프만을 돌고, 대기 작업이 있다면 작업을 옮겨주는 형태로 동작한다고 보면 됩니다. ( 그래서 이벤트 '루프'입니다. )

이벤트 루프 과정을 Lydia Hellie 님께서 고퀄리티 gif 애니메이션으로 표현한 이미지가 있어서 같이 보면서 소개하고자 합니다.

그리고 동작과정을 직접 코드를 작성하여 눈으로 보고 싶으신 분은 아래의 사이트를 추천합니다.
이벤트 루프 테스트 사이트

setTimeout 내부 동작 과정

function bar() {
  setTimeout(() => {
  	console.log("Second")
  }, 500);
}

function foo() {
  console.log("First");
}

function baz() {
  console.log("Third");
}

bar();
foo();
baz();

위의 자바스크립트 코드 실행 과정은 다음의 순서로 진행됩니다.

  1. bar() 함수가 호출되고 그 안의 setTimeout() 함수가 호출되어 스택에 쌓입니다.
  2. setTimeout() 함수의 매개변수에 할당된 콜백 함수를 Timer Web API에 전달합니다.
    그리고 Timer Web API 에서는 백그라운드로 500 밀리초를 셈합니다.
  3. 다음 foo() 함수가 호출되고 콘솔창(output)에 "First"가 출력됩니다.
  4. 이때 500밀리 초 대기시간이 만료되면서, 이벤트 루프는 Timer Web API에서 가지고 있던 콜백함수를 Task Queue로 옮깁니다.
  5. 그 다음 baz() 함수가 호출되고 콘솔창에 "Third"가 출력됩니다.
  6. 스택에 있는 모든 메인 자바스크립트 코드가 실행 완료 되어 Call Stack이 비워지게 됩니다.
  7. 이벤트 루프는 Call Stack 이 비어있는 경우 탐지하여 Task Queue에 있는 콜백 함수를 Call Stack으로 옮깁니다.
  8. Call Stack에서 콜백 함수 코드를 실행하게 되고 콘솔창에는 "Second"가 출력됩니다.

이 동작 원리의 핵심은 특정한 작업에 대해 비동기로 멀티 작업을 할 수 있다는 것입니다.
이처럼 이벤트 루프는 이러한 작업들을 별도로 브라우저의 멀티 스레드에게 인가하여 비동기로 처리해주는 핸들러 역할을 해줍니다.

웹브라우저와 Node.js의 Web API 차이

웹 브라우저의 Web APIs와 Node.js APIs들은 구성은 비슷하지만 동작 측면에서 약간 차이가 있다. 웹브라우저의 Web APIs 비동기 작업이 끝나면 스스로 callback queue에 적재하지만, Node.js API들은 이벤트 루프가 직접 옮겨줍니다.
예를 들어 Timer Web API에서 타이머가 모두 지나가면, 자바스크립트 환경이 웹 브라우저냐 Node.js냐 에 따라 차리가 갈립니다.

  • Node.js : Timer API 가 타이머 완료 이벤트를 발생시키고, 이벤트 루프가 이를 감지하여 Task Queue에 콜백 함수를 추가합니다.
  • 웹 브라우저 : Timer API가 스스로 Task Queue에 콜백 함수를 추가합니다.

Task Queue와 Microtask Queue

Callback Queue는 Web API가 수행한 비동기 함수를 넘겨받아 Event Loop가 해당 함수를 Call Stack에 넘겨줄 때까지 비동기 함수들을 쌓아놓는 곳입니다.
위에서 Callback Queue의 종류에는 Task Queue, MicroTask Queue 2가지가 있다고 했습니다.
그 중 자바스크립트 Promise 객체의 콜백이 쌓이는 곳이 바로 MicroTask Queue입니다.
그리고 MicroTask Queue는 그 어떤 곳보다 가장 먼저 우선으로 콜백이 처리됩니다.
( 심지어 브라우저 화면 렌더링하기 전에 처리합니다. )

MicroTask Queue 처리 과정

실제 예시 코드에서 setTimeout이 Promise 객체의 콜백이 동시에 주어졌을때 어떻게 처리되는지 보여드리겠습니다. 우선 코드 내용을 살펴보면 먼저 setTimeout을 통해서 0초동안 대기 하였다가 "Timeout!"을 출력하는 콜백 함수를 실행한다. 그 다음 Promise 객체에 의해 "Promise!"라는 텍스트를 출력하는 then 핸들러의 콜백 함수를 실행합니다. 이 코드를 실행해보면 아래와 같은 애니메이션 동작이 발생합니다.

console.log('Start!');

setTimeout(() => {
	console.log('Timeout!');
}, 0);

Promise.resolve('Promise!').then(res => console.log(res));

console.log('End!');
  1. Call Stack에 console.log('Start1'); 코드 부분이 쌓인 뒤 실행 되어 콘솔창에 "Start!" 가 출력됩니다.


2. setTimeout 코드가 콜 스택에 적재되고 실행되면, 그 안의 콜백 함수가 이벤트 루프에 의해 Web API로 옮겨지고 타이머가 작동하게 됩니다. ( 0초라서 바로 종료 )


3. 타이머가 종료됨에 따라 setTimeout 의 콜백 함수는 MacroTash Queue에 이벤트 루프에 의해 적재되게 됩니다.
4. Promise 코드가 콜스택에 적재되어 실행되고 then 핸들러의 콜백 함수가 이벤트 루프에 의해 MicroTash Queue에 적재되게 됩니다.


5. console.log("End!") 코드가 실행되고 "End!" 텍스트가 콘솔창에 출력됩니다.


6. 모든 메인 스레드의 자바스크립트 코드가 실행이되어 더이상 Call Stack엔 실행할 스택이 없어 비워지게 됩니다.
7. 그러면 이벤트 핸들러가 이를 감지하여, Callback Queue에 남아 있는 콜백 함수들을 빼와 Call Stack에 적재하게 됩니다.
8. 이때 2종류의 Queue 중 MicroTask Queue에 남아있는 콜백이 우선적으로 처리됩니다.
(만일 콜백이 여러개가 있다면 전부 처리됩니다.)


9. MicroTask Queue가 비워지면, 이제 이벤트 루프는 MacroTask Queue에 있는 콜백 함수를 Call Stack에 적재해 실행되게 됩니다.

따라서 최종 코드의 실행 순서는 아래와 같습니다.

console.log('Start!'); // 1

setTimeout(() => {
	console.log('Timeout!'); // 4
}, 0);

Promise.resolve('Promise!').then(res => console.log(res)); // 3

console.log('End!'); // 2

[ 출처 ]

https://inpa.tistory.com/entry/%F0%9F%94%84-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%A3%A8%ED%94%84-%EA%B5%AC%EC%A1%B0-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC

0개의 댓글