본문 바로가기
Front-End/Javascript

javascript 기초부시기 - 이벤트 루프

by kimik 2022. 5. 29.

사실 이벤트루프는 자바스크립트의 기초는 아니다. mdn web docs에서도 Adavanced에 속해있으니까.. 

하지만 이런 개념을 모르고 자바스크립트를 공부하다보면 작성자와 같이 다시한번 기초를 공부해야할 수 있다.

 

1.이벤트루프

자바스크립트는 싱글스레드 언어이다. 싱글 스레드라는건 한번에 하나의 작업만 처리 할 수 있다는 것인데,

자바스크립트를 다뤄보면 반복문을 실행하면서 API를 요청하고, 그 결과를 보여주는 다양한 작업을 한번에 수행하는 것처럼 보인다. 

이 싱글스레드의 메인스레드인 이벤트루프가 어떻게 동작하는지 알아보자.

 

이벤트루프는 스택(Stack), 힙(Heap), 큐(Queue)로 구성되어있다.

 

mdn 공식 문서에서 이벤트루프

1.1 스택(Stack)

자바스크립트에서 함수를 호출할때마다 프레임 스택을 형성하여 스택에 쌓이게 된다.

이때 동기함수냐 비동기함수냐에 따라서 동작이 달라진다.

 

1.1.1 동기 함수

setTimeout이나 promise, async/await와 같은 비동기 함수를 제외한 모든 함수를 말한다. 이 동기함수는 

 - 특별한 과정없이 스택에 쌓인다.

 - 스택은 선입후출의 룰에 의해 제일 마지막에 들어온 함수가 먼저 실행된다. 

 - 해당 함수의 스택이 사라지는건 return되어 값을 반환했을 때이다.

 

1.1.2 비동기 함수

setTimout이나 promise, async/await와 같은 비동기 함수는 

 - Web API가 호출된다. 

 - Web API가 비동기 함수의 콜백함수를 큐에 넣는다. 

 - 이벤트루프는 스택이 빈 상태가 되었을때 첫번째 큐에 있는 콜백을 스택으로 이동시킨다( 이를 틱(tick)이라 한다.)

 

이 스택이 쌓이고 사라지는 과정은 얼핏보면 자연스러워 보인다. 하지만 

 

function delay() {
    for (var i = 0; i < 100000; i++);
}
function foo() {
    delay();
    bar();
    console.log('foo!'); // (3)
}
function bar() {
    delay();
    console.log('bar!'); // (2)
}
function baz() {
    console.log('baz!'); // (4)
}

setTimeout(baz, 10); // (1)
foo();

 

위의 코드를 보면 for (var i = 0; i < 100000; i++); 부분이 10밀리초보다 더 오래 걸리기 때문에,

 

10밀리초 후에 baz!를 보여주고 bar!와 foo!가 로그에 표시될것이라 예상하지만, 결과는 

bar!
foo!
baz!

이런 console창을 볼것이다.

 

이렇게 10밀리초 후의 콜백이 의도치않게 동작하지않는것은 비동기 함수가 스택에 쌓이는 조건이

'이벤트루프는 스택이 빈 상태가 되었을때'

이기 때문이다. 즉 동기함수에서 비동기 함수가 걸리는 시간, 그 이상의 처리(대부분 반복문)를 하고있다면 비동기 함수가 원하는 시간에 

마무리되어 콜백 함수를 스택에 넣을 수 있다해도, 스택이 빈 상태가 아니기 때문에 실행할 수 없다. 

 

부가적으로 스택에 있는 함수가 값을 반환하여 스택에서 사라지더라도, 함수의 인수와 지역변수는 스택 바깥에 저장되므로 클로저가 계속 접근 할 수 있다.

 

1.2 힙(Heap)

이전 포스트, 객체 시리즈를 작성할때 "객체는 동적으로 변경되기 때문에 힙이라는 곳에 별도로 저장된다."라고 작성하였다. 

이 힙은 실행컨텍스트가 실행되면서 참조되는 객체들이 저장되어있는 메모리 공간으로 이 힙은 객체가 동적으로 변경되기 때문에 구조화되어있지 않다.

 

1.3 큐(Queue)

스택에서 설명했던것처럼 비동기로 실행되는 함수의 콜백함수, 이벤트 핸들러가 보관되는 영역이다.

 

2.이벤트 루프의 문제점 해결해보기

 

위의 코드처럼 비동기함수가 원하는 시간에 실행되지않는것은 치명적인 문제를 일으킬 수 있다.

위의 코드를 개발자의 의도대로 실행되도록 하려면 단순하게 10만번의 for문이 끝날때까지 기다리는것이 아니라, 5만번을 쪼개어 실행하면 이벤트루프의 스택이 비었을때 원하는 비동기함수가 실행할 수 있다. 

 

const s = new Date().getSeconds();

var changeQueue = (function() {
  var list = [];

  return {
    enqueue: function(c) { // 실행될 함수를 배열에 추가한다.
      list.push(c);
    },

    dequeue: function() { //실행될 함수를 제거한다.
      return list.shift();
    },

    isEmpty: function() { //배열이 비어있는지 확인
      return list.length === 0;
    }
  }
})();

function delay() {
  for (let i = 0; i < 50000; i++) { 
    changeQueue.enqueue(function() {
       console.log(i);
    })
  }

  setInterval(function() {
    for (var i = 0; i < 30 && !changeQueue.isEmpty(); i++) {
      var c = changeQueue.dequeue();

      if (c)
        c();

      if (changeQueue.isEmpty())
        console.timeEnd();
    }
  }, 0); 
}

function baz() {
    console.log((new Date().getSeconds() - s) + "초 후 실행됨");
}

setTimeout(baz, 1000); // (1)
delay();

 

위의 코드를 실행하면

 

1
2
3
...
1초 후 실행됨
... 
49998
49999
50000

 

위와 같은 로그를 볼 수 있다.

 

 

출처 : https://developer.mozilla.org/ko/docs/Web/JavaScript/EventLoop

댓글