[33-js-concepts] 9. 메시지 큐와 이벤트 루프 (Message Queue and Event Loop)

메시지 큐 & 이벤트 루프

기계어를 제외한 모든 언어는 컴파일 과정이 필요하다. 프로그래머가 작성한 코드라고 불리는 문자열은 컴파일러에게  줄씩 읽혀서 나름의 규칙에 의해 분해되고, 트리 형태로 변환되고, 어셈블리어를 거쳐 기계가 알아먹을  있는 2진수로 변환되어야 비로소 작동 가능한 상태가 된다.

 

자바스크립트(이하 js) 비롯한 스크립트언어는 다소 독특한 방식으로 컴파일을 진행하는데, 프로그램을 모두 읽어서 분석하는 과정을 거치는게 아니라, 코드를  줄씩 읽어 바로 실행시킨다. 이런 방식의 해석을  주는 머신을 인터프리터 라고 하는데, 보통 C C++ 같은 고급 언어로 작성 되어 있다. 인터프리터는 스크립트 언어로 작성된 코드를 분석하여 중간 단계의 고급 언어로 변환하여 최종적으로 기계어로 변환한다.

 

 방식은 바로 어셈블리어를 거쳐 기계어로 변환되는 컴파일 방식에 비해 느리다고 생각될 수도 있는데, 다르게 생각해보면 고급 언어로 돌리는 프로그램을 즉시 실행할  있다는 장점이 있다. 10GB 크기의 소스를 실행시켜야 한다고   컴파일 언어는 모든 컴파일이 끝날때 까지 한참 기다려야 하지만, 인터프리터는 즉시 실행하여 동작하는 프로그램을   있다. 그러므로 인터프리터는 소스코드만 존재하고 컴파일이 완료된 프로그램이 생성되지 않는다.

 

인터프리터는 크게 2가지 종류로 나눌  있다. 컴파일과 인터프리팅을 혼합한 방식과 소스코드를 그대로 유지하고 읽어서 해석하는 방식이 있다.

 

 

컴파일과 인터프리팅을 혼합한 방식은 대표적으로 Java 사용하는 방식인데, 자바는 .java 파일을 (사람이 문자열로 작성한 코드가 담긴 파일) JVM 해석할  있는 ByteCode 변환한다. 이때 파일은 .class 파일로 변환되고 프로그램이 동작되기 시작하면, JVM  ByteCode  줄씩 기계어로 변환하면서 실행한다.

 

소스코드를 그대로 유지하고 읽어들이는 방식은 js 사용하는 방식인데, 실제로 그대로 유지하는 것은 아니고 성능 향상을 위해서 실행시점에서 소스코드를 네이티브 코드( 변환되는 고급언어 )  변환하는데 이런 기법을 JIT (Just-in-time) 기법이라고 한다. js 또한 소스코드를 일부분씩 바이트코드로 변환하는 방식을 사용한다.

 

JIT(Just-in-time) : 프로그램을 실제 실행하는 시점에서 기계어로 변역하는 방식

 

JIT 방식도 2가지가 있다.

- Method JIT : 메소드 단위로 프로그램을 실행시킬 때 마다 기계어로 번역한다.

- Tracing JIT : 인터프리터가 자주 실행하는 부분만 기계어로 변환한다.

 

 내용에 대해  알아보고 싶은 사람은  밑의 링크를 참고하자.

 

위의 자바도 소스코드가 모두 ByteCode 변환된 이후 부터는 JIT 방식으로 해석된다.

https://huns.me/development/360

 

js엔진은 여러가지 최적화 기법을 사용하며, Tracing JIT 기법의 경우 크롬에 적용된 V8엔진에서도 사용하고 있다.  최신의 js엔진들은 하나의 방식만을 사용하는 것이 아니라 상황에 맞는 최적화 기법을 바꿔가면서 사용한다

https://velog.io/@godori/JavaScript-engine-1 

 

 

사실  주제와 벗어나는 js 컴파일 방식을 이렇게나 많이 설명한 것은  한가지를 강조하기 위한 것이다.

 

 

JS 코드는 동시에 실행되지 않으며,  번에  명령씩 수행된다!

그런데 js 작동되는 모습을 보면 무언가 동시에 수행되는거 같다. 특히 콜백 함수가 실행되는 모습을 보면 마치 multi thread 사용해서 대기하고 있는것 같기도 하다.

 

그런데 js single thread 기반의 언어이다.

 

만약  조건대로라면, 용량이  파일을 받는 등의  시간이 걸리는 작업을 한다면  뒤의 작업은 아무것도  일어나야 한다.

 

아니 당장 for문을 1억번 정도 돌려본다면 브라우저는 하얀 화면을 띄우고 반복문이 종료될  까지 멍청하게 기다릴것이다.

 

하지만 js 비동기(asynchronous) 동작하며 콜백을 사용하여 Non-blocking 하게 프로그래밍 한다면 이런 제약없이 매우 효율적이고 빠르게 동작할  있다.

 

이런 js 비동기 - 논블록킹 프로그래밍을 가능하게  주는 것이 바로 메시지큐와 이벤트루프 모델이다.

