본문 바로가기
Javascript/Javascript

JavaScript 동작원리와 Event Loop, Queue, Web API

by img 2023. 5. 26.

몇 달 전 자바스크립트의 콜스택, 힙, 가비지 컬렉터 등 관련 블로그 글을 퍼블리싱했었는데, 좀 더 깊게 자바스크립트 작동원리에 대해서 공부해 새롭게 글을 작성합니다. 중복된 내용이 있을 수 있습니다!

예전 글 ) https://imagineu.tistory.com/76

 

[Javascript] 콜스택(Call Stack), 메모리힙(Memory Heap), 가비지컬렉션, 메모리릭 그림으로 이해하기

Call Stack과 Heap 시작하기에 앞서 하나의 스레드 당 하나의 스택메모리를 사용하는데, 자바스크립트는 싱글스레드 언어이기 때문에 콜스택이라는 스택메모리 하나만 사용합니다. const a = 'Hello Worl

imagineu.tistory.com

 

자바스크립트 엔진

자바스크립트는 스크립트 언어로서 자바스크립트 엔진에 의해 실행이 됩니다. 자바스크립트 엔진으로는 Rhino, SpiderMonkey 등 여러 엔진들이 있지만, 대표적으로는 Chrome과 node.js에서 사용하는 구글에서 만든 V8엔진이 있습니다. 각 엔진마다 디테일한 작동들은 다르지만, 보통 다음과 같은 과정을 거칩니다.

(1) 입력된 코드를 의미있는 작은 단위(토큰)로 나눈다.

(2) 토큰들을 추상화된 코드의 구조를 나타내는 AST(추상 구문 트리)로 변환한다.

(3) AST를 CPU가 실행할 수 있도록 0과 1만을 사용한 코드로 변환합니다.

(4) 컴파일된 코드를 실행한다.

이러한 과정을 거치며 자바스크립트 엔진이 코드를 읽어나가며 실행하게 됩니다.

 

자바스크립트 작동원리

자바스크립트를 그림으로 표현하면 다음과 같은데요. 하나하나 살펴보겠습니다.

우선 JS 박스 안의 Memory Heap과 Call Stack은 자바스크립트의 데이터들을 저장하는 메모리 영역입니다.

콜스택 (Call Stack)

콜스택은 자바스크립트가 코드를 읽어나가면서, Primitive type의 데이터와 실행컨텍스트가 스택으로 쌓이는 메모리 영역입니다. Stack이라는 이름을 보면 알 수 있듯이, FILO(First In, Last Out)의 자료구조 형태로 입출력되며, 자바스크립트가 싱글스레드라고 불리는 이유도 콜스택이 한 개이기 때문에 한가지의 일만을 수행할 수 있기 때문입니다.

실행 컨텍스트

콜스택에는 실행 컨텍스트가 쌓인다고 했는데요. 실행 컨텍스트란, 실행할 코드에 제공할 환경 정보들을 모아놓은 객체로, 실행 컨텍스트를 구성하는 가장 흔한 방법은 함수를 실행하는 것입니다. 함수를 실행하게 되면, 함수 안의 변수, 파라미터, 내부 함수 등이 실행 컨텍스트에 포함됩니다.

코드를 읽는 과정에서 동일한 환경에 있는 코드들을 실행할 때 환경 정보들을 모아 컨텍스트를 구성하고, 이것을 Call Stack에 쌓아 올렸다가, 가장 위에 쌓여 있는 컨텍스트와 관련있는 코드들을 실행하는 식으로 전체 코드의 환경 순서를 보장합니다.

콜스택에 메모리가 입출력되는 과정은 다음과 같습니다.

(1) 함수를 실행하면, 실행 컨텍스트가 생성되고, 이를 콜스택에 추가한 다음 함수를 수행합니다.

(2) 함수에 의해 호출되는 내부 함수들도 그 위 콜스택에 쌓여 올려지고, 해당 위치에서 실행됩니다.

