관리 메뉴

Jerry

이벤트 루프란? (in Javascript) 본문

Front/JavaScript

이벤트 루프란? (in Javascript)

juicyjerry 2021. 4. 16. 17:09
반응형

이번엔 이벤트 루프(event loop)를 정리해 보려고 한다.

 

이벤트 루프를 모르면 자바스크립트 알고 있는 것이 아니라고 하는 이야기를 들었는데 그만큼 이벤트 루프가 자바스크립트에서 자치하는 역할이 크다는 것을 의미하는 것 같다. 이 글을 정리하면서 여러 레퍼런스와 유튜브 영상을 참조하였는데 알고 있는 선에서 최대한 이벤트 루프에 대해서 이야기해보겠다. 아마 이벤트 루프에 대한 개념을 한 번만 보고 이해하기는 힘들 가능성이 높다. 하지만, 여러 번 보면 볼수록 익숙해지고 익숙해지면 더 이해하기가 수월해지지 않을까 생각해본다. 필자 역시 그랬다.ㅎ

 

 

출처: https://suddenlypain.tistory.com/notice/110

 

먼저, MDN에서 말하는 이벤트 루프를 뭐라고 이야기할까?

JavaScript has a concurrency model based on an event loop, which is responsible for executing the code, collecting and processing events, and executing queued sub-tasks. This model is quite different from models in other languages like C and Java.

 

 

자바스크립트는 이벤트 루프에 기반한 동시성 모델이라고 한다. 이벤트 루프는 코드 실행과 collecting(이벤트 수집) and processing events(이벤트 처리), queued sub-tasks(큐에 놓인 하위 테스크) 실행을 담당한다. 이 모델은 C와 자바 같은 다른 언어와 상당히 다르다.

 

 

두번째로, 제로초 블로그에서는 이벤트 루프를 뭐라고 이야기할까?

이벤트 루프는 실행 콘텍스트와 함께 필수적으로 알고 있어야 하는 개념입니다. 자바스크립트와 노드에서 사용되는데요. 자바스크립트는 보통 싱글 쓰레드라고 불리는데, 바로 메인 스레드인 이벤트 루프가 싱글 스레드이기 때문입니다.

 

 

세 번째로, javascript.info에서는 이벤트 루프를 뭐라고 이야기할까?

이벤트 루프(event loop) 정의는 아주 간단합니다. 이벤트 루프는 태스크가 들어오길 기다렸다가 태스크가 들어오면 이를 처리하고, 처리할 태스크가 없는 경우엔 잠드는, 끊임없이 돌아가는 자바스크립트 내 루프입니다(task는 '작업’이라고 번역할 수 있는데, 매크로·마이크로 태스크 등의 용어와 일치시키기 위해 '태스크’라고 음차 번역하였습니다 – 옮긴이).

 

 

마지막으로, NHN Cloud 기술 블로그에서 이벤트 루프를 뭐라고 이야기할까?

자바스크립트의 큰 특징 중 하나는 '단일 스레드' 기반의 언어라는 점이다. 스레드가 하나라는 말은 곧, 동시에 하나의 작업만을 처리할 수 있다는 말이다. 하지만 실제로 자바스크립트가 사용되는 환경을 생각해보면 많은 작업이 동시에 처리되고 있는 걸 볼 수 있다. 예를 들면, 웹브라우저는 애니메이션 효과를 보여주면서 마우스 입력을 받아서 처리하고, Node.js기반의 웹서버에서는 동시에 여러 개의 HTTP 요청을 처리하기도 한다. 어떻게 스레드가 하나인데 이런 일이 가능할까? 질문을 바꿔보면 '자바스크립트는 어떻게 동시성(Concurrency)을 지원하는 걸까'?

이때 등장하는 개념이 바로 '이벤트 루프'이다. Node.js를 소개할 때 '이벤트 루프 기반의 비동기 방식으로 Non-Blocking IO를 지원하고..' 와 같은 문구를 본 적이 있을 것이다. 즉, 자바스크립트는 이벤트 루프를 이용해서 비동기 방식으로 동시성을 지원한다.

 

각각의 레퍼런스가 이야기하는 이벤트 루프에 대한 설명이 다양하다. 

