나는 오늘도 멋있다

JavaScript의 동작 - 1 (stack,heap,Event Loop) 본문

Web/CS

JavaScript의 동작 - 1 (stack,heap,Event Loop)

나는 오늘도 멋있다 2024. 3. 27. 18:45
1. 모든행동에는 목표를 정하고, 근거야 있어야 한다.
2. 문제가 발생하면 해결보다는 원인을 찾아
3. 다른사람의 관점을 고려하자
4. 알고자 하는 것의 정의는 무엇인가
5. 알고자 하는 것의 의도는 무엇인가

 

목적
클로저의 개념을 보다보니, 실행 컨텍스트 부터 렉시컬 환경을 보게되었고
이전부터 많이 봤던 단어들이다. 확실하게는 알고 있지 않아 확실하게 알고자 글들을 찾아보다가
문득 JS의 동작을 전체적으로 아는것이 좋다고 판단을 하였고,
내가 쓰는 언어의 동작은 알아야 되지않나? 라는 생각을 가지게 되어 JS의 동작을 주제로 글을 적어본다.

JavaScript의 V8의 메모리구조

JavaScript의 Google V8 엔진
V8엔진은 JavaScript를 실행하기 위해 단일 프로세스를 사용한다. JavaScript는 주로 단일 스레드로 동작하며,  컨텍스트의 실행을 담당하기 위해 단일 프로세스를 사용한다. JavaScript의 컨텍스트가 동시에 실행되는 경우에도, 각 컨텍스트는 별도의 V8 프로스세 내에서 동작한다. (V8엔진은 JavaScript 뿐아니라 , chrome, node.js 등 여러곳에서 사용되고 있다.)

https://perfectacle.github.io/2017/02/09/C-ref-004/

 

(C/C++) 참고용 정리 - 메모리 영역(Code, Data, Stack, Heap)

프로그램을 실행하게 되면 OS는 메모리(RAM)에 공간을 할당해준다.할당해주는 메모리 공간은 4가지(Code, Data, Stack, Heap)으로 나눌 수 있다. 이미지 출처: C언어의 메모리 구조 Code

perfectacle.github.io

V8엔진의 대표적인 메모리 구조

Heap Memory
1.객체나 동적 데이터를 저장하는 곳이다.
2.메모리 영역 중 가장 큰 블럭으로 GC(Garbage Collection)가 이루어지는 곳이다.
3.heap에는 참조주소가 아닌 객체의 실제값이 저장되는 곳이다.
4.동적데이터는 프로그램 실행중에 동적으로 생성되고 해제되는 데이터를 말한다.
5.Heap에는 여러 메모리 영역이 있는데 이중에서 New Space 와 Old Space만 GC가 이루어 진다

Stack
1. V8프로세스 당 하나의 스택이 존재하고, 메서드/함수 프레임, 원시 값, 객체 포인터를 포함한 정적 데이터가 저장된다.
2. 전역적인 값은 전역 프레임으로 Stack에 유지된다.
3. 전역 프레임 안에는 일반적으로 하나의 전역 컨텍스트가 존재한다.
4. 함수 호출이 발생하면 전역 프레임위에 별도의 함수프레임이 쌓인다.
5. 함수 프레임 안의 정보들(인수, 반환값, 지역변수 등)은 해당 함수 프레임내에 저장된다.
6. 만약 현재 함수 안에서 호출된 함수는 또하나의 함수프레임을 생성하여 Stack의 맨위에 푸시 된다. 이후 함수가 반환되면 해당 프레임은 스택에서 제거된다.
7. 함수안에서 생성된 지역변수가 참조타입 일경우,  Heap에값이 저장되고, 함수가 종료될때 Stack의 쌓여 있던 포인터만이 스택프레임과 제거됨
8. 원시타입들은 포인터가 없기때문에 Stack내에 값으로 직접 저장된다. 
9. 스택은 V8자체가 아닌 OS에 의해 관리된다.

https://speakerdeck.com/deepu105/v8-memory-usage-stack-and-heap

 

V8 Memory usage(Stack & Heap)

 

speakerdeck.com

참고한 내용의 사람정보

 


JavaScript의 런타임 환경

 