(3) 함수의 실행이 종료되면, 해당 실행 컨텍스트를 콜 스택에서 꺼낸(pop)후, 다시 남은 실행 컨텍스트를 실행합니다.

(4) 만약 실행 컨텍스트가 할당된 콜 스택보다 많은 공간을 차지하면 “Maximum call stack size” 에러가 발생합니다.

 

메모리 힙 (Memory Heap)

메모리 힙은 객체, 배열, 함수와 같이 동적으로 변할 수 있는 Reference Type 데이터들을 저장하는 구조화되지 않은 메모리 영역 입니다.

let myArr = [1,2,3]

하지만 myArr라는 배열이 모두 Memory Heap에 저장되는 것이 아니라, myArr라는 식별자는 콜스택에 쌓이는데, 이 때 [1,2,3]이라는 데이터 자체는 Heap영역에 저장이 되어있고, 해당 주소를 콜 스택에서 관리합니다.

myArr = { name: "사실 난 객체야" }

만약 myArr가 이런식으로 데이터가 변경이 된다면, heap 메모리 어딘가에 새로운 객체를 다시 만들고, myArr가 가르키고 있는 주소값을 변경해주는 형태로 데이터를 변경합니다. 그러면 이제 어디서도 참조되고 있지 않은 [1,2,3] 배열데이터는 자바스크립트의 가비지 컬렉터에 의해 삭제됩니다. 자바스크립트는 가비지컬렉터가 자동으로 메모리 관리를 해줘서 메모리 관리를 완벽하게 할 필요는 없다는 장점이 있지만, 쓸모없어짐과 동시에 삭제 되는 것이 아닌 가비지 컬렉터가 동작할 때에 삭제되기 때문에 최적의 메모리 관리를 할 수 없다는 단점이 있습니다.

또한 메모리 힙은 콜스택 영역보다 훨씬 더 큰 공간을 가지고 있지만, 그것이 힙영역이 무한하다는 이야기가 아닙니다. 메모리 힙이 부주의나 일부 프로그램의 오류로 인해 제대로 관리되지 않았을 경우 자동 또는 수동으로 메모리가 해제되지 않아 데이터들이 메모리 공간의 범위를 넘어설 수 있는데, 그것을 메모리 누수라고 합니다. 과거에 사용되었지만, 메모리 힙에서 제거되지 않고 계속 차지하고 있는 현상을 의미합니다.

Call stack과 Memory Heap이 데이터를 저장하는 방식을 그림으로 보면 다음과 같습니다.

변수에 담긴 데이터가 Primitive type이라면 해당 Primitive type 데이터의 Call stack 주소를 변수가 갖고 있습니다.

하지만 만약 데이터가 Reference type이라면 해당 Reference type 데이터는 Heap 메모리 어딘가에 저장되어 있고, 그 데이터가 저장되어 있는 Heap memory 주소를 해당 변수가 가르키고 있는 Call stack의 주소에 갖고 있게 됩니다.

쉽게 말하면 Primitive type이었다면 해당 값을 갖고 있을 자리에, Heap의 위치정보를 갖고 있는 것이라고 보면 됩니다.

 

Event Loop & Queue

앞서 말했듯이, 하나의 콜스택에서는 하나의 실행 컨텍스트만 처리할 수 있기 때문에, 어떤 실행 컨텍스트의 실행이 오래 걸린다면, 실행이 마무리 될 때까지 브라우저는 아무런 동작을 할 수 없게 되고, 사용자의 인터랙션을 처리할 수도 없게 됩니다.

그렇게 된다면, 많은 기능들을 제공하는 웹사이트는 존재할 수 없었을 것입니다. 하지만 브라우저는 서버에 데이터를 요청해서 받아오는 동시에, 사용자의 인터랙션도 처리하는 등 브라우저의 구성요소와 함께 동시에 많은 작업을 할 수 있습니다. 싱글 스레드인 자바스크립트에서 동시에 많은 작업을 할 수 있는 이유는 자바스크립트가 이벤트 루프에 기반한 동시성 모델을 가지고 있기 때문입니다.