중요하다고 생각하는 키워드를 뽑아보면, (1) 동시성 모델 (2) 메인 스레드인 이벤트 루프가 싱글 스레드 (3) 루프 (4) 비동기 방식으로 Non-Blocking IO를 지원, 정확히 키워드는 아니지만 이렇게 4가지가 키워드로 뽑아볼 수 있을 것 같다.

 

 

키워드 (3)를 보면 Non-Blocking IO를 지원한다고 쓰여 있다. Non-Blocking이 있으면 Blocking이 있을 것이다. Blocking 하게 tasks를 처리하게 되면 먼저 실행되는 task가 끝날 때까지 기다려야 한다. 개발자 도구를 켜서 alert와 console.log를 찍어보면 무슨 느낌인지 알 것이다, 이 개념은 동기 / 비동기 개념과 유사하며 이런 성질이 있기 때문에 싱글 스레드이지만 여러 tasks를 동시에 처리할 수 있어 동시성 모델이라고도 한다. 

 

 

그렇다고 해서, 싱글 쓰레드가 자체적으로 모든 tasks를 처리하는 것은 아니고 실제 자바스크립트가 실행되는 환경인  Node.js와 Chrome에서는 주로 여러 개의 스레드가 사용되는데, 이러한 여러 스레드들이 싱글 스레드를 사용하는 자바스크립트 엔진과 상호 작용을 통해 사용하는 장치가 이벤트 루프라고 합니다. 

 

 

위에서 이야기하는 비동기 요청이나 동시성 처리는 자바스크립트 엔진을 구동하는 환경인 브라우저나 Node.js가 담당한다.

 

 

그렇다면 브라우저 환경은 어떻게 구성되어 있을까?

출처: (좌) NHN Cloud (중) 제로초 (우) MDN

 

Heap과 stack은 프로그램이 운영체제로부터 할당받는 대표적인 메모리 공간 중 하나로, Heap은 사용자가 동적으로 메모리를 관리한다고 합니다. stack은 실행 컨텍스트가 적재되는 스택이다.

 

우리가 자주 사용하는 setTimeout이나 AJAX 같은 비동기 호출, 이벤트들을 웹에서 작업을 수행할 수 있도록하는  Web APIs(혹은 백그라운드(node)) 영역이 따로 정의되어 있으며 이벤트 루프와 태스크 큐도 자바스크립트 엔진 외부에 구현이 되어 있다. 

 

(Task) Queue는 콜백 함수들이 대기하는 큐(FIFO) 형태의 배열이다. Web API를 사용하여 task가 생성되어 이동하여 적재되는 곳이 Queue이다. 

 

Event Loop는 stack이 비웠을 경우, queue에서 대기하고 있는 task를 stack으로 전달한다.

 

그렇다면, 이벤트 루프는 어떻게 동작할까?

1. 함수 호출부터 콜 스택까지

MDN에서 자바스크립트 함수는 Run-to-completion 방식으로 실행되며 각 함수 동작이 완료되야 다음 작업이 동작할 수 있습니다. 

"Run-to-completion", Each message is processed completely before any other message is processed.

 

 

자바스크립트 엔진에 있는 호출 스택(콜 스택)은 하나의 호출 스택을 사용하며, 스택에 쌓여 있는 함수가 실행을 마치고 제거되어야 다른 함수가 들어올 수 있다. 아래 예시를 보자. 

 

function delay() {
    for (var i = 0; i < 100000; i++);
}
function foo() {
    delay();
    bar();
    console.log('foo!'); // (3)
}
function bar() {
    delay();
    console.log('bar!'); // (2)
}
function baz() {
    console.log('baz!'); // (4)
}

setTimeout(baz, 10); // (1)
foo();

 

 

 

'bar!' -> 'foo!' -> 'baz!'의 순서로 찍히게 된다. anonymous는 전역 환경에서 실행되는 코드를 코드 블록으로 생각하자! 

코드 첫째줄이 실행될 때 추가 되며, 마지막 라인 실행 후 제거된다.

 

 

 

출처: https://meetup.toast.com/posts/89

 

 

 

 

2. 콜스택에서 Web APIs 처리와 task queue, 이벤트 루프까지 동작을 보자! (전체 동작)

