JavaScript

이벤트 루프 (Event Loop)

ㅇㄱ9 2022. 2. 8. 15:01
728x90

이벤트 루프란 무엇인가?라는 아래 영상을 보고 정리한 글입니다. 


먼저 자바스크립트 런타임을 단순화하여 도식화하면 아래와 같이 표현할 수 있다. 

메모리 할당이 일어나는 콜 스택으로 구성되어 있다.

하지만 v8엔진의 코드를 보게 되면 setTimeout이나 DOM, HHTP요청을 관리하는 코드들을 찾아볼 수 없는데 비동기 작업이 어떻게 일어나는 것일까??

 

위의 그림을 보면 v8런타임과 브라우저가 제공하는 웹 API가 있다.

브라우저는 DOM, AJAX, setTimeout등과 함께 event loopcallback queue를 가지고 있다.

 JS는 싱글 스레드 프로그래밍 언어이다. 이는 싱글 스레드 런타임을 가지고 있다는 말이고, 결국 한 번에 하나의 싱글 콜 스택만을 가지고 있다는 말이다. 즉, 하나의 프로그램은 동시에 하나의 코드만 실행할 수 있다. 

 

Call Stack

: 콜 스택은 데이터 구조로 실행되는 순서를 기억하고 있다. 

함수를 실행하면 스택에 해당하는 함수를 넣어주고 리턴이 일어날 경우 (혹은 함수 종료 시) 스택 가장 위쪽에서 해당 함수를 꺼낸다.

[출처]https://dev.to/ejjraifihamza/javascript-call-stack-4e1c

stack overflow
아래와 같이 스스로를 호출하는 함수를 계속 반복하면 chrome의 경우 RangeError : Maximum call stack size exceeded 에러가 발생한다.

[출처]https://dev.to/ejjraifihamza/javascript-call-stack-4e1c

Blocking

: 블로킹의 정확한 정의는 존재하지 않지만 보통 느린 동작이 스택에 남아있는 것을 지칭한다. 

예를 들면 네트워크 요청이나 이미지 프로세싱 혹은 while문안에서 수십억 번 불리는 console.log와 같은..

콜 스택에 있는 모든 동작들이 사라질 때까지 블로킹되면 렌더링이나 다른 요청을 처리하지 못한다.

이를 해결 화기 위해 비동기 콜백을 사용한다.

브라우저나 노드에는 블로킹 함수가 거의 없고 대부분 비동기로 만들어져 있다. 이는 어떤 코드를 실행하면 결국 콜백을 받고 이걸 나중에 실행한다는 말이다.

 

아래와 같은 코드를 실행하게 되면 First -> Third -> Second순으로 출력되게 된다. 

동작 과정을 보면 아래와 같다. 

[출처]https://aicompany2.blogspot.com/2020/10/blog-post.html

 

자바스크립트 런타임은 한 번에 하나의 일만 처리할 수 있다. setTimeout도 마찬가지이지만 이걸 동시에 할 수 있는 이유는 브라우저가 런타임 그 이상이기 때문이다.

브라우저는 WebAPI를 제공하는 데 이는 자바스크립트에서 호출할 수 있는 스레드를 효과적으로 지원해준다. 여기서 동시성이 발생하게 된다. 

 

setTimeout은 브라우저에서 제공하는 api호 v8 소스코드에는 존재하지 않는다.  즉, 자바스크립트가 실행되는 런타임 환경에 존재하는 별도의 api이다. 

 

setTimeout은 브라우저가 타이머를 실행시키고 카운트 다운을 하도록 한다. 이 자체가 setTimeout 호출은 완료되었다는 의미이고 스택에서 함수를 지운다. 

 

Web api는 스택에 함수를 집어넣는 등의 코드 수정을 직접적으로 할 수 없다. 이제 테스트 큐와 콜백 큐가 동작할 차례다

모든 Web api는 작동이 완료되면 콜백을 테스크 큐에 밀어 넣는다. 

 

이벤트 루프의 주요 업무는 콜 스택과 테스크 큐를 주시하는 것이다. 스택이 비어있다면 큐의 첫 번째 콜백을 스택에 쌓아 효과적으로 실행할 수 있게 해주는 것이다. 

 