이벤트 루프는 콜 스택과 각 Queue들을 보고 있다가, 콜스택이 비었을 경우 이벤트 발생으로 생긴 콜백함수들을 queue에서 하나씩 꺼내 콜 스택에 추가해주는 자바스크립트 내에서 끊임없이 돌아가는 루프입니다. 콜스택에 추가해주는 이런 반복적인 작업을 틱(Tick)이라고 부릅니다. callback queue를 비롯한 각 Queue들은 콜스택으로 들어가기 위한 “대기열”이라고 볼 수 있습니다. 우리가 어떤 웹사이트를 켜놓기만 하고 있을 땐 이벤트루프는 아무런 일도 하지 않고 있다가 어떤 인터랙션을 주었을 때 활성화되고, 활성화 되지 않은 경우에는 이벤트 루프의 CPU 자원 소비는 0에 가까워지게됩니다.

이벤트 루프는 다음의 단계를 거치며 각 Queue들의 작업들을 Call Stack에 추가해줍니다. (V8)

  1. 콜스택의 작업이 모두 처리되었을 경우 Microtask Queue에 처리해야할 작업이 있다면 콜 스택에 넣고 처리합니다.
    Microtask Queue는 ES6에서 도입된 컨셉으로, Callback Queue와 동일 계층에 존재하며 Promise를 통한 비동기 요청 시의 콜백함수가 여기에서 대기합니다. ex) Promise.then()
  2. Microtask Queue도 모두 처리되었을 경우 Animation Frames를 확인해 처리해야할 작업이 있다면 콜 스택에 넣고 처리합니다.
    Animation Frames는 requestAnimationFrame API의 콜백함수가 대기하는 자료구조입니다.
  3. Animation Frames도 모두 처리되었을 경우 Callback Queue를 확인하고 마찬가지로 콜 스택에 넣고 처리합니다. (Macro queue)
    Callback Queue는 비동기 함수가 실행된 후 콜백함수가 대기하는 자료구조입니다. 대표적으로, 외부 스크립트 실행 <script=””>나 event발생, setTimeout 등이 여기에 해당됩니다.

정리하면, Microtask Queue → Animation Frames → Callback Queue 의 순서로 비동기 함수의 콜백 함수를 처리하게 됩니다.

그리고 이벤트 루프를 막을 우려가 있는 무거운 연산은 Web Worker를 사용해 병렬적으로 처리할 수도 있습니다.

Web API

Web API는 자바스크립트 엔진이 아닌 브라우저에서 제공하는 API로, DOM 요소나 타이머, HTTP 요청(Ajax) 등의 기능을 제공합니다.

Web API를 사용하는 코드는 JavaScript 엔진과는 별개로 실행되며, 실행이 완료되면 앞서 살펴본 Callback Queue에 결과를 넣어줍니다. 그러면 Callback Queue의 작업을 Event Loop가 감지해 앞서 말한 Event Loop의 작업 단계를 거치며 콜스택으로 넘어와 실행됩니다.

예를 들어, setTimeout 함수는 브라우저의 Web API 중 하나입니다. setTimeout 함수에 전달한 콜백 함수는 Web API에서 실행되며, 타이머가 만료되면 이벤트 큐에 콜백 함수 실행 결과를 넣어줍니다. 이벤트 루프는 이벤트 큐에 있는 콜백 함수를 순서대로 가져와 실행하며, 이를 통해 setTimeout 함수의 콜백 함수가 실행되고 그 결과가 JavaScript 엔진으로 전달됩니다.


console.log("시작")

setTimeout(() =>{
	console.log("timeout")
},0);

Promise
	.resolve('Promise')
	.then(res=> console.log(res)

console.log("끝!")

ㅇㅖ시로 위와 같은 코드가 있을 때, 

콘솔에는 

  1. 시작
  2. timeout
  3. Promise

의 순서로 출력이 되게 됩니다. 

댓글