🌱 ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [JavaScript] 이벤트 루프(Event loop) 정리
    Front-End/JavaScript 2022. 8. 25. 03:52
    🍀 목차
    글의 목적
    프로세스와 스레드?
    자바스크립트의 런타임 환경
    이벤트 루프
    Task Queue
    Microtask Queue
    Render
    큰 그림으로 이해
    마치며

     

    글의 목적

     자바스크립트 언어 자체는 싱글 스레드(단일 스레드, Call Stack이 하나라 하나의 코드만 실행할 수 있음) 환경이다.

    의문이 든다. 

     

    🤪 "setTimeout()도 그렇고, 여러 비동기, 콜백 함수의 작업은 어떻게 하고 있는 건데?!"

     

     여기서 등장하는 개념이 이번 글에서 다룰 이벤트 루프이다. 자바스크립트의 메인 스레드는 이벤트 루프에 의해 관리된다.

    또 글을 읽기 전, 미리 알아두면 좋을 개념은 자바스크립트의 런타임 환경 멀티 스레드 환경처럼 동작한다는 것이다.

    이 글은 자바스크립트 런타임 환경과 이벤트 루프의 개념을 큰 그림으로 이해해보기 위해 작성됐다.

     

    프로세스와 스레드?

    OS 안에는 여러 개의 프로세스가 존재할 수 있다.

    • Process(프로세스) : 운영체제로부터 자원을 할당받는 작업의 단위. 메모리 위에서 독립적인 단위로 연속적으로 실행되고 있는 프로그램. Process안에는 Code 영역, Stack 영역, Heap 영역, Data 영역이 있다.
      • Code : 프로그램 실행을 위한 코드가 들어간다. 프로그램 시작부터 끝까지 메모리에 남아있다.
      • Stack : 함수의 순서와 함수가 끝난 후의 복귀 번지 등에 대한 정보를 담고 있다. 컴파일(소스코드를 작성하고 기계어로 변환되어 실행 가능한 프로그램이 되는 과정) 시에 크기가 결정된다. 
      • Heap : 데이터, 객체 등을 만들면 동적으로 저장된다. 사용자에 의해 동적으로 할당되고 해제된다. 런타임(컴파일 과정을 마치고 사용자에 의해 실행되는 과정)시에 크기가 결정된다.
      • Data : 전역, static 변수, 상수 등이 저장된다. 

     

    멀티 프로세스 
     여러 프로세스로 각 프로세스가 하나의 작업을 처리한다. 문제가 발생해도 해당 프로세스만 죽어 다른 프로세스에게 영향을 주지 않는다. 프로세스 각각 독립된 메모리 영역을 받아 프로세스 사이에서 변수를 공유하려면 어려운 통신 기법인 IPC(Inter-Process Communication, 프로세스 간 통신)가 사용된다.

     

    Process 안에는 여러 개의 쓰레드가 존재할 수 있다.

    • Thread(스레드) : 한 프로세스 안에서 여러 업무를 배정받는 일꾼의 단위. 자신들이 수행해야 하는 함수의 호출을 기억해야 해서 스레드마다 Stack이 있다. 프로세스의 Code, Data, Heap 영역은 공유한다. 공통적으로 접근/업데이트가 가능하여 프로세스가 효율적으로 일할 수 있게 도와준다. 다만, 그렇기 때문에 업데이트 순서에 문제가 발생하는 경우도 있어 멀티 스레드는 까다롭기도 하다.

     

    멀티 스레드
     하나의 프로그램을 여러 스레드로 구성, 각 스레드는 하나의 작업을 처리한다. 효율적이며 Stack 영역을 제외한 모든 메모리를 공유하여 통신의 부담이 적다. 다만, 주의 깊은 설계를 필요로 하고 자원을 공유하기 때문에 공유 영역의 동기화 문제가 발생할 수 있다.

     

    그런데 왜 JS는 싱글 스레드를 사용하는 건데?
     멀티 스레드는 공유 자원이 존재하여 동기화에 신경을 써줘야 한다. 따라서 프로그래밍 난이도가 높다. JavaScript는 기본적으로 가벼운 경량 프로그래밍 언어를 지향했고, 멀티 스레드 환경에서 발생하는 문제들을 신경 쓰지 않는 싱글 스레드 언어가 되었다.


    자바스크립트의 런타임 환경

    JavaScript Runtime Environment의 간략화.

     자바스크립트 언어 자체는 멀티 스레딩을 할 수 있는 방법이 없다.

    그러나 자바스크립트가 구동되는 런타임 환경인 브라우저, Node.js는 Web API, Task Queue, Event Loop를 활용하여 멀티 스레딩처럼 다양한 동작을 가능하게 해 준다.

     

     웹 애플리케이션이 브라우저에 올라가면 JS 엔진이 소스코드 한 줄 한 줄을 해석하고 분석/실행한다. 

    프로세스 안에서도 Heap과 Stack이 있었던 것처럼 JS 엔진에도 Memory HeapCall Stack이 존재한다.

     

    • Memory Heap(메모리 힙) : 데이터(변수, 객체 등)가 할당되면 자유로운 위치에 저장된다.
    • Call Stack(콜 스택) : 함수 실행 순서에 따라 차곡차곡 쌓이는 스택. Call Stack에서 수행 중인 함수는 끝날 때까지 보장된다(방해받지 않는다).

     

     JS 엔진(크롬의 V8 같은)에는 이벤트 루프나 setTimeout, DOM, HTTP 요청 코드가 존재하는 것이 아니다.
    이벤트 루프와 웹 API는 자바스크립트 런타임 환경(브라우저, Node.js)이 갖고 있다.
    엔진은 메모리 힙콜 스택을 갖고 있고, 다른 것과는 관련이 없는 것. 구분해서 알아두자.

     

    function second(){
    	console.log('hello');
        return;
    }
    
    function first(){
    	second();
        return;
    }
    
    function main(){
    	first();
        return;
    }
    
    main();

     

     위의 코드로 Call Stack의 처리 순서를 이해해 보자.

    ① main() 실행 👉 Call Stack = [main()]

    ② first() 실행 👉Call Stack = [main(), first()]

    ③ second() 실행 👉Call Stack = [main(), first(), second()]

    console.log('hello'); 출력 후, return 만나서 second() 종료 👉Call Stack = [main(), first()]

    ⑤ fisrt() 또한 return 만나서 종료 👉Call Stack = [main()]

    ⑥ main() 또한 return 만나서 종료 👉Call Stack = [ ]

     

     

     

    만약, return이 없었다면.

    Call Stack 초과!

     알고리즘 문제를 풀며 재귀 함수를 구현하다 한 번쯤은 마주친 오류가 있을 것이다. 

    RangeError: Maximum call stack size exceeded

    지정된 Call Stack의 크기를 넘어 함수가 쌓여있다면 발생하는 오류이다. 쌓이기만 하고 return하지 않는(끝나지 않는) 코드는 굉장히 위험한 코드기에(모든 함수는 자신의 역할이 끝나면 적절한 시점에 끝나야 하므로) 고쳐야 한다. 

     

    이벤트 루프

     이벤트 루프(event loop)는 태스크가 들어오길 기다렸다가 들어오면 처리하고, 없으면 쉬는 감시 루프이다. 런타임 환경에서 자바스크립트를 실행하는 스레드가 이벤트 루프이다. 이벤트 루프를 메인 스레드라고 부르는 이유.

     

    알고리즘은 다음과 같다.

     

    ① 처리해야 할 태스크가 있음

    • 먼저 들어온 태스크부터 순차적 처리(Call Stack으로 보냄).

     

    ② 처리해야 할 태스크가 없음

    • 쉬고 있다가 새로운 태스크가 추가되면 로.

     

    Task Queue

     Task Queue(태스크 큐)는 비동기 함수의 콜백 함수가 저장되는 공간이다. 

     

    setTimeout으로 이해해보자.

     함수 안에서 setTimeout을 호출하면, Call Stack에서 지워지고 웹 API는 타이머를 실행한다. setTimeout에 지정된 시간이 끝나면 웹 API는 사용자가 등록한 콜백 자체를 Task Queue 집어넣는다. 

     Task Queue에 있는 timeout callback은 언제 실행될까? 이 글의 주제, 이벤트 루프가 담당한다.

    이벤트 루프는 Call Stack이 비어있고 Task Queue에 대기하는 콜백 함수 등이 있다면 Call Stack으로 이동시킨다.

     

    계속 Call Stack과 Task Queue를 감시하다가, Call Stack이 비워질 때를 기다린 뒤, 텅텅 비어진 Call Stack에 Task Queue에서 대기하는 콜백을 순서대로 하나씩 Call Stack으로 데려온다. 

     

    그런데, 자바스크립트 런타임 환경에는 Task Queue만 존재하는 것이 아니다.

     

    Microtask Queue

      Task Queue는 웹 API에서 우리가 등록한 콜백 함수가 발생했을 때 넣어진다. 이 Task Queue에 넘겨받는 함수들에는 setTimeout(), setInterval() 등이 있다.

     

     JavaScript에는 비동기 작업에 빼먹을 수 없는 중요한 객체가 있다. Promise.

    그럼 Promise에 등록된 콜백은 어디에서 처리되는 걸까? Microtask Queue이다. 기존 Task Queue와는 다른 다양한 방식으로 타이밍을 제어하기 위해 도입되었다. 우선순위가 Task Queue(비교를 위해 Macrotask Queue라고도 부르기도 한다.)보다 높다. 

     

    Microtask Queue는 Promise가 수행된 후(resolve)의 then에 등록된 콜백 함수가 들어간다.

    그 외에도 async/await, Object.observe, MutationObserver 같은 비동기 콜백들이 들어간다.

     

    그리고 남은 주요 Queue가 하나 더 있다.

     

    Render

     브라우저에서 요소들을 움직이거나 애니메이션을 추가하면 해당 업데이트 사항을 반영해야 하고, 그 일을 하는 것이 Render Queue다. Render Queue의 순서는 다음과 같다. DOM Tree와 CSSOM Tree로 Render Tree 생성 -> 요소들의 위치를 계산하는 Layout -> 요소들을 실제 그리는 Paint 과정(브라우저의 렌더링 과정 참고). 그런데 Render Tree전에 웹 API 중 하나인 Request Animation Frame API를 통해 콜백을 등록해 놓았을 경우, 해당 콜백들이 먼저 순서대로 실행된다.

     

    +) Request Animation Frame도 Queue이며, 우선순위는 후에 기술한다.

     

    Request Animation Frame API
    다음 브라우저 업데이트 전에 내 콜백을 먼저 실행해줘!
    requestAnimationFrame(()=>{
    	document.body.style.backgroundColor = "red";
        });​

     

    브라우저는 어떻게 Task Queue, Microtask Queue, Render 안의 Request Animation Frame 안의 큐 렌더 순서를 꼬이지 않고 잘 실행할 수 있는 것일까?

     

    큰 그림으로 이해

     

     다시 한번, 이벤트 루프는 감시 루프이다. 이벤트 루프는 빠른 속도로 계속 감시를 돈다.

     

     이벤트 루프는 돌다가 Call Stack에 함수가 존재한다면 그 함수가 끝날 때까지 머물러 있는다. (무한 재귀가 있을 경우 브라우저가 멈추는 이유도 이벤트 루프가 Call Stack의 해당 함수에서 멈춰서 계속 머물러 있기 때문이다.)

     

     Call Stack이 비었다면? Render 쪽으로 갈 수도, 안 갈 수도 있다.

     브라우저는 사용자가 불편함을 느끼지 않는 60 frames per second(1초당 60개의 프레임)을 위해 노력한다. 60 fps(16.7ms) 동안 업데이트가 일어나는데 이벤트 루프는 한 바퀴(여러 큐를 감시하는 한 사이클)를 도는데 1ms도 걸리지 않는다. 

     이벤트 루프 입장에서는 매 1ms마다 Render를 업데이트할 필요가 없기 때문에 일정 주기로 Render에 도착해 업데이트를 해주고 다른 큐로 가고, 몇 바퀴 돌다가 다시 Render로 가서 업데이트를 할 수 있는 것이다.

     

      Microtask Queue는 조금 다르다. 이벤트 루프는 Microtask Queue에 Promise 콜백 등이 등록된 상태라면 콜백들을 하나하나 씩 Call Stack으로 가져가고 수행한다. 언제까지? Microtask Queue가 텅텅 빌 때까지.

    만약, Promise then 콜백을 실행 중인데 또 Microtask Queue에 콜백이 들어오면 Promise then 콜백이 끝나고 새롭게 들어온 콜백을 수행하게 되는 것이다. 

     

     Task Queue안의 콜백들은 Microtask Queue가 다 비어있다면 그제야 Call Stack으로 옮겨진다. 그런데, 하나의 아이템만 옮겨진 후 이벤트 루프는 다시 사이클을 돈다. 하나의 콜백이 끝나면 다시 이벤트 루프는 Render(갈 수도 안 갈 수도), Microtask Queue를 보고 비어있다면 다시 Task Queue의 콜백 하나를 Call Stack으로 가져온다.

     

    우선순위
    Microtask Queue -> (Render - Animation Frame) -> Task Queue
    * Animation Frame은 60fps에 의해 때에 따라 Task Queue에게 우선순위를 양보할 수도 있다. 

     

    해당 순회 순서는 브라우저마다 다를 수 있다.

     

     

    <script>
    	const button = document.querySelector("button");
        button.addEventListener("click", () => {
        	//addEventListener로 등록된 callback.
        	const element = document.createElement("h1");
            document.body.appendChild(element);
            element.style.color = "red";
            element.innerText = "hello";
        });
    </script>

     

     위의 코드는 문제없이 잘 수행된다. document.body.appendChild(element)가 스타일 지정 위에 선언됐는데도 말이다. 

    해당 코드는 addEventListener이라는 웹 API를 통해 콜백을 등록시켰다. Task Queue로 이동, 이벤트 루프에 의해 Call Stack이 비어져 있다면 옮겨져 수행될 것이다. 이벤트 루프는 해당 콜백 내용을 다 수행한 다음, Render로 가 렌더링 절차를 수행할 것이다. 콜백의 내용이 다 수행된 다음이기에 해당 element는 스타일 등등이 다 수행되었다. 

     

     

     

    이렇게 싱글 스레드 언어 JavaScript가 다양한 작업과 비동기 처리를 할 수 있는 이유를 알아보았다.

     

     마지막으로 Philip Roberts의 이벤트 루프 발표에서 사용된 사이트 Loupe에서 JavaScript의 런타임 환경을 이해해보자.

    사이트에 접속하여 Save+Run 버튼을 눌러 과정을 확인한 후 Click me! 를 여러 번 눌러보거나 직접 코드를 작성해 이해해보자. 시각적으로 JavaScript의 런타임 환경을 이해할 수 있다. 

     

    톱니 클릭 -> Simulate Renders 체크 시 Render Queue도 추가해 확인할 수 있다.

     위 사진처럼 톱니 아이콘을 클릭해 Render Queue도 추가할 수 있다!

     

     

     

    마치며

     

     해당 게시글은 Dream Coding 강의 브라우저 101 이벤트 루프 파트를 중점으로 추가 학습 내용을 함께 정리한 게시글입니다.

    중요한 개념이라 열심히 정리해봤지만 이해가 안 가실 수도 있을 것 같습니다.

    그런 분들에게는 Philip Roberts의 이벤트 루프 발표 영상이나 Dream Coding 브라우저 101 강의를 추천드립니다.

     

     

     

     

    참고자료

    https://ko.javascript.info/event-loop

    https://academy.dream-coding.com/courses/browser101 (Event loop 파트)

    https://youtu.be/8aGhZQkoFbQ

    https://iamsjy17.github.io/javascript/2019/07/20/how-to-works-js.html

    댓글

🍀 Y0ungZ dev blog.
스크롤 버튼