이벤트 루프

싱글 스레드

JS는 싱글 스레드 프로그래밍 언어이다. 이 말은 싱글 스레드 런타임을 갖고있고 결국 하나의 콜 스택만 갖고 있다는 말이다. (하나의 프로그래밍은 동시에 하나의 코드만 실행할 수 있음!)

JS 프로그래밍에서 느려진다는 것은 무엇일까? 정확한 개념은 존재하지 않지만 느린 동작이 스택에 남아있는 것을 보통 느려진다(Blocking) 라고 한다. 예를들면 네트워크 요청 같은 작업(실제로는 보통 비동기로 처리되지만)이 동기적으로 실행된다면 오랜시간동안 콜 스택을 Blocking 할 것이다.

이런 문제를 브라우저에서 JS는 어떻게 해결할까?

싱글 스레드 in 브라우저

만약 네트워크 요청이 동기적으로 실행된다면 콜 스택을 블로킹하여 비워질 때 까지 브라우저는 리렌더링을 하지 못한다. 브라우저에서는 이를 비동기 콜백으로 해결한다. 비동기 콜백의 작동방식을 알려면 Task Queue이벤트 루프에 대해 알아야한다!

태스크 큐는 스크립트 실행, 이벤트 핸들러, 콜백함수 등의 태스크(Task)가 담기는 공간이다. 태스크가 콜백함수라면 그 종류에 따라 두개의 큐에 나눠 담겨진다.

  • 태스크 큐 : setTimeout(),setInterval(), UI 렌더링,requestAnimationFrame()
  • 마이크로 태스크 큐: Promise, MutationObserver

이벤트 루프는 2개의 큐를 감시하고 있다가 콜 스택이 비게 되면, 콜백함수를 꺼내와서 실행한다. 이 때 마이크로 테스크 큐의 콜백함수가 우선순위를 갖기 때문에 마이크로 테스크 큐가 전부 비워지면 태스크 큐에서 꺼내와서 실행한다.

비동기 콜백(asyncronous callback)

콜백 ? 함수가 call한 다른 함수..

console.log('콜 스택!');
setTimeout(() => console.log('태스크 큐!'), 1000);
Promise.resolve().then(() => console.log('마이크로태스크 큐!'));

위와 같은 코드가 있을 때 브라우저에서 어떻게 콜 스택이 동작하는지 살펴보자.

그림이 어려운 관계로 글로 적어야겠다.

  1. main()가 콜 스택에 쌓인다.
  2. console.log()가 그 위에 쌓이고 콘솔 창에서 ‘콜 스택’을 출력한다. 그 뒤 함수가 종료되고 콜 스택에서 제거된다.
  3. setTimeout(() => console.log('태스크 큐'), 1000)가 콜 스택에 쌓이고 setTImout의 콜백함수인 ()=> console.log('태스크큐')Web API 영역에 전달된다. 전달된 시점에서 타이머가 시작된다. 그 뒤 콜 스택에 쌓인 setTimeout 함수는 제거된다.

Web API 영역?

자바스크립트 런타임은 하나의 프로그래밍은 하나의 코드만 실행할 수 있다. 브라우저는 Web API로 자바스크립트에서 호출할 수 있는 스레드를 효과적으로 지원한다. 노드에서는 C++ API가 있다.

  1. Promise.resolve가 콜 스택에 쌓이고 콜백함수가 **마이크로 태스크 큐**에 쌓인다. 그 뒤Promise.resolve` 가 스택에서 제거된다.

4.5 setTImeout의 콜백함수의 타이머가 다 된 시점에 태스크 큐에 쌓인다.

  1. 더 이상 실행할 함수가 없으니 콜 스택은 비워진다.
  2. 이벤트 루프는 콜 스택이 비워지면 태스크 큐에서 콜백함수를 가져와서 실행한다. 이때 마이크로 태스크큐가 우선순위를 갖기 때문에 먼저 실행된다.
  3. Promise의 콜백함수가 콜 스택에 쌓이고 함수가 실행되고(마이크로태스크 큐 출력) 스택에서 제거된다.
  4. setTimeout의 콜백함수가 콜 스택에 쌓이고 함수가 실행되고(태스크 큐 출력) 스택에서 제거된다.

그외..

setTimeout(cb, 0)을 하는 이유가 뭘까?

0초를 time으로 넣어 하는 이유는 스택이 비워진 뒤 실행을 보장하기 때문이다.

setTimeout을 여러번 호출하면 원하는 대로 실행되지 않는다.

setTimeout(() => console.log('hi'), 1000)
setTimeout(() => console.log('hi'), 1000)
setTimeout(() => console.log('hi'), 1000)
setTimeout(() => console.log('hi4'), 1000)

위 코드의 기대치는 4번째 코드의 경우 1초뒤에 hi4를 실행하길 기대할 것 이다. 하지만!

위에서부터 각각의 콜백함수들이 큐에서 하나씩 빠져나가면서 1초 보다 조금 늦게 실행될 수 있다. ( 동기적으로 처리되는 setTimeout 호출이 모두 끝난 뒤 비동기로 처리된 콜백들이

여기서 알 수 있는 것은 딜레이되는 최소의 시간만을 지정할 수 있다라는 점이다.

Render

브라우저의 렌더링도 하나의 콜백처럼 행동한다. 따라서 태스크 큐에 쌓이게 되는데 콜 스택이 비어있지 않으면 렌더링을 하지 못하게 된다. 따라서 동기적으로 처리되는 코드가 blocking이 오래 지속되는 코드라면 비동기적으로 처리하는 것도 렌더링을 최적화하는 한가지 방법이다.

[1,2,3,4,5].forEach((v) => console.log(v))

적절한 예시가 아니지만 forEach는 기본적으로 동기적으로 작동하기 때문에 처리하는 작업이 blocking이 지속되는 작업이라고 ‘가정’하면,

function arrrayForeach(array, cb){
  array.forEach(function(){
    setTimeout(cb, 0)
  })
}

위와 같이 비동기적으로 태스크큐에 쌓아서 처리하는것도 한가지 방법이 된다!

참고

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

https://github.com/baeharam/Must-Know-About-Frontend/blob/master/Notes/javascript/event-loop.md


Written by@[HongDongUk]
공부한 것을 소소하게 적는 블로그.

GitHubFacebook