본문 바로가기

Frontend/Javascript

Event Loop란? (+ JS 엔진,비동기함수,setTimeout)

Nest.js 책을 공부하다가 '이벤트 루프'라는 단어를 접하게 되었다. 

사실 이벤트 루프의 경우에도 기존에는 '이벤트가 loop 반복 형태로 도는구나' 정도로만 생각했지, 나의 큰 착각이었다. 

이번 기회에 Event Loop에 대해 다시 정리하고자 한다. 

 

JS 엔진 구성 (참고로, JS엔진에서 제일 유명한 엔진은 Google의 v8이다) 

출처 : 캡틴판교

https://v8.dev/

 

일단 Nest.js의 근원이 되는 Javascript부터 거슬러 올라가야 한다. 

Javascript의 엔진은 2가지 영역으로 구성되어 있는데 바로 Memory HeapCall stack 형태로 구성되어 있다. 

 

Memory Heap - 메모리 할당 

Call Stack - 코드 실행 시에 하나씩 Stack 형태로 쌓이는 장소. stack의 경우 LIFO(Last In First Out) 구조를 띄고 있다. 


비동기 함수 - Node를 활용한 서버 프로그래밍 방식에서 동기/비동기 함수 개념이 중요하다. 

Javascript는 비동기 함수이다. 비동기 함수란 무엇일까? 

 

일단 동기함수부터 알아보자. 

 

동기(synchronous)방식

 

요청을 보내고 응답을 받아야지만 다음 동작이 이루어지는 방식 

예시 코드 From Node.js

function func1() { 
    console.log('첫번째 함수가 실행되었습니다');
}

function func2() { 
    console.log('두번째 함수가 실행되었습니다');
}

function func3() { 
    console.log('세번째 함수가 실행되었습니다');
}

func1();
func2();
func3();

출력 결과는 순서대로 첫번째 함수 / 두번째 함수 / 세번째 함수 실행 순으로 출력된다. 

개인적으로는 데이터의 양이 엄청 많아지게 된다면, 만약 내가 100000번째 함수라면 첫번째부터 내 순번까지 오기까지는 정말 오랜 시간이 걸릴 것으로 예상이 된다. 


비동기(Asynchronous)방식은 왜 필요할까? 

if, 서버로부터 데이터를 뿌려주는 앱을 만든다고 치자. 이렇게 될 경우 서버로부터 데이터를 받아와서 맨 처음부터 앱 화면에 데이터를 뿌려줘야 하므로 맨 처음에는 서버로부터 데이터를 받아오는 코드를 실행해야 할 것이다. 

 

하지만 이를 비동기가 아닌 위에서 살펴본 동기 방식으로 처리하게 된다면 서버에서 데이터를 받아오기까지의 어마어마한 시간을 견뎌야 한다. 또한 데이터의 양이 기하급수적으로 많아질수록 서버에서 데이터를 가져오는 양&시간이 늘어나 앱의 실행속도가 현저히 느려지게 될 것이다. 이를 방지하기 위해 비동기 방식이 존재하는 것이다. 

 

비동기에 대한 예시는 Web API인 AJAXsetTimeout()이 주로 쓰인다고 한다. 

function func1() { 
    console.log('첫번째 함수가 실행되었습니다');
    func2();
}

function func2() { 
    setTimeout(function(){
        console.log('두번째 함수가 실행되었습니다');
    },0);
        func3();
  
}

function func3() { 
    console.log('세번째 함수가 실행되었습니다');
}

func1(); /* 첫번째 > 세번째 > 두번째 함수 실행순 */ 
func2(); /* 세번째 > 두번째 함수 실행순 */ 
func3(); /* 세번째 함수 실행*/

setTimeout()은 어떤 코드를 바로 실행(X) 일정시간 기다린 후 실행(O)

<-> setInterval은 일정 간격을 두고 함수를 실행하는 방법이다. 

 

위의 코드에서는 setTimeout()의 인자값으로 0이 들어왔는데, 모던 자바스크립트 튜토리얼을 확인해보니  

setTimeout(func,0)이나 setTimeout(func)을 사용하면 setTimeout의 대기 시간을 0으로 설정할 수 있다. 
setTimeout()의 대기 시간을 위와 같이 0으로 설정하게 된다면, func를 '가능한 한 빨리' 실행할 수 있다. 
이때 스케줄러는 현재 실행중인 스크립트의 처리가 종료된 이후에 스케줄링한 함수를 실행한다.

예시 코드 

setTimeout(() => alert("World")); 
alert("Hello");

위의 예시에서는 alert창으로 Hello 다음에 World가 출력된다.

setTimeout((() => ~));는 '0밀리초 후에 함수 호출하기'라는 태스크를 계획표에 선언해주는 역할이다. 

위의 코드에서는 alert 함수가 종료되고 나서야 계획표에 적혀있던 World(0밀리초 후에 함수 호출하기)를 확인하기 때문에 Hello World 순서로 나타나는 것이다. 

 

그래서 위의 func2() 함수의 결과가 첫번째 함수 > 세번째 함수 > 두번째 함수로 출력이 된 이유는 

일단

1. func1()에서 console.log 결과 출력 

2. func1()에서 func2() 함수가 호출되었으니 func2() 함수로 이동 

3. func2() 함수로 이동했더니 setTimeout으로 '두번째 함수 출력은 미리 계획표에 넣기만 해달라고 했네? + 0밀리초니까 가능한 한 빨리 처리해달라'를 확인만 하고, setTimeout의 요청은 확인했으니 현재 func2()내에서 실행중인 func3()을 실행시키고, 실행이 끝난 뒤에 아까 계획표에 있었던 func2() 함수는 처리해줘!를 처리하기 때문에 1>3>2번째 함수 순서로 출력이 되었다. 

4. func3()은 그냥 console.log만 찍으니 pass

Callback Queue

위에서 비동기 처리는 DOM, Ajax, setTimeout(Timeout)과 같은 Web API로 한다고 했는데, 이 비동기 함수를 보관해주는 것이 

Callback Queue이다. Callback Queue는 FIFO(First In First Out)구조를 띄고 있다. 

 

JS 엔진의 한 부분인 Call stack이 다 비워지면 Callback Queue에 존재하는 함수들은 Call Stack으로 옮겨지게 되어 비동기 처리가 되는 것이다. 

 


그렇다면 비동기 처리는 어떻게 되는 것일까? 

JS는 싱글스레드(1번에 1작업)을 하는데, JS 기반인 웹 브라우저는 한 페이지에 한 작업만 처리하는게 아니라 여러 페이지를 띄워놓고 있어도 여러 작업이 수행되는 것처럼 보인다. 

 

이는 바로 Event Loop 덕분이다. Event Loop가 JS에 동시성을 지원하기 때문에 여러 브라우저를 띄워놓고 여러 작업이 가능한 것이다. (그래도 JS의 싱글 스레드 특징은 변하지 않는다) 

 

가령 node main.js를 실행한다고 해도, Event Loop가 생성되고 main.js가 실행되는 것이다. 


참고 

https://blog.metafor.kr/164