
해당 글은 프론트 개발자의 기본기 다지기의 두번째 글입니다.
JS는 인터프리터 언어로 해당 코드를 읽고 컴퓨터에서 동작을 실행 할 수 있는 엔진이 여러개 존재하며 가장 많이 알려진 엔진들의 목록은 다음과 같다.
그중 현재(2021년) 가장 대중적은 v8 엔진에 대해 알아보자.
이 글을 작성할 시점은 2021년 이였다.
하지만 벨로그에 공개하는 2026년에도 크게 달라진 부분은 없다.
JavaScript는 원래는 인터프리터 언어, 그러나 요즘엔 JIT(머신 전용 언어)로 컴파일 된다.

오해금지! C#이나 C++처럼 컴파일이 된 실행파일이 배포되는 것이 아닌, JS코드를 받으면 그 자리에서 기계어로 변환 한 뒤에 돈다는 것, 따라서 C#처럼 컴파일 과정에서의 최적화가 적다. 얼른 최적화 하고 실행까지 해야 하기 때문
그럼에도 엔진들이 많이 실행되는 코드는 최적화를 진행한다.
최적화를 살펴보기 위해선 어떠한 부분이 비효율적인지를 먼저 알아야 한다.
따라서 많은 예시 중 JS의 데이터 타입에 대해 살펴보자
컴퓨터의 구조상 인터프리터든, JIT든 안에 데이터가 어떠한 타입인지는 알고 있어야 한다.
심지어 개발자가 적어주지 않았을 지라도!
따라서 JS에서 모든 변수들(심지어 함수까지!)은 자신의 타입을 가지고 있는 object 형태이다.
여기서의 object 개체는 String으로 된 키와 변수의 값을 접근 할 수 있게 되어 있는 딕셔너리다.
이때, 해당 Object는 개체 뿐만 아니라 class(property attribures)에도 적힌다.
해당 내용은 oejct와 class 그리고 array에서 다시 다루어보자

위의 이미지를 통해 모든 JS 변수들은 타입을 내부적으로 가지고 있는 것을 확인 할 수 있다. 누군가가 JS에는 타입이 없다고 하면 믿고 거르길 바란다
이젠 인터프리터(JS)일 경우 변수에 대한 것을 읽자마자 해당 값이 어떠한 타입인지 알아야 한다는 것을 인정 할 것이다. 하지만 JIT의 수준에서는 해당 로직이 어떻게 되어야 할까?

위의 이미지는 두개의 변수를 더하는 JS 코드가 JIT(ARM 기반)로 변환 되었을때의 코드이다.
단지 두 변수를 더하기 위해서도 추가적인 검사와 예외 처리가 필요할 수 밖에 없다.
따라서, JS는 C#이나 C++과 같은 언어에 비해 기본적으로 느릴수 밖는 구조를 태생적으로 가지고 있다.
하지만, 우린 항상 그래왔듯이 방법을 찾을 것이다.
해당 내용만 파고들어도 논문 몇십개가 나올 수준이니 아주 가볍게 비유를 하며 느낌만 이해하고 가보자.
위의 코드를 다시 JS로 돌려본다면 아래와 같은 모양일 것이다.
function a(){
let v1 = 0
let v2 = 1
let v3 = v1 + v2
}
하지만 아래와 같은 상황일수도 있다.
function a(){
let r = 0;
for(let index = 0; index < 1000; index++){
r += index;
}
}
만약, 전자라면 V8 엔진은 a() 실행시 v3을 더하는 과정을 한번만 실행하게 될 것이다.
이땐 방법이 없다. (일반적으론)타입 검사부터 더하기까지 모두 그냥 해야한다.
하지만 만약 후자라면? V8엔진 입장에서 첫번째 r을 더할땐 타입 검사까지 하면서 더하겠지만 두번째에 더할 때 또 타입 검사를 꼭 해야 할까?
이렇게 자주 반복되는 부분을 V8 엔진은 hotspot이라 부른다.
위와 같이 한번 봤던 코드를 또 보게 된다면 이때 엔진은 잠시 멈춘다. 이후 해당 부분을 최적화 하기 시작한다. 이번 경우에선 타입 검사를 빼는 방법을 선택할 수 있을 것이다. 처음에 최소한의 최적화를 하는 것은 JITC (baseline-JITC)라고 지징하며 너무 많이 나와서 매우 많이 최적화 되는 부분을 JITC(Optimizing-JITC)라고 지칭한다. 아래는 V8 엔진이 최적화 하는 방법을 간단히 나타낸 것이다.

이러한 방법 외에도 정말 많은 비효율적인 부분이 존재하며 그에 따른 수 많은 최적화 방법들이 연구되고 있으며 찾아보면 재밌는게 많으니 기술에 관심이 많은 개발자라면 한번 찾아보는것도 추천한다.
(참고) JIT에서 자주 사용될꺼 같은 함수나 로직들은 엔진이 미리가지고 있다가 필요할때 바로 바로 호출하기도 한다
Javascript는 단일 스레드 언어이기 때문에 JS 실행과 V8엔진의 프로세서는 1:1 매칭이 된다.
v8은 C++ 언어로 만들어져 있기때문에 C++언어를 돌리 수 있는 환경이면 v8 엔진(node)를 통하여 Javascript를 실행 할 수 있다.
v8엔진의 대략적인 구조는 다음과 같다.