- Synchronous - Asynchronous / Blocking - Non-Blocking  대하여   알아보자

 

동기 (Synchronous)  비동기(Asynchronous)  차이는 함수의 실행 결과를 어디서 처리하느냐 따라 다르다

 

동기 : 함수를 부르고, 실행결과를 함수가 호출한 곳에서 받아서 처리해야한다.

비동기 : 함수를 부르고, 실행결과를 함수를 호출한 곳에서 받아서 처리할 필요가 없다. 보통 콜백함수에서 처리하는 경우가 많다.

 

 

동기 예시

function add(a, b) {
  return a + b;
}

const result = add(3, 5);
console.log(result); // 8

 

비동기 예시

function add(a, b) {
  return a + b;
}

let result;
setTimeout(() => {
  result = add(3, 5)
}, 2000);

console.log(result); // undefined

 

 

블록킹(Blocking) 논블록킹(Non-Blocking)  차이는 값의 리턴 시점, 함수의 제어권이다.

 

블록킹 : 제어권이 호출한 함수에게 넘어가서 값의 리턴이 함수가  실행되고나서 이루어지며, 값과 함께 다시 제어권이 넘어온다.

논블록킹 : 제어권은 계속 호출한 함수에 있고 값의 리턴이 함수의 실행과 동시에 이루어진다.

 

 

블록킹 예시

function infinite() {
  while (true) {}
  return true;
}

var token = infinite();
if (token) console.log("here");

 예시의 경우 메인 실행 함수에서 infinite 함수를 호출했는데,  경우 “here” 이라는 출력은   없다. 왜냐하면 infinite 함수에 제어권이 넘어가서 무한루프가 종료되지 않는  값을 반환하지 않기 때문이다.

 

 

논블록킹 예시

setTimeout(() => {
  while (true) {};
}, 0);

console.log("here");

 

 예시의 경우 “here” 이라는 출력값을   있는데 함수가 호출되고 나서도 제어권은 계속 메인 실행함수에 있으며, setTimeout 함수를 수행과 동시에 값이 리턴되며, infinite함수는 콜백 함수로 실행되기 때문에 "here"  출력된다. 물론  콜백함수가 수행되면 계속 진행은 안되겠지만 말이다.

 

 자세한 설명은 여기를 참고하자.

 

 

https://homoefficio.github.io/2017/02/19/Blocking-NonBlocking-Synchronous-Asynchronous/ 

https://victorydntmd.tistory.com/8 

 

 

Message Queue & Event Loop

JS 엔진의 아키텍쳐는 크게 2부분으로 나누어지는데.

 

Heap Stack으로 구성되어 있다.

Heap 객체가 할당되어 존재하는 영역이며,

Stack 호출되는 함수가 저장되며 (자료구조 스택)

 

 

그리고 브라우저 엔진에 Message Queue 있는데

Message Queue에는 비동기로 처리될 콜백함수들이 저장된다. (자료구조 )

 

Message Queue Event Queue 같은 용어이다.

 

