자바스크립트(9) 이벤트루프는 무엇인가? (ft. 비동기, 동기, 렌더링)

이종호·2022년 7월 14일
0

JavaScript

목록 보기
10/11
post-thumbnail

영상링크

자바스크립트 런타임

자바스크립트는 실제로 어떻게 동작할까? 발표자는 크롬의 런타임에 대해서 정확히 어떤 것을 의미하는지 잘 몰랐다고 한다. 그 후 여러 공부를 통해 내린 결론은 결국
자바스크립트는 싱글 스레드 논 블록킹 비동기 동적 언어 이다... 결국 사전적의미로 밖에 찾을수가 없었고 더 심층적으로 자바스크립트 엔진인 V8을 찾아봤는데, 콜스택과 힙을 갖고있다고 되어있다.

V8 엔진

메모리 할당이 일어나는 힙, 콜 스택이 있다.
하지만 그림과 같이 V8 프로젝트를 들여다보면 setTimeout, DOM, HTTP 요청을 관리하는 코드는 찾아볼 수 없다.
우리가 자바스크립트를 생각하면 비동기를 생각하는데, 엔진에서는 없는게 신기하다.

V8, 브라우저

그러면 위에 찾아본 setTimeout과 같은 api는 어디에 있었을까? 브라우저가 제공한다고 한다.
또한 eventloop, callback queue등을 갖고있었다!

자바스크립트란?

앞서 설명한 eventloop, callback queue, web api, 힙, 스택 서로의 체인을 이해하려면

먼저, 자바스크립트는 싱글 스레드 런타임, 싱글 콜 스택 의 특징을 갖고있고 결국 하나의 프로그램은 동시에 하나의 코드만 실행할수 있다는 뜻이였다.

콜스택

이를 이해하기 위해 예시코드가 있다.

function multiply(a,b) {
	return a * b;
}

function square(n) {
	return multiply(n, n);
}

function printSquare(n) {
	var squared = square(n);
    console.log(squared);
}

printSquare(4);

콜스택은 데이터 구조로 실행되는 순서를 기억하고 있다.
함수를 실행하려면 스택에 해당하는 함수를 집어넣게 되고, 함수에서 리턴이 일어나면 스택의 가장 위쪽에서 해당 함수를 꺼낸다.

다시 예시코드로 돌아가서, 이를 실행하면 가장 먼저 코드 그 자체를 뜻하는 main()함수가 스택에 들어간다.

그후 각 함수들을 정의한 후 마지막으로 printSquare(4)를 만난다. 이는 함수를 호출했으므로 스택에 printSquare를 추가한다.

동작순서는 다음과 같다.

  1. printSquare를 조회해서 안에 square()를 호출해서 담는다.
  2. square를 조회해서 multiply()를 호출 했으므로 담는다.
  3. 이제 더이상 호출할 함수가 없으므로 실행을 한다.
  4. a x b를 반환하여 multiply를 실행
  5. 마지막 스택이 square가 되어 다시 이를 실행
  6. 또 printSquare가 마지막 스택이 되어 실행.

이런식으로 stack이 모두 비워질때 까지 순차적으로 실행한다.

발표자는 이후 에러를 일부러 띄워서 함수가 순차적으로 어떻게 실행이 되었는지 다시 확인시켜 줬다.

스택 오버플로

탈출조건이 없는 재귀함수이다. 이를 실행하면

크롬 브라우저는 16000번 실행 한 후 이것이 버그란 것을 확인하고 사용자에게 에러로 알려준다.

블로킹 (blocking)

정의는 정확하지 않지만, 위의 스택오버플로의 예시의 재귀처럼 엄청 많은 실행을 거쳐 느려지는 현상 등을 얘기한다고 한다.

하지만 네트워크 요청, 이미지 프로세싱 등에선 다르다.
느린 동작이 스택에 남아있는 것을 말한다.

var foo = $.getSync('//foo.com');
var bar = $.getSync('//bar.com');
var qux = $.getSync('//qux.com');

console.log(foo);
console.log(bar);
console.log(qux);

제이쿼리로 동기실행 코드 예시이다.
이는 위의 예시와는 다르게 스택에 바로 쌓고 네트워크에서 응답을 보내주면 바로 빼버린다.
만약 네트워크가 느리거나 요청이 올바르지 않으면 꽤 오랫동안 요청을 보내는 상태가 된다. 이는 자바스크립트의 특징인 싱글 스레드와 관련이 있다.

실제 브라우저에서 사용하면 fetching해오는 동안 브라우저가 멈춰있고, 그 동안 open Alert를 여러번 클릭하면 응답이 모두 정상적으로 온 뒤에야 open Alert를 순차적으로 실행한다.

좀 더 전문적으로 말하면, webApi를 사용하는 동안 콜스택을 블로킹 하였다.

동기 함수

위에서의 사용자의 입장에선 에러와 같은 현상을 해결하려면 동기적으로 콜백을 주고받는 방법이 있다.

console.log('Hi');

setTimeout(function() {
	console.log('There');
}, 5000);

console.log('JSConfEU');

이를 실행하면 Hi와 JSConfEu가 먼저 출력되고 5초 뒤 There가 출력됨을 확인할 수 있다.

