이 영상은 V8 runtime과 browser가 제공하는 WEb API, Event loop와 Callback Queue에 관해 설명하는 영상이다.
나는 영상에서 나오는 몇몇 스크립트를 글로 적어보았다.
Browser는 DOM, AJAX, setTimeout()등과 함께 event loop와 Callback Queue를 가지고 있습니다.
하지만, 이것들이 어떤 식으로 연결되어 움직이는 지는 정확하게 이해하는 분들이 많지 않을 것입니다.
Javascript는 Single Threads Progamming Language 입니다.
Single Thread runtime을 가지고 있다는 말인데 결국 한번에 하나의 싱글 콜 스택만을 가지고 있다는 말입니다.
그게 싱글 스레드의 의미겠죠. 하나의 프로그램은 동시에 하나의 코드만 실행할 수 있다는 것입니다.
콜 스택은 데이터 스트럭처로 실행되는 순서를 기억하고 있습니다.
스택의 가장 위쪽에서 해당 함수를 꺼내게 됩니다. 이게 콜 스택이 하는 일의 전부죠.
코드를 실행하면 실행되는 코드 자체를 말하는 메인 함수를 스택에 집어 넣게 됩니다.
그러면 이제 함수들을 정의하게 됩니다. 현재 우리가 가지고 있는 것들을 정의한다고 볼수 있겠네요.
함수를 호출하고 스택에 함수를 추가후, 다음 함수를 호출하고 스택에 추가하고,
연산함수를 만나면 연산 결과를 반환합니다.
무엇인가를 리턴할 때마다 우리는 스택 맨위에 있는 것을 꺼내게 됩니다.
function multiply(a,b){
return a * b;
} // stack 추가 task2
function square(n){
return multiply(n, n);
} // stack 추가 task3
function printSquare(n){
var square = square(n);
console.log(squared);
} // stack 추가 task4
printSquare(4); // stack 추가 task1
multiply에서 square로 리턴되고, printSquare까지 돌아옵니다. console.log()를 실행하겠네요.
여기에 리턴은 보이지 않지만, 암묵적으로 리턴합니다.
함수의 마지막 줄에 도달했기 때문이죠. 자 이제 다 끝났습니다.
이것이 바로 콜 스택입니다.
이해가 되셨나요? 콜 스택을 그림으로 그려보시지 않았더라고 해도 브라우저에서 개발을 하시다보면 어느정도 이해하셨을 거라고 생각합니다.
function foo(){
throw new Error('Oops!');
}
function bar(){
foo();
}
function baz(){
bar();
}
baz();
baz가 호출하는 함수 bar가 호출하는 foo함수는 이렇게 에러를 만들게 된다면,
크롬 개발자 도구에서는 아래와 같이 스택의 꼬리를 물면서 Oops!를 표시하게 될겁니다.
에러가 발생한 스택의 상태를 보여주는 것이죠. uncaught error는 foo에서 생겼는데 bar가 호출했고, bar는 또 baz에게서 이런식으로 익명함수 즉 main 함수까지 올라가게 됩니다. 스택을 날려먹었다는 용어를 들어보셨나요?
좋은 예시 하나를 보여드리려고 합니다. foo 함수를 호출하는 foo함수가 있다면 어떻게 될까요?
main 함수가 foo함수를 호출하는 foo함수를 호출하는 foo함수를 호출하게 되죠.
(function main(){
function foo(){
return foo();
}
foo(); //스택 추가 task 1
})(); //IIFE
그럼 크롬이 이렇게 말합니다. "스스로를 호출하는 foo함수를 16,000이나 계속 하실건 아니겠죠?"
ReangeError
RangeError 객체는 어떤 값이 집합에 없거나 허용되는 범위가 아닐 때 오류를 나타냅니다.
어떤 값을 그 값이 포함되는 범위를 허용하지 않는 함수에 인수로 전달하려 할 때 RangeError가 발생합니다.
probably didn't mean to K foo 16,000 times recursively so I'll just like kill things for you and can figure out where your bug lies right so although I may be representing a new king of side of the C stack you've probably got some sense of it umm in your development practice
"이 녀석들을 중지 시킬게요. 버그를 고쳐주세요." 그렇겠죠?
콜 스택이라는 측면을 설명하고 있긴 하지만, 이 역시 개발하시다 보면 이미 겪어 보셨을 겁니다.
그럼 이제 중요한 질문이 생깁니다. 느려진다는 것은 어떤것인가?
블로킹 혹은 블로킹 현상에 대해서 이야기하긴 하지만, 블로킹이라는것에 대한 정확한 정의는 존재하지 않습니다. 그저 느리게 동작하는 코드일 뿐입니다.
console.log 자체는 느리지 않습니다. 하지만 while loop안에서 수십억번 실행된다면 느리겠죠.
What happens when things are slow?
느려진다는 것은 어떤것인가
So, we talk about blocking and blocking behavior and blocking, there's no strict definition of what is and didn't blocking, really it's just code that's slow.
네트워크 요청이나 이미지 프로세싱은 느립니다.
느린 동작이 스택에 남아있는 것을 보통 블로킹이라고 말하게 됩니다.
So console.log isn't slow, doing a while loop from one to ten billion is slow, network requests image requests are slow.
Things which are slow and on that stack are what are blocking means.
그래서 console.log는 느리지 않고, 10억에서 100억까지 while 루프를 돌리는 것은 느리고, 네트워크 요청 이미지 요청은 느립니다.
느리고 그 스택에 있는 것은 차단하는 것을 의미합니다.
So heres a little example, so let's say we have, this is like a fake bit of code, getSynchronous, right, like jQuery is like, AJAX request.
에를 들어 이런 코드가 하나 있다고 가정해보죠. 동기적으로 AJAX 요청을 보내는 JQuery 함수 getSync가 있다고 한다면, 어떤식으로 동작하게 될까요?
일단 비동기 콜백은 잊어버리세요. 동기적으로 작동한다고 생각해봅시다. 이 코드를 한줄한줄 실행해보죠.
이제 스택을 지울수 있겠네요. 프로그래밍 언어에서 싱글 스레드라고 하는 것은 루비 같은 언어와는 달리 여러개의 스레드를 사용하지 않는다는 의미입니다.
네트워크 요청을 하고는 마냥 끝날때까지 기다립니다. 그거 말고는 방법도 없어요.
문제가 뭐냐구요? 웹 브라우저에서 코드가 실행되고 있기 때문입니다.
자 이제 그럼.. 잠시만요 크롬 브라우저를 실행시켰습니다.
이 코드를 실행시켜 볼건데요. 브라우저는 실제로 ajax 요청을 동기적으로 실행시키지 않습니다.
var foo = $.getSync('//foo.com');
var bar = $.getSync('//bar.com');
var qux = $.getSync('//qux.com');
console.log(foo);
console.log(bar);
console.log(qux);
그래서 저는 이걸 동기적으로 실행하는 while loop 안에 넣고, 5초동안 가상으로 동작하는 코드를 만들었습니다.
콘솔을 열면 실제로 무엇이 일어나는지 볼수 있겠네요.
foo.com에 요청을 하고 있는 동안 아무것도 클릭할 수 없습니다. 왜 그런걸까요?
좀 전에 클릭한 Run 버튼조차 re-rendering을 끝내지 못했네요.
브라우저가 멈췄어요.
브라우저는 모든 request가 완료될때까지 멈춰있을겁니다.
그리고 나서는 이런 심각한 문제들이 나타납니다. 멈춰있는 동안 행동을 기억하고 있었지만, 그릴수도 렌더링 할 수 없었습니다.
아무것도 할 수 없었죠. 왜나하면 콜스택에 어떤 것들이 남아있으면, 동기적으로 실행되는 네트워크 요청이 콜 스택을 블로킹하여 브라우저는 다른 일들을 할 수 없었습니다. 렌더링이나 다른 코드를 실행하지 못하고 그냥 멈춰버렸죠.
별로네요. 유려한 UI를 만드려고 한다면, 콜스택을 멈추게 해서는 안되겠죠.
Here's a function, Call me maybe?
그러면 어떻게 해결해야 할까요?
제일 쉽게 접할수 있는 건 비동기 콜백입니다.
브라우저 혹은 노드에는 블로킹 함수가 거의 없습니다.
대부분 비동기로 만들어졌죠. 이는 어떤 코드를 실행하면 결국 콜백을 받고 이걸 나중에 실행한다는 말입니다.
자바스크립트를 해보셨다면 비동기 콜백들은 다들 경험해보실겁니다.
그러면 이것들은 실제로 어떤식으로 실행될까요? 예를 들어보겠습니다. 이런 코드가 있습니다.
console.log('Hi');
setTimeout(function() {
console.log('There');
}, 5000);
console.log('JSConf EU');
setTimeout을 사용하여 console.log로 hi를 출력하도록 하면 console.log는 큐에 등록되고 setTimeout을 사용하여 console.log로 hi를 출력하도록 하면 console.log는 큐에 등록되고 JSConf를 먼저 출력합니다. 5초 뒤에 there를 찍게 됩니다. 그렇죠?
이렇게 setTimeout이 뭔가를 실행하고 있습니다. 그러면 지금까지 다뤄왔던 스택상에서는 어떻게 실행되는 것일까요? console.log hi를 출력하고 setTimeout을 실행합니다.
어떻게 실행되는 것일까요? console.log hi를 출력하고 setTimeout을 실행합니다.
아시다싶이 바로 실행되지 않습니다. 5초 안에 실행되겠죠.
스택에 추가되지 않고, 어떻게인지는 모르겠지만 그냥 사라져버립니다. 아직 설명할 방법이 없네요.
곧 알게될겁니다.
- 다음 JSConf EU를 출력합니다.
- 그리고 5초후, 마법처럼 there가 스택에 나타납니다.
one thing at a time, except not really.
어떻게 된걸까요?
여기에서 이벤트 루프와 동시성이 역할을 하게 됩니다.
저는 자바스크립트는 한번에 하나의 일만 할수 있다고 이야기 했습니다. 거짓말일까요?
물론 사실입니다. 자바스크립트는 한번에 하나밖에 할수 없습니다.
자바스크립트는 다른 코드를 실행시키는 동안 AJax요청을 실행할 수 없습니다. setTimeout 역시 마찬가지죠.
하지만 우리가 이걸 동시에 할 수 있는 이유는 브라우저는 단순 런타임 이상을 의미하기 때문입니다.
이 그림 기억나시나요? 자바스크립트 런타임은 한번에 하나만 할 수 있습니다.
하지만 브라우저가 Web API와 같은 것들을 제공하죠.
이들은 자바스크립트에서 호출할 수 있는 스레드를 효과적으로 지원합니다.
여기에 동시성이 들어오는 것이죠. 백엔드 개발자시라면,
Web API대신 C++ API를 사용할 뿐이지, Node 또한 다르지 않습니다.
C++가 숨기고 있죠. 이제 좀 더 브라우저란 무엇인가를 보여주는 그림을 보면서 알아보겠습니다.
이전과 같습니다. 코드를 실행하면 console에 hi를 출력합니다.
이제 콜백 함수와 지연시간을 setTimeout 콜에 넘겨보겠습니다.
setTimeout은 브라우저에서 제공하는 API입니다. V8 소스코드에 존재하지 않다는걸 기억하시나요?
javascript가 실행되는 런타임 환경에 존재하는 별도의 API입니다.
브라주어가 타이머를 실행시키고 카운트 다운을 시작합니다.
이건 setTimeout 호출 자체는 완료되었다는 의미고 우리는 스택에서 함수를 지울수 있습니다.
"JSConfEU"를 출력하고 다시 지워집니다. 이제 Web API에서 실행하고 있는 타이머가 남았습니다.
어느 순간 갑자기 스택에 함수를 집어넣던가 하는 것 말이죠.
이제 테스크 큐와 콜백 큐가 활약할 차례입니다.
모든 Web API는 작동이 완료되면 콜백을 테스크 큐에 밀어 넣습니다.
드디어 이 발표의 제목이기도 한 이벤트 루프에 다달았습니다.
이벤트 루프는 이 전체 시스템에서 아주 단순한 일을 하는 작은 파트 입니다.
이벤트 루프의 역할은 콜 스택과 테스크 큐를 주시하는 것입니다.
스택이 비어있으면, 큐의 첫번째 콜백을 스택에 쌓아 효과적으로 실행할 수 있게 해줍니다.
보시는바와 같이 스택이 비어있고, 테스크 큐에는 콜백이 하나 있네요.
이벤트 루프는 "어, 내가 할일이 있네. 자 이거 받아" 하며 콜백을 스택에 넣어줍니다.
스택은 Javascript 영지라는걸 기억하세요.
이제 V8엔진으로 돌아가서 console.log('there')를 실행합니다. 이해가 되시나요?
좋습니다. 이제 여러분들은 javascript 비동기 함수가 어떤식으로 동작하는지 아시게 되었을 겁니다.
특히 무엇인가 알수 없는 문제가 생겼을때 "setTimeout 0을 사용하면 해결될거야" 라고 말하는 상황이 말이죠.
음.. 함수를 0초 후에 실행하라고?
그럼 setTimeout 자체를 왜 하라는건데? 라며 저와 비슷한 분들은 그러셨을 겁니다.
되긴 되는것 같은데, 왜 그런지 모르겠다.
일반적으로 이것은 스택이 비어있을 때까지 기다리게 하기 위해서 입니다.
"hi", "JSConfEU" 그리고 마지막에 "there"가 개발자 콘솔에 나타날 겁니다.
이 코드를 실행해보면서, 작동원리를 알아보죠.
"hi"를 프린트하고, setTimeout 0이 실행되면 Web API는 바로 종료하고 큐에 콜백을 집어넣습니다.
큐에 있는 콜백을 스택에 쌓을 수 있습니다. 스택은 계속해서 실행을 합니다.
console.log('JSConfEU')를 실행하고 스택이 정리됩니다.
이제 이벤트 루프가 개입하여, 콜백을 호출합니다. 이것이 setTimeout 0이 코드 실행을 어떤 이유에선가 스택에 마지막까지 지연시키는 이유입니다.
정확히는 스택이 비워질때까지겠죠.
모든 이런 종류의 Web API는 동일한 방식으로 동작합니다.
Ajax Request는 URL로 호출할 때 콜백을 함께 실행하게 됩니다.
이 역시 동일하게 작동하게 되죠.
console.log('hi') 다음 AJax 요청하고 AJax 요청은 javascript runtime이 아니라,
브라우저 Web API에서 실행됩니다. 이제 XHR Web API가 실행되는 동안 다른 코드는 정상적으로 실행할 수 있습니다. XHR 요청이 끝날떄까지 끝나지 않을 수도 있습니다. 그래도 상관없습니다.
스택은 계속해서 코드를 실행할 수 있습니다. XHR 실행이 완료되었다면 콜백은 큐에 쌓이게 되고,
이벤트 루프에 의해 실행됩니다. 이 과정이 비동기 함수가 호출되는 방식입니다.
자, 그럼 진짜 복잡한 예제를 실행해보죠.
여기에 500개의 애니메이션을 아 이런..
휴 링크가 나오네요. 모두 잘 보이시나요?
자바스크립트 런타임을 시각화 해주는 프로그램을 만들었습니다. Loop라고 하는데요.
한번 예시를 보시면 방금 전 슬라이드에 있던 예시랑 비슷한데 XHR을 SHIM 하지는 않았어요.
할 수는 있었지만 우선 하지 않았고, 여기를 보시면 로그가 생기는데 addEventListener, setTimeout 등을 기준으로 shim을 해서 콘솔에 보여주는 겁니다.
실제로 실행을 해봅시다. DOM API, timeout을 추가하고, timeout이 시간을 재고 코드가 실행되죠.
콜백은 큐로 쌓이고, 곧 실행이 됩니다.
여기를 클릭하면 Web API를 실행할거고 큐에 콜백을 쌓은 후 실행합니다.
제가 클릭을 100번 하면 이런 식으로 작동하죠.
그 후에 차례대로 처리됩니다.
예시 몇 개가 더 있는데 보여드리도록 하지요.
실제로 맞닥뜨리실만한 상황들이지만, 여러분이 Async API와 관련해서 생각하시지 못했을 부분들에 대해 말씀드릴게요.
만약 1분 딜레이가 설정된 setTimeout을 4번 호출하고, hi를 콘솔에 찍는다면, 콜백이 큐에 쌓인 후 네번째 콜백이 1초후에 실행되어야 함에도 불구하고 여전히 실행되지 않고 있습니다.
이걸 보면 timeout이 실제로 정해진 시간과는 달리 제대로 작동하지 않을 수도 있고 다만 딜레이되는 최소의 시간만을 지정할 수 있다는 것을 알 수 있습니다.
마치 0초로 설정된 코드가 바로 실행되지 않는 것 처럼요. 대신 차례를 기다린 후 실행되죠. 그렇죠?
이 예시에서는 콜백에 대해서 더 이야기 하려고 합니다.
누구에게 물어보느냐에따라 다르지만, 콜백은 둘중에 하나로 묘사됩니다.
하나, 콜백은 다른 함수가 부르는 함수이다.
혹은 앞으로 큐에 쌓일 비동기식 콜백이라고 묘사할 수 있죠.
여기 이 코드가 그러한 차이의 예시인데 ForEach 함수의 경우, 함수를 실행시키기는 합니다.
콜백이라고 할 수 있지만, 비동기적으로 실행하지는 않죠.
자신의 자체적 스택에서 실행시킵니다. 한편 async foreach를 하나 선언해서 배열과 콜백을 받아 각 요소에서 setTimeout을 0으로 실행하는 것도 가능합니다.
값을 하나 넘겨야 할 것 같지만, 어쨌든, 실행을 한번 해보고 다른점이 무엇인지 보죠.
첫번째 블록의 경우, 스택을 차지합니다. 그렇죠? 실행이 다 끝날 때까지요.
반대로 비동기 버전은 여러개의 콜백을 큐에 쌓을거고, 스택이 비워지면, 실재로 쌓인 콜백들이 실행되게 되죠.
이 예시에서는 콘솔 함수가 금방 실행되서 async의 이점이 잘 드러나지 않지만, 여러분이 각 배열 요소에 대해 오래 걸리는 처리를 해야한다고 치면, delay 함수가 있고 이게 느린 함수라면...
실제 브라우저 repaint 혹은 렌더링 상황을 재연하는 기능입니다.
제가 아직 이러한 것들이 렌더링과는 어떤 관계가 있는지 충분히 설명하지 않았지만 브라우저는 여러분이 자바스크립트로 하는 무언가로 인해 제약을 받습니다.
브라우저는 기본적으로 화면을 매 16.6 밀리세컨드 즉, 1초에 60프레임을 repaint하는게 이상적입니다. 그게 제일 빠른거죠. 하지만 브라우저는 여러분이 자바스크립트로 하는것들로 인해 여러가지 이유로 제약을 받습니다. 그래서 스택에 코드가 있으면, 렌더링을 못합니다. 렌더도 하나의 콜백처럼 행동하니까요.
스택이 비워질 때까지 기다려야 하는 겁니다. 다른 점이라면, 렌더는 여러분의 콜백에 비해 더 높은 우선순위를 갖죠. 매 16milliSecond 마다 큐에 렌더가 들어가고, 스택이 깨끗해진 후에야 렌더링을 합니다.
그래서 이 렌더 큐가 그 렌더링을 재연한거예요. 그래서 매 초마다 "렌더 해도 될까?" "그래" 하는 식으로요. 지금은 아무것도 없기 때문에 진행이 되는거에요.
하지만 제가 코드를 실행하면, 우리가 이 느린 동기식 루프를 진행하는 동안 렌더는 막히게 됩니다.
렌더가 막히면, 화면의 텍스트를 선택하거나 선택해서 반응을 보거나 하는게 불가능하죠. 이 전에 보여드린 예시처럼요.
지금 우리가 async timeout을 큐에 쌓는 동안 스택이 쌓이지만 상대적으로 빨리 사라지고는있죠.
이때 우리는 렌더에게 각 요소 중간중간에 렌더가 끼어들 수 있는 기회를 줄수 있습니다.
큐가 async를 통해 쌓여있으니까요. 즉, 이게 렌더링을 재연한 것입니다.
사람들이 event loop를 막지 말라고 할때 바로 이런 현상을 뜻하는 것이죠.
스택에 필요없는 느린 코드를 쌓아서 브라우저가 할일을 못하게 만들지 말아라, 유동적인 UI를 만들어라.
이것이 이미지 처리나 애니메이션이 너무 잦아졌을 때 큐 관리에 주의를 기울이지 않으면 이런 일이 일어나니까요.
예를 들어서 스크롤 핸들러를 이용해보면, 스크롤 이벤트는 DOM에서 매우 자주 일어나죠.
스크롤이 매 프레임에서 매 15 밀리세컨드마다 작동한다고 짐작했을때, 이런 코드를 작성해보았습니다.
document.scroll이 일어날때 애니메이션을 넣거나 무언가를 하죠. 이 코드상으론 제가 스크롤을 할때마다,
큐에 엄청나게 많은 콜백을 쌓습니다. 그리고 매번 이걸 처리하면서 각각의 느린 프로세싱이 일어날 때 마다, 스택을 채우지는 않지만 큐를 이벤트로 범람시키죠. 그래서 이것을 통해, 제 생각엔 어떤 식으로 대처할지, 큐에 이벤트가 쌓이는 것은 어쩔수 없지만, 매 몇 초마다 혹은 유저가 스크롤을 멈출 때 까지 작업량을 줄인다든지 하는 결정을 내릴 수 있겠죠.
제가 준비한것은 여기까지 입니다.
이것에 관련된 다른 토론들도 아주 많아요.
코드를 실행할 때, 예를 들어 이 코드는 런타임에서 실행되는데 제가 자바스크립트 parser로 이걸 실행하고, 매 코드 앞뒤로 while루프를 실행해서 0.5초씩 걸리게 한다면, 모든 코드를 슬로우 모션으로 실행해요.
이걸 웹 워커로 옮기고 이런저런걸 해서 시각화 시키면, 런타임에서 어떤 일이 일어나는지 알수 있습니다.
참조글: https://dev.to/rahmanmajeed/javascript-the-runtime-environment-35a2
참고: https://dev.to/marienoir/understanding-javascript-runtime-environment-4na2
참고: http://latentflip.com/
참고: https://v8.dev/docs
참고: https://chromium.googlesource.com/v8/v8.git