실제 JS 엔진은 단일 스택과 힙이 끝이며, 나머지는 모두  JS엔진을 실행시키는 환경에 포함되어 있다.  환경은 브라우저나 Node.js 같은 환경을 뜻한다. (https://meetup.toast.com/posts/89)

 

 

위에 보이는 Web API 같은건 브라우저가 C 혹은 C++ 작성된 API 이며, js 엔진에 구현되어 있는 것이 아니다.

console.log("Hello 1");

setTimeout(() => {
  console.log("Hello 2");
}, 2000);

console.log("Hello 3");

 

 

 

 코드가 어떻게 동작하는지 알아보자.

console.log  동기적으로 동작하는 함수이므로 먼저 Call Stack 들어간다.

 

 

이제 console.log(“Hello1”) pop 되면서 실행되고 콘솔에 찍힌다.

  

 

 다음은 setTimeout 함수가 push 된다.  함수는 Web API 중에서 Timer 함수를 호출한다

( 알아보고 싶은 사람은 https://www.w3.org/TR/2011/WD-html5-20110525/timers.html 여기를 참고)

 

 

 Timer 함수는 정해진 시간동안 특정 함수의 실행을 지연 시키거나 반복해서 실행 시킬  있게 해주는 함수다.

이때 callback 인자로 받는데, setTimeout 에서는 지연이 끝나면  콜백 함수를 실행시킨다

Timer 함수는 백그라운드에서 실행되는데,  말은 브라우저가 백그라운드로  작업을 수행하는 것을 뜻한다

이때 브라우저는 고급 언어로 작성된 멀티 스레드 프로그램이므로, 별도의 스레드를 생성하여 작업을 수행할  있다

 

 

 

 

setTimeout 함수가 실행되었으므로 다음 코드인 console.log(“Hello3”)  Call Stack push된다.

 

 

그리고 pop 되면서 실행되고, 콘솔에 Hello3 찍힌다.

 

2초가 지나서 Timer 함수가 종료되면서 콜백 함수를 Task Queue 넣는다.  콜백함수는 조용히 대기하다가 EventLoop 의해서 Call Stack 추가가 된다. Web API 직접 Call Stack 접근할  없고, 오직 Task Queue에만 접근   있다.

익명함수를 수행하면 console.log(“Hello2”) 다시 CallStack 추가된다.

 

console.log(“Hello2”) pop 되어지고 콘솔에 찍힌다. 그리고 익명함수가 종료되면서 CallStack 비워지게 된다.

 

그럼 언제 Task Queue 있는 함수가 EventLoop 의해서 Call Stack 추가가 될까?

 

바로 Call Stack 비었을 이다.

 

이벤트 루프는 Task Queue 콜백 함수가 들어오기를 계속 대기하고 있다가, 콜백 함수가 들어오고 Call Stack 비워졌음을 확인하면 Call Stack 작업  개를 올려보낸다.

 

 

다음은 MDN 사이트에 나와있는 이벤트 루프 구현 예시이다.

while (queue.waitForMessage()) {
  queue.processNextMessage();
}

 

이벤트 루프의 1회전을 tick 이라고 하는데, Node.js에서는 process.nextTick() 함수를 사용하여 이벤트 루프를 제어할  있다.

 

process.nextTick(callback) 함수를 통해 콜백 함수를 전달하면, 다음 tick으로 넘어가기 전에 실행할  있다.

 

setTimeout(callback, 0) 함수를 사용하면 다음 tick 끝날  콜백함수가 실행된다.

 

(https://flaviocopes.com/node-process-nexttick/)

 

 

 

Call Stack 작업이 많거나, Task Queue 작업들이 가득  있으면 실행이 느려질  있으므로,

 

setTimeout 이나 setInterval 설정한 시간이 정확하게 떨어지지는 않는것도 이러한 js 구조 특성에 기인한다.

 

 

이벤트 루프의 동작은 여기서  확인할  있으니 참고하자.

(http://latentflip.com/loupe/)

 

 

Event Queue 종류와 우선순위

ES6부터 Promise 추가되고, Job Queue 개념이 추가되었다.

 

- Task : 스크립트 실행, 이벤트, HTML 파싱, 콜백 등의 작업. Task Queue 추가됌. 일반적인 작업들임

- TaskQueue : Task들을 저장하고 이벤트루프를 통해 Call Stack으로 올려보냄

 

Micro Task : 새로운 Task, 기존의 Task 영향을 받지 않고 비동기로 빠르게 수행되는 Task. 현재 실행중인 Task 바로 뒤에 일들이 쌓임.  우선순위에 따라 Task 사이에 끼어들기가 가능.

 

process.nextTick(), Promise, Object.observe, MutabtionObserver

 

 Task 끝나거나 이벤트 루프의 시작과 끝에서 체크가 된다.

 

(https://blog.javarouka.me/2016/11/12/javascript-async-promise-3/#%EC%B0%B8%EA%B3%A0)

 

Job Queue : Promise 객체가 생성되고 모두 수행된 후에, 결과에 따라 resole, reject되고 추가된 콜백이  큐에 추가된다.

 

간단한 예시를 보자.

console.log('Message no. 1: Sync');

setTimeout(function() {
  console.log('Message no. 2: setTimeout');
}, 0);

var promise = new Promise(function(resolve, reject) {
   resolve();
});

promise.then(function(resolve) {
   console.log('Message no. 3: 1st Promise');
})
.then(function(resolve) {
   console.log('Message no. 4: 2nd Promise');
});
console.log('Message no. 5: Sync');

 

 

 코드를 실행하였을때 결과는 어떻게 될까?

// Message no. 1: Sync

// Message no. 5: Sync

// Message no. 2: setTimeout

// Message no. 3: 1st Promise

// Message no. 4: 2nd Promise

 

 

이렇게 예상되겠지만 답은

// Message no. 1: Sync

// Message no. 5: Sync

// Message no. 3: 1st Promise

// Message no. 4: 2nd Promise

// Message no. 2: setTimeout

 

 

이것이다

 

Promise 수행된 후에 반환되는 콜백은 Job Queue 추가된다. Job Queue 이벤트 루프의 tick 오면 큐에 있는 모든 작업을 수행한다. 그리고  뒤에 Task 실행시킨다.

 

 부분은 브라우저 제조사 마다 약간 다르게 동작할 수도 있으니 조심해야 한다.

 

https://medium.com/@Rahulx1/understanding-event-loop-call-stack-event-job-queue-in-javascript-63dcd2c71ecd 

https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/ 

 

 

 

추가적으로 Job Queue 추가되는 Job 종류로는 Script Job Promise Job 나뉘는데 Script Job 개별 스크립트가 실행 되는 순서대로 적재되는 것이다.

 

2개의 스크립트 태그는 각각 Job Queue 등록되어 실행되게 됩니다.

 

자세한 내용은 https://www.bsidesoft.com/?p=5385 여기를 참고하세요. <— 좋은글이니 시간될  읽어보시길..

댓글(0)

Designed by JB FACTORY