콜스택의 입장에서 보면

  1. main 함수 스택
  2. console hi를 먼저 쌓고 실행후 큐
  3. setTimeout를 쌓고 실행후 큐
  4. console JSConfEU를 쌓고 실행후 큐
  5. 먼저 쌓여있던 main이 큐
  6. 다 비워진 스택에 setTimeout내부 동작 등장
  7. console there를 쌓고 실행후 큐

분명 setTimeout은 뒤늦게 실행되는데, 스택에선 바로 사라진다. 어떻게 된 일일까?

이벤트 루프와 동시성

드디어 기나긴 개요가 끝났다...
위의 setTimeout의 이상한 점을 설명한다.
앞서 V8엔진에는 단지 힙과 스택을 알고있을 뿐이고,
브라우저에서 이벤트루프 등을 갖는다고 했었다.

위의 3번째 과정까지 진행된 상황이다.
콘솔에 이미 hi가 찍혀있고, setTimeout이 스택에 들어온 상황이다.
그리고 이를 큐 시킨다.

이제 setTimeout의 두번째 인자인 시간을 인식해서 webapi중 하나인 timer()에 매개변수를 넣어 대기시킨다.

타이머가 대기 중일때 마지막 코드인 console JSConfEU가 스택 후 힙이되어 콘솔에 찍혔다.

드디어 타이머가 끝났고, 이제 setTimeout의 콜백함수를 테스크 큐에 넣는다.
이제 대망의 이벤트루프 가 나왔다.
역할은 콜스택과 테스크 큐를 주시하는데, 스택이 비어있으면 큐의 첫번째 콜백을 스택에 쌓아 효과적으로 실행해준다.

다시말하면, 태스크 큐에 담고있던 콜백함수를 콜스택이 비어있으니 감지해서 밀어넣어 준것이다.

그 이후, 드디어 콜백에 담긴 console.log를 인식해서 콜스택에 담아냈다.

이벤트루프 이용한 꼼수?
위와 같은 동작으로 axios등을 사용하지 않고 비교적 순수...? 자바스크립트 만으로 동기적으로 실행시키고 싶을때, setTimeout(()=>{},0)이렇게 0초를 넣어 실행시키기도 한다. 그냥 단지 동작은 똑같은데 엄청 빨라진 것이다.

ajax 요청

위의 설명과 같이 ajax를 사용한 모습이다. 이제 우리가 흔히 백엔드 api에 요청할때 어떤식으로 동작하는지 예상할수 있게 되었다. 똑같이 먼저있는 hi를 실행하고 get요청을 webapis로 보낸다.

똑같이 뒤엣줄의 JSConfEU를 출력하고 스택을 비운다. get요청은 계속 요청하고 있다.

드디어 요청이 끝나고 응답을 태스크 큐에 밀어 넣고,

콜스택이 비워져있는 것을 확인한 이벤트루프는 콜 스택에 태스크 큐 값을 집어넣고, 이제 webapi의 콜백함수를 스택 -> 실행 -> 큐 시킨다.

ajax with 클릭이벤트

놀랍게도 이번 세션에서는 발표자분이 직접만든 이벤트 루프 예시 사이트가 있었다. 직접 코드를 입력하고 테스트 해볼수 있어서 좋았다.

링크

링크에서의 예시 말고도, 각 이벤트의 처리를 모두 1초로 두면 setTimeout이 쌓여져 있고 그냥 순차적으로 처리되는것도 확인할 수 있었다.
즉, setTimeout은 단지 지연시간을 주는거여서 내부의 콜백이 실행되는 시간을 정해주는게 아니라는 것 을 알았다.

이벤트루프를 알아야 하는 이유

브라우저는 기본적으로 화면을 매번 16.6ms, 즉 1초에 60프레임을 repaint한다. 하지만 자바스크립트를 사용함으로써 콜스택에 무언가가 쌓여있으면 렌더링이 막히게 된다.

다만 렌더는 콜백보다 더 높은 우선순위를 갖는다.
위의 말뜻을 그대로 하면 16ms마다 큐에 렌더가 들어가고, 스택이 깨끗해진 후에 렌더를 한다.

이러한 렌더를 방해하고 혹은 피해가는 코드를 짜보자.
동기와 비동기 예시코드가 있다.

// Synchronous
[1,2,3,4].forEach((i)=>{
	console.log('processing sync');
    delay();
});

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

asyncForEach([1,2,3,4], (i) => {
	console.log('processing async', i);
    delay();
})

맨 위의 코드를 실행하면 동기적으로 느리게 진행되며 콜 스택을 차지하게 된다. 이렇게 되면 렌더를 막아버리게 된다. 렌더링이 막히면 텍스트 선택, 방응 등을 보는게 불가능해진다.

두번째 비동기 코드를 실행했다.
asyncForeach를 실행해서 스택에 쌓고 빠르게 webApi를 거쳐 callback큐에 쌓아놓게 된다.

이렇게 콜백큐에 대기시켜놓을수 있게 되면서 앞서 이벤트루프의 기본 동작인 콜스택을 감지해서 비워져있을때 하나씩 갖고있는 콜백큐를 내보내고, 그 틈새를 이용해서 렌더큐가 빠르게 돌아 렌더와 자바스크립트 실행을 동시에 할수있게 되었다.

profile
Frontend

0개의 댓글