JavaScript · 4장 객체
참조와 복사: 같은 객체를 두 이름이 가리킬 때
"복사본을 고쳤는데 원본까지 바뀌었어요." 자바스크립트 초보가 가장 자주 만나는 버그 중 하나예요. 원인은 객체가 변수에 담기는 방식에 있어요. 참조가 무엇인지, 얕은 복사와 깊은 복사가 어떻게 다른지 정리했어요.
01. 변수에는 객체로 가는 길이 담겨요
숫자 · 문자열 같은 원시값은 변수에 값 자체가 담겨요. 다른 변수에 넣으면 값이 복사돼서 둘은 따로 놀아요. 하지만 객체 · 배열은 변수에 객체가 있는 곳으로 가는 길(참조)이 담겨요. 다른 변수에 넣으면 길만 복사되고, 두 변수는 같은 객체 하나를 가리켜요.
let x = 1;
let y = x; // 값이 복사돼요
y = 2;
console.log(x); // 1 — x는 그대로
const a = { qty: 1 };
const b = a; // 길(참조)만 복사돼요
b.qty = 5;
console.log(a.qty); // 5 — 같은 객체라서 a로 봐도 바뀌었어요
02. 객체 비교는 "같은 객체인가"예요
같은 이유로 ===는 객체의 내용이 아니라 같은 객체인지를 비교해요. 내용이 똑같아도 따로 만든 객체는 다르다고 나와요.
const p1 = { x: 1 };
const p2 = { x: 1 };
const p3 = p1;
console.log(p1 === p2); // false — 내용은 같지만 다른 객체
console.log(p1 === p3); // true — 같은 객체
console.log([] === []); // false
내용을 비교하고 싶다면 필요한 속성을 직접 비교하거나(p1.x === p2.x), 테스트에서는 도구의 "내용 비교"(Vitest의 toEqual)를 써요.
03. 얕은 복사: spread와 Object.assign
원본은 그대로 두고 따로 고칠 사본이 필요하면 복사를 해요. 가장 많이 쓰는 건 전개 구문 { ...객체 }, [...배열]이에요. 예전 코드에서는 Object.assign({}, 객체)도 보여요.
const original = { name: '연필', price: 500 };
const copy = { ...original, price: 450 }; // 복사하면서 price만 바꿔요
console.log(original.price, copy.price); // 500 450
그런데 이 복사는 한 층만 복사해요. 속성 값이 또 객체라면, 그 안쪽 객체는 복사되지 않고 참조만 복사돼요. 그래서 이름이 얕은 복사예요.
const original = { name: '연필', options: { color: 'red' } };
const copy = { ...original };
copy.name = '색연필'; // 바깥 층: 사본만 바뀌어요
copy.options.color = 'blue'; // 안쪽 객체: 원본과 함께 써요
console.log(original.name); // 연필
console.log(original.options.color); // blue — 원본까지 바뀌었어요
04. 깊은 복사: structuredClone
안쪽 객체까지 전부 새로 만들려면 structuredClone(객체)을 써요. 브라우저와 Node.js에 기본으로 들어 있는 함수예요. Date · Map · Set도 제대로 복사해요.
const original = { name: '연필', options: { color: 'red' }, tags: new Set(['new']) };
const copy = structuredClone(original);
copy.options.color = 'blue';
copy.tags.add('sale');
console.log(original.options.color); // red — 원본은 그대로
console.log(original.tags); // Set(1) {'new'}
다만 복사할 수 없는 것도 있어요. 함수가 들어 있으면 오류가 나고, 클래스로 만든 객체는 메서드 없는 평범한 객체로 바뀌어요(9장). 예전에는 JSON.parse(JSON.stringify(객체))로 깊은 복사를 하기도 했는데, 이 방법은 Date를 문자열로 바꾸고 undefined 속성을 지워 버려요(5장 JSON).
const withFunction = { name: '연필', format() { return this.name; } };
const copy = structuredClone(withFunction);
05. 함수에 객체를 넘기면
함수에 객체를 넘기면 매개변수에도 같은 객체로 가는 길이 들어가요. 그래서 함수 안에서 객체를 고치면 부른 쪽의 객체도 바뀌어요. 이걸 모르고 쓰면 "이 함수를 불렀더니 엉뚱한 데이터가 바뀌었다"는 버그가 생겨요.
function applyDiscount(product) {
product.price = product.price * 0.9; // 받은 객체를 직접 고쳐요
return product;
}
const pencil = { name: '연필', price: 1000 };
const sale = applyDiscount(pencil);
console.log(pencil.price); // 900 — 원본이 바뀌었어요
console.log(sale === pencil); // true
원본을 지키려면 함수 안에서 새 객체를 만들어 돌려줘요. return { ...product, price: product.price * 0.9 };처럼요. 이렇게 바꾸지 않고 새로 만드는 방식(불변 업데이트)은 화면 프레임워크의 상태 관리에서 특히 중요해서, 5장 끝에서 따로 다뤄요.
"수량 미리 보기"를 위해 장바구니를 복사해서 수량을 바꿨는데, 원래 장바구니의 수량까지 바뀌어요. 원본 cart는 그대로 두고 preview만 바뀌게 고쳐 보세요. 마지막 줄이 1 3이 나와야 해요.
const cart = [{ name: '연필', qty: 1 }, { name: '공책', qty: 2 }];
const preview = [...cart];
preview[0].qty = 3;
console.log(cart[0].qty, preview[0].qty);
해답 보기
[...cart]는 배열만 새로 만들고, 안의 상품 객체는 원본과 함께 써요. 상품 객체까지 새로 만들어야 해요. structuredClone(cart)로 통째로 복사하거나, 바꿀 항목만 새 객체로 만들어요(5장 map에서 더 자연스럽게 쓸 수 있어요).
const preview = structuredClone(cart);
preview[0].qty = 3;
console.log(cart[0].qty, preview[0].qty); // 1 3
// 또는 바꿀 항목만 새로
const preview2 = [...cart];
preview2[0] = { ...cart[0], qty: 3 };
06. 실전 포인트
- 언제 쓰나요? 받은 데이터를 고치기 전에, 원본을 다른 곳에서도 쓴다면 복사해요. 한 층만 바꿀 거면
{ ...obj, 바꿀속성 }, 깊은 곳까지 마음껏 고칠 거면structuredClone이에요. - 대신 쓰는 것: 무조건 깊은 복사를 하면 큰 데이터에서 느려져요. 바뀌는 부분만 새로 만들고 나머지는 그대로 같이 쓰는 방식(불변 업데이트, 5장)이 실무의 기본이에요.
JSON.parse(JSON.stringify(…))복사는 쓰지 않아요. - 흔한 실수: spread 한 번으로 안쪽 객체까지 복사됐다고 믿는 것, 함수 인자로 받은 객체를 그 자리에서 고치는 것, 내용이 같은 객체를
===로 비교하는 것이에요.
정리
- 객체 · 배열 변수에는 참조가 담겨요. 다른 변수에 넣어도 같은 객체를 가리켜요.
===는 같은 객체인지를 비교해요. 내용이 같아도 따로 만든 객체는false예요.{ ...obj }·[...arr]는 한 층만 복사하는 얕은 복사예요. 안쪽 객체는 함께 써요.- 깊은 복사는
structuredClone이에요. 함수는 복사하지 못해요. - 함수에 넘긴 객체를 고치면 부른 쪽 객체도 바뀌어요. 새 객체를 만들어 돌려줘요.
const b = a; 다음에 b.count++를 하면 a.count는 어떻게 될까요?
똑같이 1 늘어나요. b = a는 객체를 복사하지 않고 같은 객체로 가는 길만 복사하기 때문이에요.
{ ...user }로 복사한 뒤 copy.address.city를 바꾸면 원본은?
원본의 address.city도 바뀌어요. 전개 구문은 바깥 층만 새로 만들고, address 같은 안쪽 객체는 원본과 함께 써요.
메서드가 들어 있는 객체를 structuredClone으로 복사하면?
DataCloneError 오류가 나요. 함수는 복사할 수 없어요. 데이터만 담긴 객체를 복사하거나, 클래스라면 새로 만드는 방법을 써요.