JavaScript의 런타임
JavaScript는 싱글 스레드 프로그래밍 언어이다. 하나의 Call Stack 만을 가지고 있다는 말인데...
즉 하나의 프로그램은 하나의 작업만 할 수 있다는 것이다. 이로 인해 작업이 오래 걸리는 작업을 진행한다면 어떻게 될까?
예를 들면 일반적인 네트워크 요청 처럼 말이다. 이러한 동작이 Call Stack에서 이루어진다면 요청하는 동안 다른 코드를 실행 할 수가 없다. 이것을 blocking이라고 말한다. 그렇다면 blocking이 된다면 어떤 현상들이 일어날까? 내가 작성한 코드는 물론 브라우저의 렌더링 또한 발생하지 않는다. 즉, 해당작업을 처리하기위해 다음코드가 실행되지 않고, 브라우저도 멈추는 것이다. 이또한 그냥 멈추는 것이아닌 멈춘동안의 사용자의 상호작용한 행동을 기억하고 있지만 렌더링이 발생하지 않게 된다. 만약 사용자가 Click Evenet를 통해 웹에서 alert를 출력하는 코드가 있다고 한다면? 사용자는 눌렀을때 아무것도 뜨지않기때문에, 계속 누르게 될것이고 이를 브라우저는 기억하고 나중에 한번에 쏟아 낼것이다. 이러한점은 UX적으로도 분명 좋지가 않다.  그렇기에 Non-blocking되어야 UX적으로 좋은것이다. 그렇다면 Non-blocking는 어떻게 해야 가능한 걸까? 사실 알게모르게 사용하고 있는 비동기처리가 해결을 해주고 있다. 여기서 나는 새로운 사실을 알게 되었는데. JavaScript 비동기 개념이 없다. 예를 들면 AJAX 처럼 말이다. 즉 JavaScript의 V8은 DOM, HTTP요청 등을 포함하지 않는건데. 이는 브라우저에서 지원하는 webAPI로써 브라우저가 처리하는 것이다. 또한 이와관련하여 Non-blocking을 유지하기 위해 Evenet Loop 와 Queue 가 존재한다.  밑에 간단한 예시를 보자
// 1
console.log("first");

//2
setTimeout(()=>{
	console.log("wait");
},5000)

//3
console.log("last");


해당 코드를 실행하면 당연히 "first", "last", "wait"으로 나오는것을 알수 있다. 그렇다면 5초가아닌 0초로 한다면?
바로 실행되는것이 아닌가? 라는 생각을 하지만 동작은 그렇게 이루어 지지 않는다. 이유가 뭘까? setTimeout은 JS 런타임 환경에 존재하는 별도의 API다. 그렇기에 다르게 동작하는것인데 과정은 이렇다.

1. first 실행되고 stack에 쌓여서 log를 반환한다.
2. setTimeout이 실행되어 stack에 쌓인후 webAPI영역으로 옴겨 브라우저의 webAPI가 처리를 시작한다.
3. setTimeout은 stack에서 사라지고, 브라우저의 webAPI는 초에 따라 함수를 딜레이 시켰다가 넘겨받은 함수를 실행한다.
3. last가 실행되고 stack에 쌓여서 log를 반환한다.
4. webAPI에서 처리가 완료된 callback은 task queue에 밀어 넣는다
5. Evenet Loop가 동작하여 Call Stack이 비어있는지를 확인한다.
6. 비어있다면, task queue의 call back을 Call Stack에 밀어 넣는다.

위의 동작 과정처럼 setTimeout의 초가 0 초든 N초든 상관이없다. 해당 초는 브라우저가 처리하기전의 딜레이시키는 초로써
작업이 완료 되었을경우에, Task queue에 전달 된기때문에 출력과정은 변하지가 않는다.  해당 과정을 이해하고 난후에 또다른 과정도 예상할수가 있다.

setTimeout(()=>{
	console.log("1");
},1000);

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

setTimeout(()=>{
	console.log("3");
},1000);


위의 코드처럼 1초로 설정한다고 하여 동시에 출력될까? 이또한 그렇지도 않다. JavaScript는 싱글 스레드이고 하나의 작업만을 처리한다. 그렇다면 Task queue에 쌓인것을 Evenet Loop가 하나씩 Call stack에 넘기기때문에 1초에 3개가 출력될수 없는것이다.
또한 setTimeout의 초는 최소한의 초를 지정하는것이기 때문에 그이상이 걸릴수도 있다는 말이다.  또한 모든 webAPI는 작동이 완료되면 콜백을 task queue에 밀어 넣고, 처리가 다되었다고 해서 Stack에 바로 넣을수가없다. Evenet Loop는 Call Stack이 비어있는 것을 확인해야 하기 때문이다. 여기서 내가 오해한것은 또하나가 있다. 그렇다면 동기적으로 실행되는 코드가있고. 작업이 일찍 끝난 비동기가 동기적으로 실행되는 코드에서 CallStack이 비어있는틈에 실행될수 없는걸까? 라고... 당연히 말이안되는 소리이다.  해당 처럼 예상한다면 무슨 게임도아니고 동작을 더 예측할수가 없다. Stack이 비어있다는것은 동기적인 코드가 끝난것을 말하는거기 때문이다. (webAPI는 각각의 싱글 스레드로 동작한다 ~) 즉 비동기 작업은 스택의 마지막까지 지연시킨다로 이해하면 될것같다.

Task queue(MarcoTask Queue) vs MicroTask Queue
Task queue는 기본적으로 브라우저 관련 비동기처리를 하는데 사용한다. 그렇기에 setTimeout, setInterval, fetch 처럼 비동기로 처리되는 콜백 함수가 들어가는 큐이다. 위에서는 Task queue에대한 언급만 하였다. 그렇다면 Microtask queue는 어떤걸까? 
Promise를 처리하는 곳이다.  Mircrotask queue는 우선순위가 높다. 그렇기에 Task queue 보다 먼저 실행된다.(렌더링보다도 먼저실행된다고한다)

