[코어 자바스크립트] 1. 데이터 타입
JavaScript에서 Primitive type과 Reference type이 왜 다르게 동작하는지 정확히 이해해보기
✍🏻 핵심 정리
- JavaScript에서 변수를 선언하면 메모리의 빈 공간에 식별자를 저장하고, 값은 별도의 데이터 영역에 저장된다.
- 변수에는 해당 데이터 영역의 참조가 저장된다.
- Primitive type은 값 자체를 저장하고 있으며, Reference type에는 객체가 저장되어 있고 객체 내부 프로퍼티가 다시 값을 참조한다.
데이터 타입의 종류
JavaScript의 데이터 타입은 크게 두 가지로 나뉜다.
Primitive Type
Primitive value가 저장되는 타입이다.
| 타입 | 설명 |
|---|---|
Number | 숫자 |
BigInt | 매우 큰 정수 |
String | 문자열 |
Boolean | true / false |
Symbol | 고유하고 변경 불가능한 값 (ES6~) |
null | 의도적으로 비어있음을 나타냄 |
undefined | 값이 할당되지 않음 |
Reference Type (Object)
객체를 가리키는 reference value가 저장되는 타입이다.
(Array, Function, Date, RegExp, Map, Set 등)
메모리 저장 방식
Primitive type
1
2
var a = "abc";
a = "abcdef";
변수 영역 / 데이터 영역은 정식 명칭이 아니며, 메모리의 힙/스택과도 대응되지 않음
변수 영역
| 주소 | 1002 | 1003 | 1004 |
|---|---|---|---|
| 데이터 | 이름: a 값: @5002 -> @5003 |
데이터 영역
| 주소 | 5002 | 5003 | 5004 |
|---|---|---|---|
| 데이터 | ‘abc’ | ‘abcdef’ |
- 변수 영역에서 빈 공간 확보하여 식별자를
a로 지정 (@1003) - 데이터 영역의 빈 공간에
'abc'저장 (@5002) - 1의 변수 영역(@1003)에 데이터 영역의 주소 (@5002) 저장
- 데이터 영역의 빈 공간에
'abcdef'저장 (@5003) - 1의 변수 영역(@1003)에 데이터 영역의 주소 (@5003) 업데이트
- 각각 나누어 저장하므로 데이터의 크기에 맞게 공간을 변경할 필요가 없음
- 엔진에 따라 동일한 데이터를 재사용하기도 함
Reference type
1
2
var obj = { a: 1, arr: [2, 3] };
obj.a = 4;
객체는 여러 개의 프로퍼티를 가지므로,
객체 자체는 데이터 영역에 저장되고 객체의 프로퍼티들은 별도의 영역에서 관리됨
변수 영역
| 주소 | 1001 |
|---|---|
| 데이터 | 이름: obj 값: @5001 |
데이터 영역
| 주소 | 5001 | 5002 | 5003 | 5004 | 5005 | 5006 |
|---|---|---|---|---|---|---|
| 데이터 | @7101 | 1 | @7201 | 2 | 3 | 4 |
@5001 객체 프로퍼티 영역
| 주소 | 7101 | 7102 |
|---|---|---|
| 데이터 | 이름: a 값: @5002 -> @5006 | 이름: arr 값: @5003 |
@5003 배열 프로퍼티 영역
| 주소 | 7201 | 7202 |
|---|---|---|
| 데이터 | @5004 | @5005 |
- 변수 영역에서 빈 공간 확보하여 식별자
obj생성 (@1001) - 데이터 영역에 객체 생성 (@5001)
- 객체 내부 프로퍼티를 저장할 별도의 영역 생성 (@7101)
a프로퍼티 생성 후 값1을 데이터 영역에 저장 (@5002)arr프로퍼티 생성 후 배열 객체 생성 (@5003)- 배열 내부 요소
2,3을 데이터 영역에 저장 (@5004, @5005) - 데이터 영역에
4저장 (@5006) - 객체 프로퍼티
a(@7101)가 참조하는 주소를 업데이트
Hidden Class (Shape)
실제 JS 엔진은 객체의 프로퍼티 구조를 Hidden Class(Shape)로 따로 관리한다. 같은 구조로 생성된 객체들은 Hidden Class를 공유하며, 이를 통해 프로퍼티 접근을 빠르게 최적화할 수 있다. 프로퍼티가 추가/삭제되면 Hidden Class가 변경될 수 있다.
불변성 (Immutability)
한 번 만들어진 값은 절대 변경되지 않는다는 의미이다.
Primitive type은 immutable하다.
값 자체는 변경되지 않으며, 새로운 값이 할당되면 새로운 데이터가 생성된다.
기존 값은 그대로 유지되기 때문에 여러 변수에서 안전하게 공유할 수 있다.
1
2
3
let str = "hello world";
str[0] = "H";
console.log(str); // 'hello world' ← 변경되지 않음
문자열은 immutable하기 때문에 조작해도 반영되지 않는다. 새로운 문자열을 만들어야 한다.
변수 복사 비교
1
2
3
4
5
6
7
8
var a = 10;
var b = a;
var obj1 = { c: 10, d: "d" };
var obj2 = obj1;
b = 15;
obj2.c = 20;
변수 영역
| 주소 | 1001 | 1002 | 1003 | 1004 |
|---|---|---|---|---|
| 데이터 | 이름: a 값: @5001 | 이름: b 값: @5001 -> @5004 | 이름: obj1 값: @5002 | 이름: obj2 값: @5002 |
데이터 영역
| 주소 | 5001 | 5002 | 5003 | 5004 | 5005 |
|---|---|---|---|---|---|
| 데이터 | 10 | @7101 | ‘d’ | 15 | 20 |
객체 프로퍼티 영역
| 주소 | 7101 | 7102 |
|---|---|---|
| 데이터 | 이름: c 값: @5001 -> @5005 | 이름: d 값: @5003 |
- a와 b는 서로 독립적인 값을 가지지만, obj1과 obj2는 여전히 같은 주소를 보고 있음
- obj2의 변경이 obj1에도 반영됨
하지만 객체의 경우에도 프로퍼티 변경이 아닌 새로운 객체를 할당하게 되면 (obj2 = {...})
데이터 영역에 새로운 객체가 생기고 해당 주소로 업데이트 하기 때문에 서로 다른 주소를 바라보게 된다.
불변 객체 (Immutable Object)
왜 필요할까?
1
2
3
4
5
6
7
8
9
10
11
const changeName = (user, newName) => {
const newUser = user; // 같은 참조
newUser.name = newName;
return newUser;
};
const user = { name: "Alice", gender: "Female" };
const user2 = changeName(user, "Amy");
console.log(user.name, user2.name); // Amy Amy
console.log(user === user2); // true
user2는 새로운 객체가 아니라 기존 객체의 참조를 그대로 사용한 것이기 때문에 비교가 의도대로 동작하지 않는다.
해결: 새로운 객체를 만들어 반환
1
2
3
4
5
6
7
8
const changeName = (user, newName) => {
return { ...user, name: newName }; // 새 객체 생성
};
const user2 = changeName(user, "Amy");
console.log(user.name, user2.name); // Alice Amy
console.log(user === user2); // false
얕은 복사 vs 깊은 복사
얕은 복사 (Shallow Copy)
객체의 1 depth 값만 복사한다.
primitive 값은 그대로 복사되지만, 객체 값은 참조만 복사된다.
따라서 내부 객체를 수정하면 원본도 변경된다.
깊은 복사 (Deep Copy)
객체 내부에 있는 모든 객체를 재귀적으로 복사하여 새로운 참조 구조를 생성한다.
1
2
3
4
5
// 방법 1: structuredClone
structuredClone(obj);
// 방법 2: JSON 직렬화 (함수, undefined 등은 복사 안됨)
JSON.parse(JSON.stringify(obj));
Structural Sharing
불변성을 얻기 위해 무조건 깊은 복사를 수행할 필요는 없다.
변경된 경로의 객체만 새로 생성하고 나머지는 기존 객체의 참조를 재사용하는 방식이다.
1
2
3
4
5
6
7
const newState = {
...state,
user: {
...state.user,
name: "Amy"
}
};
불변성을 유지하면서도 불필요한 깊은 복사를 피할 수 있으며, 참조 비교를 통해 변경 여부를 효율적으로 판단할 수 있다. (React의 상태 관리가 이 방식이다.)
undefined vs null
undefined는 값이 할당되지 않았음을 나타내는 값이다. JS 엔진이 자동으로 할당하는 경우는 다음과 같다.
- 값을 할당하지 않고 선언된 변수
- 객체에 존재하지 않는 프로퍼티에 접근할 때
return값이 없는 함수의 실행 결과- 배열의 존재하지 않는 index에 접근할 때
배열을 처음 만들면 요소들이
empty상태인데, 이는undefined조차 존재하지 않으며 index 프로퍼티 자체가 존재하지 않는 상태이다.
따라서 의도적으로 비어있음을 나타내기 위해서는 null을 사용하는 것이 바람직하다.
Call by Sharing
JavaScript의 함수 인자 전달 방식은 Call by Sharing이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Primitive: 값 자체가 복사되어 전달
function change(x) {
x = 10;
}
let a = 5;
change(a);
console.log(a); // 5
// Object: 참조가 복사되어 전달
function change(obj) {
obj.name = "Amy";
}
const user = { name: "Alice" };
change(user);
console.log(user.name); // Amy
// 새로운 객체 할당 → 외부에 영향 없음
function change(obj) {
obj = { name: "Amy" };
}
const user = { name: "Alice" };
change(user);
console.log(user.name); // Alice
Garbage Collection (GC)
사용하지 않는 메모리를 자동으로 정리해주는 기능이다.
Mark-and-Sweep 방식
- 현재 실행 컨텍스트에서 접근 가능한 객체를 표시 (mark)
- 접근할 수 없는 객체는 메모리에서 제거 (sweep)
객체가 더 이상 어떤 reachable object에서도 참조되지 않으면, 다음 GC 과정에서 메모리에서 해제한다.
💼 실무 연결 포인트
React에서 불변성이 중요한 이유
React는 Virtual DOM 비교 시 얕은 비교(참조 비교)를 사용하여 성능을 최적화한다. 객체를 직접 수정하면 참조가 동일해 React가 변경을 감지하지 못해 리렌더링이 일어나지 않는다. 따라서 spread 연산자나 map, filter 같은 불변 메서드로 새 객체/배열을 생성해야 한다.
🗣️ 면접 대비 Q&A
Q1. Primitive type과 Reference type의 차이는?
Primitive type은 값 자체가 메모리에 저장되고 불변하며, 복사 시 값이 독립적으로 복사됩니다. 반면 Reference type은 객체가 저장된 메모리 주소를 참조하며, 복사 시 참조만 복사되어 같은 객체를 가리킵니다. 따라서 primitive는 재할당 시 새 값이 생성되지만, 객체는 내부 프로퍼티를 직접 변경할 수 있습니다.
Q2. React에서 불변성을 지켜야 하는 이유는?
React는 Virtual DOM 비교 시 얕은 비교(참조 비교)를 사용하여 성능을 최적화합니다. 객체를 직접 수정하면 참조가 동일해 React가 변경을 감지하지 못해 리렌더링이 일어나지 않습니다. 따라서 spread 연산자나 map, filter 같은 불변 메서드로 새 객체/배열을 생성해야 합니다.
Q3. JavaScript의 함수 인자 전달 방식은?
JavaScript는 Call by Sharing 방식입니다. Primitive는 값이 복사되어 전달되고, 객체는 참조의 복사본이 전달됩니다. 따라서 함수 내부에서 객체의 프로퍼티는 변경할 수 있지만, 새로운 객체를 할당해도 외부 변수에는 영향을 주지 않습니다.
💡 최종 인사이트
- JavaScript에서 데이터를 저장하는 방식을 이해해야 버그를 예방할 수 있으며, 성능 또한 최적화할 수 있다.
- 불변성을 통해 예상 가능하고 안정적인 코드를 작성할 수 있다.
- Structural Sharing으로 성능과 불변성을 동시에 잡을 수 있다.