Skip to content

Commit 95f3d7c

Browse files
committed
docs: JavaScript 객체 생성 방식 및 V8 엔진 최적화에 대한 게시글 초안 추가
1 parent b208065 commit 95f3d7c

2 files changed

Lines changed: 353 additions & 0 deletions

File tree

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
---
2+
title: 객체리터럴 vs 정적메서드 vs 클래스 인스턴스 vs 클로저함수
3+
createdAt: 2025-05-08
4+
category: JavaScript
5+
description: 객체리터럴, 정적메서드, 클래스 인스턴스, 클로저함수를 사용하면 공통적으로 객체를 생성할 수 있습니다. 이들은 모두 객체를 생성하는 방법이지만, 각각의 특징과 장단점이 다릅니다. 이 글에서는 이 네 가지 방법을 비교하고, 각각의 장단점과 사용 예시를 살펴보겠습니다.
6+
---
7+
8+
::: danger
9+
작성중인 게시글 또는 초안입니다.
10+
:::
11+
12+
# 🤔 객체리터럴 vs 정적메서드 vs 클래스 인스턴스 vs 클로저함수
13+
14+
자바스크립트는 객체리터럴, 정적메서드, 클래스 인스턴스, 클로저함수를 사용하여 객체를 생성할 수 있습니다. <br/>
15+
각각의 방식은 V8 엔진에서 최적화 되는 방식 (Ignition 인터프리터, TurboFan JIT 컴파일러 등) 과, 동작방식이 다릅니다. <br/>
16+
그렇다면 각각의 방식은 내부적으로 어떻게 동작하며, 성능적으로는 어떤 차이가 있을까요 ?
17+
18+
## 🚘 V8 엔진의 최적화 방식 Inline Caching 과 Hidden Class
19+
20+
각각의 객체 방식의 차이점을 알아보기 전에, V8 엔진이 JavaScript 코드를 어떻게 최적화 하는지 알아보겠습니다. <br/>
21+
22+
### 1. Inline Caching
23+
24+
V8 엔진은 JavaScript 코드를 바이트코드 형태로 변환하고, 실행하는 **Ignition 인터프리터** 를 먼저 거칩니다.
25+
26+
함수가 반복적으로 호출될때, Ignition 인터프리터는 해당 함수의 타입 정보를 수집합니다. <br/>
27+
함수가 일정 횟수 이상 호출되면, V8 엔진은 **TurboFan JIT 컴파일러** 를 사용하여 수집된 타입을 기반으로 최적화된 머신코드를 생성합니다 <br/>
28+
29+
V8 엔진의 핵심 최적화 중 하나는 **Inline Caching** 입니다. <br/>
30+
동일한 연산이 반복될때, 동일한 타입이 사용된다고 가정하고 최적화 하는 방식입니다.
31+
32+
```js
33+
function add(a, b) {
34+
return a + b;
35+
}
36+
```
37+
38+
예를 들어, 다음과 같은 함수가 있고, 함수가 호출될 때 `a`, `b` 타입이 숫자 타입으로 호출된다고 가정할때 <br/>
39+
V8 엔진은 `a``b`의 타입을 숫자 타입으로 가정하고, 최적화된 머신코드를 생성합니다. <br/>
40+
41+
### 2. Hidden Class
42+
43+
V8 엔진은 객체를 실행 중 빠르게 접근하고 속성을 바꾸기 위해서, 객체를 내부적으로 하나의 클래스처럼 저장합니다. <br/>
44+
45+
```js
46+
let obj1 = {};
47+
obj1.a = "A";
48+
obj1.b = "B";
49+
50+
let obj2 = {};
51+
obj2.b = "B";
52+
obj2.a = "A";
53+
```
54+
55+
예를 들어, 다음과 같은 코드가 있을때 <br/>
56+
`obj1``obj2` 는 서로 같은 프로퍼티 이름을 가지고 있지만, 순서가 다르기 때문에 <br/>
57+
서로 다른 **Hidden Class** 를 생성합니다. <br/>
58+
59+
<center>
60+
<img src="./img/js-object-1.png" width={800} />
61+
</center>
62+
63+
```js
64+
let obj3 = {};
65+
obj3.a = 1;
66+
obj3.b = 2;
67+
```
68+
69+
반면, `obj3``obj1` 과 동일한 프로퍼티 이름과 순서를 가지고 있기 때문에 <br/>
70+
캐싱된 **Hidden Class** 를 사용하여, 성능을 최적화 합니다. <br/>
71+
72+
## 😎 객체 생성 방식 비교
73+
74+
### 1. 객체 리터럴 (Object Literal)
75+
76+
```js
77+
const me = {
78+
name: "DaeGeon",
79+
age: 26,
80+
greet: function () {
81+
console.log(`Hello, my name is ${this.name}`);
82+
},
83+
};
84+
```
85+
86+
객체 리터럴 (Object Literal)은 `{}` 중괄호를 사용하여 객체를 생성하고 <br/>
87+
이 방식은 지정된 프로퍼티와 메서드를 한번에 정의하고 **런타임**에 새로운 객체를 생성합니다.
88+
89+
코드 내부에서 객체 리터럴(Object Literal)을 사용하면, **새로운 객체가 할당** 되며, **각각의 프로퍼티와 메서드가 초기화** 됩니다. <br/>
90+
91+
앞서 말했던것과 같이, V8 엔진은 이런 객체에 대해 프로퍼티 구성에 따라 **Hidden Class** 를 생성합니다.
92+
93+
같은 프로퍼티 이름과 순서로 정의된 객체 리터럴은, 첫번째 생성 이후 동일한 (캐싱된) **Hidden Class** 를 사용합니다. <br/>
94+
95+
하지만, 나중에 새로운 프로퍼티를 추가하거나, 객체 생성시 프로퍼티의 구성이 조금이라도 다르면 Hidden Class 가 달라지고, 성능이 저하될 수 있습니다.
96+
97+
특히 객체 리터럴 내부에 **메서드** 가 포함되어 있는 경우, 객체를 생성할 때 마다 **새로운 함수 객체** 가 생성됩니다.
98+
99+
이런 반복적인 함수 객체 생성을 피하기 위해서는, 메서드를 공유하는 방식인 **Prototype 또는 Class** 를 사용하는 것이 좋습니다. <br/>
100+
101+
### 💻 객체 리터럴 사용 사례
102+
103+
1️⃣ 유틸리티 함수
104+
105+
유틸리티 함수를 통해 객체 리터럴을 반복해서 생성하는 경우는 드물지만, 하나의 객체 리터럴을 모듈화 하여 사용하는 경우는 많습니다. <br/>
106+
107+
```js
108+
const MathUtils = {
109+
add: function (a, b) {
110+
return a + b;
111+
},
112+
subtract: function (a, b) {
113+
return a - b;
114+
},
115+
};
116+
```
117+
118+
이런 경우 객체는 한번만 생성되고, 그 안의 함수 프로퍼티들은 유틸리티 메서드로 사용됩니다 <br/>
119+
성능은 독립적인 함수 호출과 동일합니다 (객체는 한번만 생성되고, 메서드 호출시 정적인 객체에서 프로퍼티 조회만 하기 때문)
120+
121+
2️⃣ 간단한 데이터를 담는 객체
122+
123+
```js
124+
const person = {
125+
name: "DaeGeon",
126+
age: 26,
127+
};
128+
```
129+
130+
객체 리터럴은 간단한 데이터를 담는 객체를 생성할 때 유용합니다. <br/>
131+
코드가 간단하고 객체가 몇개 안되는경우 성능상의 문제는 없습니다.
132+
133+
비슷한 구조의 객체를 많이 사용하는 경우, 각 객체는 독립적이지만 **프로퍼티 이름과 순서가 같다면** Hidden Class 를 공유하고 V8 엔진의 최적화를 받을 수 있습니다.
134+
135+
하지만, 객체 리터럴에 **메서드**를 포함시키는 경우, 객체가 만들어질 때 마다 새로운 함수 객체가 생성되므로, **메모리 사용량이 증가하고, 메서드 호출시 CPU 사용량이 증가** 할 가능성이 있습니다 <br/>
136+
이런 경우는 앞서 말했던것과 같이 Prototype 또는 Class 를 사용하는 것이 좋습니다. <br/>
137+
138+
### 💡 정리
139+
140+
| ✅ 장점 | 내용 |
141+
| ------------------ | ---------------------------------------------------------------------------------- |
142+
| 단순한 문법 | 클래스나 생성자 없이 `{}`만으로 객체 생성 가능 |
143+
| 빠른 생성 성능 | 속성이 적은 경우 V8에서 매우 빠르게 생성됨 |
144+
| 유연한 구조 | 런타임에 속성 추가/삭제가 쉬움 (단, 히든 클래스 변경시 성능 고려해야함) |
145+
| 단기 데이터에 적합 | 짧게 쓰이고 폐기되는 데이터 구조에 효율적 (설정객체, 단순데이터, 일회성 객체 등..) |
146+
147+
<br />
148+
149+
| ❌ 단점 | 내용 |
150+
| ----------------------- | --------------------------------------------------------------------------- |
151+
| 메서드 중복 생성 | 객체마다 메서드가 새로 만들어져 메모리/CPU 낭비 발생 가능성 있음 |
152+
| 성능 저하 가능 | 구조가 일관되지 않으면 히든 클래스가 분기되어 Inline Caching 비효율 |
153+
| 공유 로직 어렵고 반복됨 | 프로토타입 체이닝이 없어 로직 재사용이 불편함 |
154+
| 인라인 캐싱 저하 가능성 | 다양한 구조의 객체를 전달할 경우 Inline Caching 최적화가 불가능해질 수 있음 |
155+
156+
## 2. 정적 메서드 (Static Method)
157+
158+
```js
159+
class Math {
160+
static add(a, b) {
161+
return a + b;
162+
}
163+
static subtract(a, b) {
164+
return a - b;
165+
}
166+
}
167+
```
168+
169+
정적 메서드는 클래스 내부에 static member 로 함수를 정의하는 방식입니다. <br/>
170+
해당함수는 클래스의 인스턴스가 아닌, 클래스 자체에 속합니다.
171+
172+
정적 메서드는 Execution Context 의 **Execution Phase 에서 한번만 생성**됩니다. <br/>
173+
(Creation Phase 에서는 `class` 는 호이스팅되어 **Lexical Environment** 에 등록되고, TDZ 상태)<br/>
174+
175+
static member 들은 **생성자 함수의 객체 자체 프로퍼티**로 저장됩니다. <br/>
176+
177+
> ‼️ static member 는 클래스의 인스턴스가 아닌, 클래스 자체에 속한다?
178+
>
179+
> 위 코드에서, `Math.add` 와 같이 클래스 이름을 통해 접근할 수 있는 반면, <br/> `Math.prototype.add` 와 같이 프로토타입 체인을 통해 인스턴스에서 접근할 수 없습니다. <br/>
180+
> 이는 static member 가 클래스의 인스턴스가 아닌, 클래스 자체에 속하기 때문입니다. <br/>
181+
182+
V8 엔진 내부적으로 위 예제에서 `Math` 객체는 `add``subtract` 를 고정된 순서, 위치로 포함하는 **Hidden Class** 를 생성합니다. <br/>
183+
이를 통해 Inline Caching 최적화가 가능합니다
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
---
2+
title: "동적 타이핑 언어 JavaScript 값은 실제로 어떻게 저장될까? (Feat. Tagged Pointer, NaN-Boxing)"
3+
createdAt: 2025-07-02
4+
category: JavaScript
5+
description: JavaScript 에서는 숫자, 문자열, 불리언, 객체 등 다양한 값을 다룰 수 있습니다. 일반적인으로는 원시값은 스택에, 나머지는 힙에 저장된다고 합니다. 하지만 동적 타입 언어인 JavaScript 는 런타임에 타입이 결정되는데, 그렇다면 실제로 값은 어디에 또 어떻게 저장될까요 ?
6+
---
7+
8+
::: danger
9+
작성중인 게시글 또는 초안입니다.
10+
:::
11+
12+
# 🤔 JavaScript 값은 어디에 저장되나? (힙 or 스택)
13+
14+
JavaScript에서는 숫자, 문자열, 불리언, 객체 등 다양한 값을 다룰 수 있습니다.
15+
일반적인 언어 지식에서는 "원시 값은 스택에 저장되고, 객체는 힙에 저장된다"고 배웁니다.
16+
17+
하지만 스택에는 고정된 크기의 메모리에 데이터를 저장하게되는데, <br />
18+
그렇다면 런타임에 타입이 결정되는 JavaScript 에서는, <br />
19+
크기도 타입도 다양한 값들을 어떻게 고정된 메모리에 저장할 수 있을까요? <br />
20+
21+
또 값을 읽을 때는 어떻게 타입을 구분할까요 ?
22+
23+
이를 이해하기 위해서는 JavaScript 엔진의 메모리 관리 방식을 알아야 합니다.
24+
25+
## 💾 실제로는 JavaScript 의 모든 값은 '힙메모리' 에 저장됩니다
26+
27+
V8 엔진에서는 숫자, 문자열, 객체를 포함해 **모든 값이 힙 메모리에 저장** 됩니다
28+
29+
이를 통해 어떤 값이든 **객체 처럼** 다뤄지고, Garbage Collector (GC) 가 메모리를 효율적으로 관리할 수 있습니다.
30+
31+
> **❓ JavaScript 의 원시값은 스택에 저장되는거 아닌가요?** <br/>
32+
>
33+
> JavaScript 언어 관점에서 원시값은 스택에 저장되고, 객체 배열 등 참조값은 힙에 저장됩니다 <br/>
34+
>
35+
> 여기서 모든 값이 힙에 저장된다는것은 V8 엔진 관점에서의 이야기입니다. <br/>
36+
>
37+
> _정리하자면, JS 의 모든 값은 **객체처럼** 다루어 지며, <br/>
38+
> V8 엔진 입장에서는 어떤 값이든 HeapObject 로 취급될 수 있도록 설계되어있다는 의미입니다 <br/>
39+
> (즉, 값을 '동일한 인터페이스로 다룰 수 있도록' Tagging 구조로 표현한다는 뜻)_
40+
41+
## 🏷️ Tagged Value
42+
43+
V8 엔진에서는 값을 저장할 때, **타입 정보(Tag Bits)** 를 함께 저장합니다.
44+
45+
64비트 슬롯의 **하위 2비트 (LSB, Least Significant Bit)** 를 사용하여 값을 구분합니다.
46+
47+
```cpp
48+
// v8/objects/objects.h
49+
50+
// Tagged<T> represents an uncompressed V8 tagged pointer.
51+
//
52+
// The tagged pointer is a pointer-sized value with a tag in the LSB. The value
53+
// is either:
54+
//
55+
// * A small integer (Smi), shifted right, with the tag set to 0
56+
// * A strong pointer to an object on the V8 heap, with the tag set to 01
57+
// * A weak pointer to an object on the V8 heap, with the tag set to 11
58+
// * A cleared weak pointer, with the value 11
59+
```
60+
61+
하위 비트만 확인하면 별도의 타입 정보를 찾지 않고도, 해당 값이 정수(SMI)인지, Heap Object 를 가리키는 포인터인지 즉시 알 수 있습니다.
62+
63+
| LSB 값 | 설명 |
64+
| ------ | ------------------------ |
65+
| 00 | SMI (Small Integer) |
66+
| 01 | Strong Ref (Heap Object) |
67+
| 10 | Weak Ref (Heap Object) |
68+
69+
## 🤔 Tag Bits 의 역할
70+
71+
`Tag Bits` 는 두가지의 역할을 합니다.
72+
73+
> **1. V8 힙에 있는 객체를 가리키는 포인터인지 (강한참조 / 약한 참조) 구분**
74+
>
75+
> **2. 혹은 작은 정수 (SMI, Small Integer) 인지 구분**
76+
77+
이 덕분에 V8은
78+
79+
작은 정수 (SMI) 는 별도의 Heap Object 를 생성하지 않고, 64비트 스택 슬롯에 직접 값을 저장할 수 있습니다.
80+
81+
> **❓ 어떻게 가능한데 ??** <br/>
82+
> 별도의 V8 실제 힙공간을 할당하지 않고, 스택 슬롯에 바로 31비트 정수 + 태그 비트를 넣어서 표현할 수 있습니다. <br/>
83+
84+
### ⌨️ 실제 코드 예시
85+
86+
> **❗️ JavaScript 관점의 힙메모리** <br/>
87+
> JS에서 말하는 힙은 객체나 배열처럼 참조 타입이 저장되는 공간을 말합니다. <br/>
88+
>
89+
> **❗️ V8엔진 관점의 힙메모리** <br/>
90+
> 실제로 JS 에서 선언되는 모든 값은 V8 엔진의 힙에 저장됩니다. <br/>
91+
> V8 엔진에 의해 GC (Garbage Collector) 가 관리되는 물리적인 힙 영역을 말합니다
92+
93+
> V8 엔진은 항상 힙에 객체를 word 단위 (word-aligned) 의 주소에 할당합니다. <br/>
94+
> 64비트 시스템에서는 8바이트 단위로 할당되며, 32비트 시스템에서는 4바이트 단위로 할당됩니다.
95+
96+
실제로 v8 엔진에서는 다음과 같이 `kHeapObjectTag`, `kWeakHeapObjectTag`, `kHeapObjectTagSize` 를 정의하고 있습니다.
97+
98+
```cpp
99+
// v8/include/v8-internal.h
100+
101+
// Tag information for HeapObject.
102+
const int kHeapObjectTag = 1; // 01
103+
const int kWeakHeapObjectTag = 3; // 11
104+
const int kHeapObjectTagSize = 2; // 10
105+
106+
// Tag information for Smi.
107+
const int kSmiTag = 0;
108+
const int kSmiTagSize = 1;
109+
const intptr_t kSmiTagMask = (1 << kSmiTagSize) - 1;
110+
```
111+
112+
## 💡 SMI (Small Integer)
113+
114+
하지만, 숫자와 같은 작은 값들을 매번 객체로 만들면 성능이 떨어집니다.
115+
116+
V8 은 SMI(Small Integer) 라는 방법을 사용해 작은 정수는 힙 객체를 만들지 않고, **스택 슬롯에 직접 저장** 합니다.
117+
118+
<img src="./img/v8-pointer-compression-1.png" alt="V8 Pointer Compression" width={400} />
119+
120+
정수 값, SMI (Small Integer) 는 왼쪽으로 Shift 한 뒤, 하위 2비트 (LSB)를 `00` 으로 두고 "이건 SMI야!" 라는 태그를 붙입니다. <br />
121+
122+
<br />
123+
124+
V8 엔진 내에서 JavaScript의 정수도 객체처럼 다뤄야 합니다.
125+
126+
하지만 정수 값이 변경될 때 마다 매번 새로운 객체를 만들면 성능이 떨어집니다.
127+
128+
그래서 V8은 **SMI (Small Integer)** 라는 트릭을 씁니다. <br/>
129+
정수값을 별도의 객체 없이, **64비트 스택 슬롯에 직접 넣는 방식**입니다. <br/>
130+
그리고 그게 정수인지 포인터인지 구분하기 위해 **하위 LSB 2비트를 Tag Bits** 로 사용합니다.
131+
132+
```cpp
133+
V8_INLINE static constexpr int SmiToInt(Address value) {
134+
int shift_bits = kSmiTagSize + kSmiShiftSize;
135+
// Shift down and throw away top 32 bits.
136+
return static_cast<int>(static_cast<intptr_t>(value) >> shift_bits);
137+
}
138+
```
139+
140+
정리하면, LSB 1비트는 SMI vs HeapObject 를 구분하는데 사용되고, <br />
141+
LSB 2비트는 Strong Ref vs Weak Ref 를 구분하는데 사용됩니다.
142+
143+
## 👀 예를들어, 아래와 같은 코드가 있다고 해보겠습니다.
144+
145+
<center>
146+
<img src="./img/v8-pointer-compression-2.png" alt="V8 Pointer Compression" width={800} />
147+
</center>
148+
149+
### 🧠 JavaScript 관점에서는...
150+
151+
- `a = 10` 은 원시 타입이기 때문에, 변수 `a` 자체에 값 10이 직접 저장됩니다.
152+
- 반면 `b = {name : "김대건"}` 은 객체 타입이기 때문에, 변수 `b` 는 실제 객체의 "참조 (주소)" 만 스택에 저장되고, 객체 자체는 힙(Heap) 에 저장됩니다.
153+
154+
### 🧠 V8 엔진의 관점에서는...
155+
156+
- V8 엔진은 내부적으로 모든 값을 Tagged Value 포맷으로 저장합니다.
157+
- `a = 10` 의 경우 SMI (Small Integer) 를 힙에 객체로 만들지 않고, 64비트 슬롯 안에 직접 저장합니다.
158+
- `10` 은 내부적으로 좌측으로 2비트 shift 한 뒤, 하위 2비트 (LSB)를 `00` 으로 두고 "이건 SMI야!" 라는 태그를 붙입니다.
159+
- `b = {name : "김대건"}` 의 경우, 객체는 메모리상의 힙에 새롭게 할당됩니다.
160+
- `b` 에는 객체가 저장된 주소 `0x12` 를 좌측으로 2비트 shift 한 뒤, 하위 2비트 (LSB)를 `01` 로 두고 "이건 Strong Ref로 가리키는 Heap Object 야!" 라는 태그를 붙입니다.
161+
162+
<br />
163+
<br />
164+
165+
ㅇㅇ
166+
167+
# 🔗 참고 자료
168+
169+
- [V8 - Pointer Compression](https://v8.dev/blog/pointer-compression)
170+
- [NodeJS - Understanding and Tuning Memory](https://nodejs.org/en/learn/diagnostics/memory/understanding-and-tuning-memory)

0 commit comments

Comments
 (0)