자바스크립트 동작 원리와 비동기 처리의 원리 (Event Loop, Task Queue)

JS (TIL & Remind)·2022년 2월 21일
4

자바스크립트 엔진

자바스크립트 엔진은 대표적으로 Google V8 엔진이 있다. Chrome과 Node.js 에서 사용하고 있다.

자바스크립트 엔진은 가능한 빠르게 최적화된 코드를 생성하기 위해 전통적으로는 인터프리터 방식으로 구현되지만, 특정한 방식으로 바이트코드로 JIT(Just-In-Time) 컴파일을 할 수도 있다.

JIT (Just-In-Time) 란?
번역한 기계어를 저장해놨다가 필요할 때 다시 꺼내 쓰는 방식

인터프리터 란?
고급 언어로 작성된 프로그램을 한 줄 단위로 번역하고, 동시에 즉시 실행시키는 프로그램

Memory Heap

메모리 할당이 일어나는 곳, 자바스크립트는 자동으로 메모리를 할당하고 불필요하다고 판단되면 메모리를 해제한다. 이를 가비지 컬렉션이라고 한다.

Call stack

코드 실행에 따라 호출 스택이 쌓이는 곳.

Call Stack(호출 스택)

자바스크립트는 싱글 스레드 기반 언어이기때문에, 하나의 Call Stack만을 가지고 있다. 따라서, 한 번에 하나의 Task만 수행할 수 있다. 즉, 하나의 함수가 실행되면 이 함수의 실행이 끝날 때 까지 다른 Task가 중간에 끼어들지 못한다.

Call Stack은 LIFO(Last In First Out) 구조로, 스택에 쌓인 함수를 가장 마지막에 들어온 것부터 처리하며, 해당 함수의 실행이 끝나면 Call Stack에서 제거한다. 예제와 표를 보면 더 이해하기 쉽다.

const bar = () => {
	console.log('bar');
}

const foo = () => {
	bar();
	console.log('foo');
}

foo();

// 결과
// bar
// foo

위의 코드가 실행되면 호출 스택의 단계는 다음과 같이 변한다.

Step 1Step 2Step 3Step 4Step 5Step 6
console.log(’bar’)
bar()bar()bar()console.log(’foo’)
foo()foo()foo()foo()foo()foo()

자바스크립트 런타임

자바스크립트는 자바스크립트 엔진 외에도 관여하는 요소들이 있다. 자바스크립트 *런타임은 자바스크립트 엔진을 포함해 Web API와 Task Queue, Event Loop 등의 요소들이 있으며 각 다음의 역할을 한다.

런타임이란?
특정 언어를 실행할 수 있는 환경을 말한다.
자바스크립트 런타임은 자바스크립트가 구동되는 환경을 말하며, 브라우저와 Node.js가 있다.

Web API

브라우저에서 제공되는 API.

DOM(Document Object Model), AJAX(Asynchronous Javascript And XML), setTimeout 등과 같은 자바스크립트 엔진에서 정의되지 않은 메소드를 지원한다.

Callback Queue

이벤트 발생 후 호출되어야 할 콜백 함수들이 기다리는 공간.

Event Loop

Event Loop는 현재 실행중인 Task가 없는지, Callback Queue에 Task가 있는지를 반복적으로 확인하여, 현재 실행중인 Task가 없을 때(Call Stack이 비워졌을 때) Callback Queue의 Task를 Call Stack으로 이동시킨 후 함수를 실행시킨다.

자바스크립트의 비동기 처리

위에서 말했듯이 자바스크립트는 싱글 스레드 기반 언어이기 때문에, 한 번에 하나의 Task만 실행시킬 수 있다. 이런 상황에서 자바스크립트는 Promise, async, setTimeout과 같은 *비동기 함수를 런타임의 Callback Queue와 Event Loop를 통해 처리한다.

비동기 처리란?
자바스크립트의 비동기 처리란 특정 코드의 연산이 끝날 때까지 코드의 실행을 멈추지 않고 다음 코드를 먼저 실행하는 특성.

Callback Queue의 구조와 비동기 처리 과정

Callback Queue 내부는 다음과 같이 구성 되어 있다.

MicroStack Queue (Job Queue)

Promise, async/await와 같은 비동기 호출의 콜백 함수가 담겨있다.

MicroStack Queue에 등록 된 모든 콜백 함수가 처리될 때까지 계속 수행하는것이 특징이다.

function loop() {
  function infinityThen() {
    Promise.resolve().then(infinityThen);
  }
  Promise.resolve().then(infinityThen);
}

loop();

Animation Frames

사용자가 스크롤을 이동하거나, 요소를 클릭하는 등 화면을 갱신해야 될 때,(requestAnimationFrames api를 사용했을 때)와 같이 브라우저 렌더링과 관련된 Task를 받는 Queue이다.

MacroStack Queue (Task Queue)

setTimeout, setInterval, onClick과 같은 비동기 호출의 콜백 함수가 담겨있다.

MicroStack Queue와 다르게 콜백 함수를 하나씩 실행한다. 즉, 콜백 함수 하나를 실행하면 이벤트 루프를 놓아주어 다른 동작을 수행할 수 있도록 한다.

function loop() {
    function infinitySetTimeout() {
        setTimeout(infinitySetTimeout, 0);
    }

	infinitySetTimeout();
    
}

loop();

Task 우선 순위

만약 코드에서 Promise, requestAnimationFrame, setTimeout을 모두 실행시키면, 이벤트 루프가 각 Queue에 있는 Task들은 다음과 같은 순서로 처리하도록 할 것이다.

  1. 처음에 Call Stack에 쌓여있는 Task를 모두 처리.
  2. MicroStack Queue에 쌓여있는 Task를 모두 처리.
  3. Animation Frames에 쌓여있는 Task를 처리.
  4. MacroStack Queue에 쌓여있는 Task를 하나씩 처리.

MicroStack Queue → Animation Frames → MacroStack Queue

console.log('start');

setTimeout(() => {
	console.log('setTimeout Callback');
}, 0);

Promise.resolve().then(() => {
	console.log('promise then 1');
}).then(() => {
	console.log('promise then 2');
});

requestAnimationFrame(() => {
	console.log('requestAnimationFrame Callback');
});

console.log('end');

// 결과
// start
// end
// promise then 1
// promise then 2
// requestAnimationFrame Callback
// setTimeout Callback

비동기 처리 과정을 그림으로 본다면

console.log('Start!');
setTimeout(() => {
	console.log('Timeout!');
}, 0);
Promise.resolve('Promise!')
	.then(res => console.log(res));
console.log('End!');

// 결과
// Start!
// End!
// Promise!
// Timeout!

위 코드의 처리 과정을 그림으로 본다면 다음과 같다.

CALL STACK에서 console.log()가 실행 된 후 제거된다.

setTimeout()이 CALL STACK에서 실행되고 콜백 함수가 WEB API로 이동한다.

setTimeout의 인자로 넘긴 시간이 지나면 콜백함수가 WEB API에서 MACROTASK QUEUE로 이동한다.


코드가 모두 실행되고 CALL STACK이 비워지면 EVENT LOOP가 QUEUE의 TASK들을 CALL STACK으로 옮긴다.

profile
노션에 더욱 깔끔하게 정리되어있습니다. (하단 좌측의 홈 모양 아이콘)

0개의 댓글