위의 예를 다시 보자면 setTimeout에 설정된 0.5초가 경과하면  테스크 큐에 콜백 함수인 console.log("Second")가 들어가게 되고 baz함수까지 실행된 이후 스택이 비어있게 되면 console.log("Second")를 스택에 넣어 실행되게 하는 것이다. 

 

아래와 같이 setTimeout의 delay시간을 0초로 지정하는 경우가 있다. 

-> 크롬 101부터는 delay 시간을 0초로 지정하는 것을 지원하지 않기에 1초로 바꾸어주어야 한다. 
참고: https://chromestatus.com/feature/4889002157015040

console.log('Hi');

setTimeout (function cb() {
	console.log('there');
},0);

console.log('JSConfEU')

일반적으로 이것은 스택이 비어있을 때까지 콜백으로 들어간 함수의 실행을 기다리게 하기 위한 코드로 쓰인다. 

 

다시 돌아와 모든 이런 종류의 Web API는 동일한 방식으로 동작한다. AjaxRequest는 URL로 호출 시 콜백을 함께 실행하게 된다. 

$.get('url', function cb(data) {
	console.log(data)
});

AJAX요청은 자바스크립트 런타임이 아니라 브라우저 Web API에서 실행되고, XHR Web API가 실행되는 동안 다른 코드들은 정상적으로 실행할 수 있다. 이후, XHR실행이 완료되면 콜백은 큐에 쌓이게 되고 이벤트 루프에 의해 실행된다. 

 

이 과정이 비동기 함수가 호출되는 방식이다. 

 

비동기 함수를 여러 개 쓸 경우 지정한 딜레이 시간이 경과해도 함수가 실행되지 않는 경우가 있는데 이는 딜레이 시간은 딜레이 되는 최소한의 시간을 지정하는 것이기 때문이다. 정해진 시간이 경과돼도 차례가 되지 않으면 (= 스택이 비어있지 않으면) 콜백 함수는 실행되지 않는다. 

 

콜백 함수란?

다른 함수가 부르는 함수 혹은 앞으로 큐에 쌓일 비동기식 콜백으로 설명될 수 있다. 아래 예시를 보자. 

//Synchronous
[1,2,3,4].forEach(function (i){
	console.log(i);
});

//Asynchronous
function asyncForEach(array, cb){
	array.forEach(function(){
    	setTimeout(cb,0);
    })
}

asyncForEach([1,2,3,4], function (i) {
	conosle.log(i);
})

첫 번째 forEach함수의 경우 함수를 실행시키기는 하지만 비동기적으로 실행하지는 않는다. 즉, 자신의 자체적 스택에서 함수를 실행시킨다. 

 

한편 asyncForEach를 선언해서 배열과 콜백을 받아 각 요소에서 setTimeout을 0으로 실행하는 것도 가능하다. 

 

브라우저 렌더링과 콜 스택

브라우저는 기본적으로 화면을 1초에 60 프레임을 repaint 하는 게 이상적이다. 하지만 브라우저는 우리가 자바스크립트로 하는 무언가에 의해 제약을 받는다.

 

렌더도 하나의 콜백처럼 행동하기 때문에 스택에 코드가 있으면 렌더링을 하지 못한다. 즉, 스택이 비워질 때까지 기다려야 하는데 콜백과 다른 점이 있다면 렌더는 콜백에 비해 높은 우선순위를 가지게 된다. 매 16ms마다 큐에 렌더가 들어가고 스택이 비워진 후에야 렌더링을 하게 된다. 

 

동기적으로 함수를 실행시키는 경우 콜백이 실행되는 속도가 느리다면 렌더는 그 콜백이 끝날 때까지 계속 기다려야 하지만 비동기 콜백의 경우 큐에 쌓은 후 실행되기 때문에 중간중간 렌더가 끼어들 수 있는 기회를 줄 수 있다. 

 

이벤트 루프를 막지 마라
= 스택에 필요 없는 느린 코드를 쌓아서 브라우저가 할 일을 못하게 만들지 말아라
= 유동적인 UI를 만들어라

 

 

---참고 사이트 

https://aicompany2.blogspot.com/2020/10/blog-post.html

https://www.youtube.com/watch?v=8zKuNo4ay8E 

https://dev.to/ejjraifihamza/javascript-call-stack-4e1c

 

728x90
반응형