function run() {
  console.log('동작');
}
console.log('시작');
setTimeout(run, 0);
console.log('끝');

 

 

출처: https://www.zerocho.com/category/JavaScript/post/597f34bbb428530018e8e6e2 

 

1. 전역 컨텍스트 main()이 콜 스택에 들어갑니다. (anonymous라고도 함)

 

2. console.log('시작'); 이 콜 스택으로 호출되어 콘솔에서 실행 후 제거됩니다.

 

3. setTimeout(run, 0); 이 콜스택으로 호출되어서 실행할 때, WebAPIs(백그라운드)로 이동합니다. 

 

4. 콜 스택이 비워지자마자, console.log('끝'); 이 콜스택으로 호출되어 콘솔에서 실행 후 제거됩니다.

 

 

보기 불편 하실까봐, 똑같은 예시코드를 넣었습니다.

function run() {
  console.log('동작');
}
console.log('시작');
setTimeout(run, 0);
console.log('끝');

 

5. 3번에서처럼 이동 후 0초를 기다립니다. 이 때 0초든 3초든 어떤 시간이든 이 시간은 대기 시간보다는 최소 실행 대기(지연) 시간이라고 생각합니다. (mdn: 지연(delay)은 보장된 시간이 아니라 요청을 처리하기 위해 필요한 최소의 시간이기 때문입니다.)

 

6. 0초 후, 태스크 큐로 이동합니다.

 

7. 이동 후 대기 중인 큐(함수)는 이벤트 루프가 스택이 비워졌다고 판단되면(main 제거) 콜 스택으로 전달합니다. 이벤트 루프는 태스크 큐에 새로운 함수가 들어올 때까지 대기합니다.

 

8. 이동한 함수가 실행이 됩니다.

 

9. 실행이 종료됩니다.

 

 


매크로 태스크 큐(macrotask queue)와 마이크로 태스크 큐(microtask queue)

 

여기까지 이벤트 루프의 동작까지 세밀하게 살펴보았다. 

여기서 추가로, 태스크 (큐)에 관해서 조금 더 알아보려고 한다.

중요한 개념이라고 하니 조금만 더 집중해보자!!

 

태스크 큐에서 매크로 태스크 큐(macrotask queue)와 마이크로 태스크 큐(microtask queue)가 존재한다.

 

매크로 태스크 큐는 자바스크립트 엔진을 활성화하는 태스크로 다음과 같다.

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

이렇게 나열하니깐 처음보는 것 같은데 지금까지 다뤘던 task라고 생각하면 된다. 이 task들은 순차적으로 처리된다.

 

 

마이크로 태스크 큐 코드를 사용해서만 만들 수 있는데, 주로 프라미스를 사용해 만듭니다. 프라미스와 함께 쓰이는. then/catch/finally 핸들러가 마이크로 태스크가 되죠. 여기에 더하여 마이크로태스크는 프라미스를 핸들링하는 또 다른 형태의 문법인 await를 사용해 만들어지기도 합니다. 

 

마이크로태스크 전체가 처리되는 동안에는 UI 변화나 네트워크 이벤트 핸들링이 일어나지 않습니다. 렌더링이나 네트워크 요청 등의 작업들은 마이크로태스크 전부가 처리되고 난 직후 처리됩니다.

이 마이크로 태스크 큐는 특이하게도 매크로 태스크 큐 보다 뒤에 대기하고 있어도 우선적으로 처리가 된다. 왜냐하면, "마이크로 태스크 간 동일한 애플리케이션 환경 보장 때문입니다." 이라고 한다. 

 

 

 

이상으로 이벤트 루프에 대한 전반적이 내용을 나름대로 열정을 가지고 정리해보았습니다.
부족한 점이 있다면 다른 레퍼런스를 참고해주시고 지적할 부분이 있다면 편안하게 의견을 주시면 상생하는 블로그가 될 것입니다. 감사합니다 :))

 

 

 

출처: MDN | mzl.la/3uY6VMS

출처: ZeroCho | bit.ly/3sugwd2

출처: javascript-info | bit.ly/3e5uuwA

출처: 삼바의 성장 블로그 | bit.ly/3x2gjBh

출처: NHN Cloud Blog | bit.ly/3emFRR9

 

반응형