console.log('Start');

// Task Queue에 콜백 함수 추가
setTimeout(() => {
  console.log("Task Queue");
}, 0);

// Microtask Queue에 콜백 함수 추가
Promise.resolve().then(() => {
  console.log("Microtask Queue");
});


console.log('End');

만약 Microtask Queue가 Task Queue보다 우선 순위가 없다면 순서는 예상한대로 흘러가지만, 해당 코드를 실행해보면 "Task Queue"가 제일 마지막에 실행되는 것을 알수가 있다. 볼게 별로 없는 코드지만 간단한 테스트로도 EvenetLoop가 Microtask Queue를 먼저 CallStack에 넣는것을 알수가 있다. (브라우저보다 먼저 실행되는 것은 따로 테스트 해봐야 겠다)
이렇게 Task Queue와 MircoTask Queue를 Callback Queue라고 부르는데 비동기 관련 Queue이기 때문이다.


사용된용어들
* Call Stack: 함수 호출 및 실행의 순서를 관리하여 코드의 실행 흐름을 제어하는 메커니즘이다. 
* blocking:  Call Stack이 다른 작업을 처리하지 못하고 대기하는 상태
* Non-blocking: Call Stack의 작업이 막히지 않는 상태
* Evenet Loop: 브라우저의 동작 타이밍을 제어하는 관리자로써 Callstack이 비어있을때 Callback Queue의 작업을 호출스택으로 이동시킨다.
* Queue: 데이터를 일시적으로 저장하는 자료구조
* Task Queue(Marcotask queue): 브라우저 환경에서 비동기 작업을 처리하는 큐(setTimeout, setinterval, fecth, addeventlistener)
* Callback Queue: Task, Microtask 를 포괄하는 개념으로 주로 비동기 작업에 대한 통칭하는 용어
* Microtask Queue: Promise의 콜백 함수들이 대기하는 큐, Marcotask Queue보다 우선순위를 가진다 (axios, async/await)
* Promise: 비동기 작업의 결과를 나타내는 객체로써 pending(대기), fulfilled(이행), rejected(거부)가 존재한다

 

https://www.youtube.com/watch?v=8aGhZQkoFbQ

 

[JavaScript 작동방식 - 엔진, 런타임 및 호출 스택] = Alexander Zlatkov

https://medium.com/sessionstack-blog/how-does-javascript-actually-work-part-1-b0bacc073cf 

 

[JavaScript의 Event Loop & Call Stack] = Felix Gerschau

https://felixgerschau.com/javascript-event-loop-call-stack/

 

[JavaScript의 메모리 관리 설명] = Felix Gerschau

https://felixgerschau.com/javascript-memory-management/

 

[JavaScript의 동작방식 & Event Loop] = philip Roberts

https://vimeo.com/96425312

 

[JavaScript의 Event Loop] = philip Roberts

https://www.youtube.com/watch?v=8aGhZQkoFbQ&t=80s

 

[JavaScript의 콜스택 보기]
http://latentflip.com/loupe/?code=JC5vbignYnV0dG9uJywgJ2NsaWNrJywgZnVuY3Rpb24gb25DbGljaygpIHsKICAgIHNldFRpbWVvdXQoZnVuY3Rpb24gdGltZXIoKSB7CiAgICAgICAgY29uc29sZS5sb2coJ1lvdSBjbGlja2VkIHRoZSBidXR0b24hJyk7ICAgIAogICAgfSwgMjAwMCk7Cn0pOwoKY29uc29sZS5sb2coIkhpISIpOwoKc2V0VGltZW91dChmdW5jdGlvbiB0aW1lb3V0KCkgewogICAgY29uc29sZS5sb2coIkNsaWNrIHRoZSBidXR0b24hIik7Cn0sIDUwMDApOwoKY29uc29sZS5sb2coIldlbGNvbWUgdG8gbG91cGUuIik7!!!PGJ1dHRvbj5DbGljayBtZSE8L2J1dHRvbj4%3D

 

[JavaScript의 Stack & Heap]

https://speakerdeck.com/deepu105/v8-memory-usage-stack-and-heap

 

V8 Memory usage(Stack & Heap)

 

speakerdeck.com

 


JavaScript가 어떤식으로 동작하는지 전체적인 동작을 짐작할수 있게 되었다. 여러 글들을 보고, 테스트 해보고 하면서 좀더 JS에 가까워진 느낌이다. 하지만 아직 의문이 가는점이 많지만 코드를 작성하면서 예상한대로 흘러가지 않는다면, 내용을 추가할 예정이다.  그래도 비동기, EventLoop, 싱글 스레드인데 멀티스레드 처럼 동작할수 있었던거지 등등 말이다.

이제는 원래 알아보려고 했던 실행 컨텍스트를 조사해보자

 

'Web > CS' 카테고리의 다른 글

브라우저의 동작 원리  (0) 2024.04.04
함수형 프로그래밍  (0) 2024.03.26
컴퓨터 부품조사  (2) 2024.01.11
CORS(Cross-Origin Resource Sharing)에 대해서  (1) 2023.12.06