JavaScript Event Loop
JavaScript의 Event Loop 살펴보기
  • NodeJS

Event loop

JavaScript는 메인쓰레드겸 싱글쓰레드로서 비즈니스 로직을 수행한다.
모든 비즈니스 로직은 메인쓰레드해서 실행되고 심지어는 GC조차 이벤트루프를 통해 수행되기 때문에 while등의 함수에서 빠져나오지 못한다면 메모리가 뻗는 현상까지 발생할 수 있다.
이 모든 건 JavaScript가 싱글쓰레드와 Event loop를 통해 동작하고 있기 때문이다.

JavaScript의 Event loop에 대해 정리해 보고자 한다.

JavaScript에서 Call Stack에서의 Event loop 의 동작원리

간단한 코드를 실행해보자.

image

위 코드를 살펴보면 1을 출력 후 1초 쉬고 2를 출력하고 3을 출력한다.
JavaScript가 아닌 언어에서 살펴본다면 1이 출력되고 1초 후 2가 출력되고 마지막으로 3이 출력될 것이다.

그래서 결과값은 아래와 같다.

1
(1초 delay)
2
3

하지만 JavaScript에서의 결과값은 이와 같지 않다.

1
3
(1초 delay)
2

왜 그럴까?

순서대로 그림을 통해 살펴보자.

image

JavaScript에는 작업을 Call Stack에서 수행하게 된다.

image

제일먼저 console.log('1')이 실행을 위해 Call Stack으로 들어온다.

그러면 console에서는 1을 확인할 수 있다.

image

다음으로 setTimeout(() => { console.log('2') })를 실행하려고 한다.

image

하지만 JavaScript는 setTimeout은 즉시 실행하지 않고 Task Queue로 옮기게 된다.

참고로 즉시 실행되지 않고 task queue에 담겼다가 실행되는 함수는 다음과 같다.

  1. web component events ex) 클릭 이벤트
  2. XMLHttpRequest (ajax 를 사용할 때 호출되는 api)
  3. setTimeout

image

setTimeout 에 1초라는 delay를 주었기 때문에 즉시 실행되지 않고 background 대기열로 우선 이동한다.

image

그 다음 console.log('3')이 Call Stack으로 들어오게 되고

image

3이 출력되는것을 볼 수 있다.

image

다음으로 1초가 지나 대기열에 있던 setTimeout이 실행가능한 시간이 된다면 대기열에 있던 작업은 Task Queue로 이동한다.

image

Task Queue에 있는 작업은 Event loop에 의해 Call Stack로 올려지게 되는데,
이 때 중요한 점은 Call Stack에 아무작업이 없이 비어있을 때에만 이동이 가능하다.

image

그러면 Call Stack에 작업이 비어있는것을 확인한 Evnet loop는 Call Stack으로 console.log('3') 을 올려준다.

image

그러면 Call Stack은 작업을 실행하고

image

마지막으로 2가 출력되는것을 볼 수 있다.

JavaScript는 이와같이 동작하기 때문에 다른 언어와 달리 코스의 순서가 곧 출력되는 순서가 아닐 수 있다.

바로 실행되는 setTimeout 0초

image

위와같은 코드가 있다.

앞에서 살펴본 예제와 동일하지만 차이는 setTimeout에 delay로 0초를 주었다.
(setTimeout의 default delay값은 0므로 생략 가능하다.)

위 코드를 보면 0초 뒤, 즉 바로 실행될거라 예상된다.
결과값을 살펴보자.

image

하지만 예상과는 달리 위에서 1초 뒤 실행하는 결과랑 동일하다.
원인은 아무리 0초 뒤에 실행하게 코드를 작성하였지만, setTimeout은 즉시 실행하는 함수가 아닌 Task Queue로 옮겨지고, Call Stack이 모두 비어졌을 때 실행가능한 함수이다.
따라서 1,3이 출력된 후 Call Stack이 모두 비고나면 2가 출력되는 것이다.

image

JavaScript에서 Micro Task Queue

Micro Task Queue라는것에 대해 살펴보자.

간단한 코드를 보자.

image

이번엔 setTimeout와 Promise 함수를 작성해보았다.
setTimeout은 timer 이벤트를 발생시켜서 인자로 넘겨주는 값만큼 시간을 기다렸다가 실행한다. 위에서 설명했듯이 인자가 없다면 기본값은 0이다.

그리고 Promise는 resolved를 통해 빈 결과값이 반환될 것이고 then을 통해 2를 출력하는 코드이다.

이 2줄의 코드는 어떻게 실행될까? 이번에도 한 단계씩 살펴보자.

image

제일 먼저 코드의 순서에따라 setTimeout이 Call Stack로 들어온다.

image

위에서 설명한 것처럼 setTimeout은 즉시 실행함수가 아니기 때문에 background 대기열로 이동한다.

image

그리고 다음으로는 Promise 함수가 Call Stack으로 들어온다.

image

이로써 대기열에는 2개의 함수가 쌓이게 되고, 두 함수는 각각 실행가능한 시점이 되면 위에서 살펴본 바와 같이 Task Queue로 이동할 것이다.

image

먼저 setTimeout의 코드가 Task Queue로 이동하고

image

Promise의 코드가 Task Queue로 이동한다.
그런데 여기서 차이가 조금 있다. Promise의 경우 Task Queue가 아닌 Micro Task Queue로 이동한다.

둘의 차이는 무었일까?

Task Queue vs Micro Task Queue

  • Task Queue

브라우저 혹은 그 외 구동 환경에서 순차적으로 실행되어야 하는 작업이다. 단순한 스크립트나 setTimeout, dom event, ajax 호출등이 있다.

  • Micro Task Queue

지금 실행중인 작업 바로 다음에 실행되어야 할 작업이다. 즉 바로 다음 실행되기 때문에 Task보다 높은 우선순위를 가진다.
Promise, MutationObserver, process.nextTick등이 있다.

따라서 setTimeout, Promise가 동시에 실행할 수 있는 상황이 되었더라도, Micro Task로 이동하는 Promise는 setTimeout에 비해 높은 실행 우선순위를 가진다.

image

그래서 Call Stack에는 Promise의 then으로 실행된 console.log('2')가 먼저 들어오게 된다.

image

console에는 2가 먼저 출력되고

image

다음으로 Task Queue에 있는 console.log('1')이 Call Stack에 쌓이고

image

1의 출력이 실행된다.

image

console에 찍힌 모습을 보면 2, 1 순으로 출력되는것을 볼 수 있다.

결론

  • Task는 항상 순차적으로 실행되기 때문에 어떠한 Task가 수행중이라면 다른 Task가 실행될 수 있다. (싱글 쓰레드이다)
  • Micro Task는 Task보다 높은 우선순위를 가진다.
  • setTimeout은 즉시 실행되는 함수가 아닌 Task Queue에 담겼다가 실행되는 함수이기 때문에 1초 delay를 주었다고 하더라도,

이 1초는 최소 1초뒤 실행은 보장하지만 정확히 1초 뒤 실행을 보장하지는 않는다. (앞의 Task가 끝나야 실행되기 때문)

참고