a = 1과 a = { name: 'Kim' }은 겉으로는 똑같은 할당처럼 보입니다. 그런데 복사하거나 비교하거나 함수에 넘기는 순간 전혀 다르게 동작합니다. JavaScript가 두 종류의 데이터를 메모리에 완전히 다른 방식으로 저장하기 때문입니다.
JavaScript의 모든 데이터 타입은 원시값과 참조값 두 범주로 나뉩니다.
number, string, booleannull, undefinedsymbol, bigintobject (일반 객체, 배열, 함수 모두 포함)원시값은 스택에 값 자체가 저장되고, 참조값은 힙에 실제 데이터가 저장되며 스택에는 그 주소만 담깁니다.
[ stack ] [ heap ]
┌─────────────┐
│ a = 42 │ ← value
├─────────────┤
│ b = "hi" │ ← value
├─────────────┤ ┌──────────────────┐
│ c = 0x1A3F │ ──addr──▶ │ { name: 'Kim' } │
└─────────────┘ └──────────────────┘
스택은 크기가 고정된 데이터를 빠르게 읽고 쓰는 데 적합합니다. 객체처럼 크기가 가변적인 데이터는 힙에 두고, 스택에는 그 위치(주소)만 저장합니다.
스택(Stack): 함수 호출 시 생성되는 고정 크기 메모리 영역. 함수가 끝나면 자동으로 해제됩니다.
힙(Heap): 동적으로 크기가 결정되는 데이터를 저장하는 메모리 영역. 가비지 컬렉터가 관리합니다.
let a = 10;
let b = a; // 10이라는 값이 복사됨
b = 99;
console.log(a); // 10 — a는 영향 없음
console.log(b); // 99
b를 바꿔도 a는 그대로입니다. 서로 독립된 복사본이기 때문입니다.
const user1 = { name: 'Kim' };
const user2 = user1; // 주소가 복사됨
user2.name = 'Lee';
console.log(user1.name); // 'Lee' — user1도 바뀜!
console.log(user2.name); // 'Lee'
user1과 user2는 같은 객체를 가리키는 두 개의 주소입니다.
[ stack ] [ heap ]
┌──────────────┐
│ user1 = 0x1A │ ──┐ ┌──────────────────┐
├──────────────┤ └──▶ │ { name: 'Lee' } │
│ user2 = 0x1A │ ──────▶ └──────────────────┘
└──────────────┘
두 변수가 동일한 힙 주소를 공유하므로, 하나를 통해 내부를 수정하면 나머지에도 반영됩니다.
console.log(1 === 1); // true
console.log('hello' === 'hello'); // true
const a = { x: 1 };
const b = { x: 1 };
const c = a;
console.log(a === b); // false — 내용이 같아도 주소가 다름
console.log(a === c); // true — 같은 주소를 가리킴
내용이 동일한 두 객체라도 서로 다른 힙 위치에 저장되면 ===는 false를 반환합니다.
// 원시값: 함수 내부의 변경이 외부에 영향 없음
function addTen(n) {
n += 10;
}
let score = 50;
addTen(score);
console.log(score); // 50 — 변하지 않음
// 참조값: 함수 내부의 변경이 외부에 반영됨
function rename(obj) {
obj.name = 'Park';
}
const person = { name: 'Kim' };
rename(person);
console.log(person.name); // 'Park' — 바뀜
함수에 참조값을 넘기면 주소가 전달되므로, 함수 내부에서 프로퍼티를 수정하면 원본 객체가 변경됩니다.
const original = { name: 'Kim', age: 30 };
// spread 연산자
const copy1 = { ...original };
// Object.assign
const copy2 = Object.assign({}, original);
copy1.name = 'Lee';
console.log(original.name); // 'Kim' — 독립적
중첩 객체가 있으면 내부 객체는 여전히 주소를 공유합니다.
const a = { info: { city: 'Seoul' } };
const b = { ...a };
b.info.city = 'Busan';
console.log(a.info.city); // 'Busan' — 중첩 객체는 공유됨
const a = { info: { city: 'Seoul' } };
const b = structuredClone(a);
b.info.city = 'Busan';
console.log(a.info.city); // 'Seoul' — 완전히 독립
structuredClone: 브라우저와 Node.js 17 이상에서 지원하는 내장 깊은 복사 함수입니다. Date, Map, Set 등도 올바르게 복사합니다.
| 구분 | 원시값 | 참조값 |
|---|---|---|
| 저장 방식 | 스택에 값 자체 저장 | 스택에 주소, 힙에 데이터 저장 |
| 복사 | 값이 독립적으로 복사됨 | 주소가 복사되어 같은 객체 공유 |
| 비교(===) | 값이 같으면 true | 같은 주소일 때만 true |
| 종류 | number, string, boolean, null, undefined, symbol, bigint | object, array, function |
| 상황 | 방법 |
|---|---|
| 중첩 없는 객체 | 얕은 복사 — { ...obj }, Object.assign |
| 중첩 있는 객체 | 깊은 복사 — structuredClone(obj) |
변수에 담기는 것이 값인지 주소인지를 파악하면, 예상치 못한 데이터 변경과 비교 오류의 원인을 빠르게 찾을 수 있습니다. 참조값을 독립적으로 복사할 때는 중첩 구조 여부에 따라 방법을 구분합니다.