원문링크
https://www.youtube.com/watch?v=8aGhZQkoFbQ
jsconfEu편 2번째 내용을 가져왔다. JavaScript 런타임 환경에 대한 설명이다.
https://velog.io/@kip/%EB%8F%99%EA%B8%B0%EC%99%80-%EB%B9%84%EB%8F%99%EA%B8%B0
여기서도 가볍게 다뤘다.
그때는 생소한 지식이라 설명을 함에도 헷갈리는 부분이 있었는데, 이번엔 어느정도 이해를 가지고 디테일한 설명을 하려고한다.
이번에도 최대한 강연자의 의미를 해치지 않기위해 중요한 부분에는 단어 그대로를 사용예정.
Javascript Runtime
single-thread
한번에 단 하나의 single call-stack 만을 가진다.
(위의 이미지 참조)
즉,
callstack은 data structure로서, 실행되는 순서를 기억하고 있다.
(함수 실행-> 스택에 해당하는 함수 -> 리턴 -> 스택의 최상단에 함수를 꺼낸다.)
이때, 우리가 XMLHttpRequest를 생각해보자.
서버와의 통신을 위해서 사용하는 ajax나 axios같은 경우, 어떤 요청을 보낸다고 하고, 요청의 속도가 1초 정도라면, 동기적으로 실행되기 때문에 그 사이동안 브라우저의 동작을 하지 못한다(browser request).
the solution? asynchronous (비동기)
위와 같은 문제를 해결하기 위해 비동기 콜백이 등장한다.
비동기 함수 중 setTimeout이 있다.
setTitmeout(() => {}, 1000 //시간(ms))
어떤 콜백함수를 호출하는데 이 함수는 1초 뒤에 실행이 된다.
왜 그렇게 되는 것일까?
여기서 등장하는 키워드 2개
자바스크립트는 한번에 하나밖에 할 수 없다는 사실은 변함이 없다.
그럼에도 이걸 동시에 할 수 있는 이유는 브라우저는 런타임 이상의 의미가 있다는 것이다. 브라우저는 webAPIS(자바스크립트에서 호출할 수 있는 스레드를 효과적으로 지원)를 제공한다.
다시 본론으로 돌아가서 이번엔 비동기+동기를 호출해본다고 가정한다.
이렇게 되면 브라우저에서 지원해주는 API인 setTimeout(v8엔 존재하지 않는다)으로 인해 Javascript가 실행되는 런타임 환경에 존재하는 별도의 API로 있는다.
브라우저가 타이머(여기선 1초)를 실행시키고 카운트 다운을 한다.
=> setTimeout이라는 함수의 호출은 완료되었고, stack에서 이 함수를 지울 수 있게된다.
즉, 지운다는 말은 다음 동기함수를 바로 호출한다.
이 과정에서, setTitmeout은 webAPIs에 남아 1초 동안 대기한다.
1초가 지난 뒤, webAPIs는 작동이 완료되면 태스크 큐(콜백 큐)에 콜백함수(cb)를 밀어넣게 된다.(setTimeout(()=>{ } //이 함수, time)
그럼 stack과 webapis에서의 동작은 모두 끝난 상태이고 콜백 큐에 cb함수가 남은 채로 있다. 여기서 event loop는 stack 과 task queue를 주시한다.
만약 stack이 비어있으면(evnet loop는 stack이 비워질 때까지 기다린다.), 큐의 첫번째 cb를 stack에 쌓아 효과적으로 실행할 수 있게 도와준다.
if, setTimeout의 시간이 0이라면 어떻게 될까?
직접 해보면 안다.
이 설명을 제대로 이해한 후 곰곰히 생각해보면 어떻게 동작되는지 예상할 것이다. 결과는 0이라도 똑같이 비동기로 동작한다.
만약 동기 + 동기 + 비동기가 있다고 가정한다면,
event loop는 stack이 비워질 때까지 기다린다고 했으니 시간이 0이라도 stack에는 2개의 동기함수가 쌓여있을 것이다.
Ajax Request도 url로 호출할 때 cb를 함께 실행하게 된다.
ajax요청도 자바스크립트 런타임이 아닌 브라우저의 web API에서 실행된다.
if, setTimeout함수의 4개가 전부 1초뒤에 실행을 시킨다면 어떻게 될까?
직접 해보면 안다가 아닌 실제로 동작시킨다면,
결과는 시간을 지키지 못한다.
그 이유는 4개가 stack->webAPIs->callback queue로 가 있는 상태에서 첫번째 들어온 timeout함수가 먼저 실행될 동안 callback queue에는 3개의 timeout 함수가 대기 중이다.
따라서 정해진 시간과는 달리 제대로 동작하지 않을 수도 있다고 한다.
브라우저는 기본적으로 화면을 매 16.6ms, 즉 1초에 60프레임을 repaint한다. 하지만 우리가 사용하는 js로 인해 여러가지 제약을 받게되고, 스택에 코드가 있다면, 렌더링을 하지못한다.
렌더도 하나의 콜백과 같이 행동
그렇다면, forEach나 map과 같은 built-in 함수의 동기 비동기 차이는?
예를 들어보자.
[1,2,3,4] 라고하는 배열이 있고,여기에
[1,2,3,4].forEach(() => {
console.log("sync")
})
인 동기함수와
function async(arr, cb){
arr.forEach(()=>{
setTimeout(cb,0)
})
}
async([1,2,3,4], cb = () => console.log("async"))
인 비동기 함수가 있다고 하자.
앞서 설명한 바와 같이 똑같이 동작을 하는데, 차이가 있다.
브라우저 렌더링에 관한 문제이다.
브라우저의 렌더링은 콜백과 같이 행동한다고 했고, 또 내가 실행시킨 콜백 함수보다 더 높은 우선순위를 가진다.
해석해보자면, 동기함수의 호출에는 스택이 쌓여있고 이 호출이 끝나기 전에는 브라우저가 렌더링될 틈이 없다.
그런데 비동기 함수의 호출에는 더 높은 우선순위를 가지는 브라우저가 렌더가 되면서(16ms마다), 스택이 빈 뒤에 렌더링을 하게된다.
event loop에서 stack으로 가는동안 브라우저의 렌더가 끼어들 수 있는 기회를 준다.
결론
우리는 브라우저에 ui를 만들거나 어떤 이벤트를 발생시킬 때, 사용자들에게 최소한의 로딩과 지연을 줘야하는 것이 핵심이다.
비동기에서 또한
stack-> wep APi -> task queue에 비동기함수가 줄서서 기다릴 때 고민해야할 부분들이 있다.
강연자가 말하고자 하는 것은 즉,
브라우저가 할 일을 못하게 만들지 말고, 유동적인 ui를 만들어라
가령,
이미지 처리나 애니메이션이 너무 잦아졌을 때 큐 관리에 주의를 기울이지 않는다면 이런 일이 발생한다.
스크롤 이벤트가 대표적인 예시다.
스크롤은 매 프레임에서 15ms마다 작동한다고 하면,
스크롤을 할 때마다 callback queue에 수많은 cb를 줄세운다.
비동기로 이루어져 있다면 스택을 채우지는 않지만, 큐를 이벤트로 범람시키게 된다. 실제로 cb를 이렇게 작동시킬때 어떻게 대처할지,몇 초의 시간마다 유저가 스크롤을 멈출 때까지 작업을 중단시키는 등의 고민을 해야한다.