다른 언어들과 비슷한 부분은 call stack 그리고 heap이 존재한다는 것이다.
하지만 조금 다른점은 Event loop부분이다.
다른 대부분의 언어들은 멀티스레딩으로 처리하는 비동기 처리가 JS에선 이벤트 루프라는 방법으로 처리되기 때문이다.
요즘엔 JS와 비슷한 방법으로 비동기 처리를 하는 언어들이 많아지고 있다.
멀티스레딩은 대부분의 개발자에게 너무나 어렵고 복잡하며 실수를 유발하기 쉬운 방식이기 때문이다.
또 구조를 보다보면 놀라게 되는 부분중 하나는 아무렇지 않게 사용하던 setTimeout은 사실 javascript 언어의 함수가 아닌 web api였다는 것, 따라서 일부 몇몇 환경에선 JS를 실행 할 수 있는 환경일지라 하여도 해당 함수가 작동하지 않는 경우가 있다.
이미지 우측 하단에 있는 (이벤트) 콜백 큐는 call stack이 전부 비어야지만 실행되게 되어 있다.
따라서 다음과 같은 상황이 펼쳐지기도 한다.
setTimeout(() => {console.log('hello')}, 1000 * 3)
// 3초 이상 뒤에
setTimeout(() => {console.log('hello')}, 1000 * 3)
while(true){}

setTimeout에서 hello를 출력하게끔 하여 2번 호출 하였지만, 밑에 있는 true는 나오지 않는다.
무한 루프라서 call stack이 비워지지 않기 때문,
따라서 다음과 같은 현상도 나타나게 된다.
function test(){
console.log('a');
function c(){
setTimeout(() => {console.log('b'), 1})
}
c();
for(let index = 0; index < 100; index++){
if(index % 10 === 0){
console.log(index);
}
}
}
test()

c()를 먼저 실행 하였고 1ms(0.001)초 뒤에 실행해라 한 뒤에 for문이 돌았기에 1ms가 분명 지났지만 중간에 나타나지 않고 call stack이 전부 끝난 뒤에 b가 출력 된다.
v8엔진의 메모리구조는 다음과 같다.

모든 개체정보 및 동적 데이터들은 전부 힙에 저장된다.
또한 GC의 관리를 받는다. (GC역시 다음번에 배워보자)
스택은 다른 언어들과 동일하게(?) 하나의 프로세스가 하나의 스택을 가지며 메서드의 주소, 개체 포인터의 값 등의 정적 데이터들이 존재한다. 전반적으로 다른 언어와 동일한 느낌.
힙 영역은 조금 복잡하다. New, Old, 라지 오브젝트, 코드, 셀, 속성 셀, 맵 영역으로 나눠지며 각각의 설명은 다음과 같다. (대충 읽어보자)
-min_semi_space_size(초기값)와 -max_semi_space_size(최대값) V8 엔진의 플래그 값을 사용해 조정할 수 있다.-initial_old_space_size(초기값)와 max_old_space_size(최대값)을 사용해 조정할 수 있다. 이 영역은 다시 2개의 영역으로 나누어진다.Cells, PropertyCells, Maps을 포함한다. 각 영역은 모두 같은 크기의 객체들을 포함하며, 어떤 종류의 객체를 참조하는지에 대한 제약이 있어서 수집을 단순하게 만든다.간단하게 정리하면 역시 또 다른 GC언어들과 비슷하게 힙 영역은 여러 부분으로 나눠져 있으며 작은 공간에 개체들을 밀어넣고 다 차면 GC가 돌면서 관리하고 살아남은 애들은 따로 관리하는 곳으로 이동 시키는 방식이다. 조금 독특한 점은 포인터(다른 개체를 포함하는 개체)와 순수 데이터(여러 데이터 '들')을 따로 관리한다는 것? 그리고 코드를 힙에 넣는 다는 것,
스택과 힙이 어떻게 작동되는지는 아래에서 확인해보자
https://speakerdeck.com/deepu105/v8-memory-usage-stack-and-heap
해당 사이트에서 확인 할 수 있는 부분은
int와 string과 같은 모든 원시 타입 값은 스택에 바로 저장된다. 이는 전역 스코프에서도 적용되며, 자바스크립트에서 문자열은 원시 타입에 해당한다.Employee와 Function과 같은 객체 타입의 값은 힙에서 생성되고 스택 포인터를 사용해 힙에서 스택을 참조한다. 함수들은 자바스크립트에서 객체이다. 전역 스코프에도 적용된다.이로써 스택 V8이 대략적으로 어떻게 코드를 읽고 어떠한 방법으로 동작하는지 대략적으로 알았다.
이제 다음 시간에는 힙 메모리에 대해 조사해보자