2025년 12월 1일에 작성된 글입니다.
객체를 복사했는데 원본까지 바뀌는 경험 있으신가요? JavaScript에서는 데이터를 복사할 때 타입에 따라 동작 방식이 다르기 때문에 이런 일이 발생합니다. 이 글에서는 복사의 기본 원리부터 실전 복사 전략까지 단계별로 알아보겠습니다.
이 글을 통해 얻을 수 있는 것
- 데이터 타입에 따른 복사 방식의 차이를 이해한다
- 객체 복사 시 주의해야 할 점을 안다
- 상황에 맞는 복사 방법을 선택할 수 있다
1. JavaScript 데이터의 복사: 타입에 따라 다르다
JavaScript의 타입은 크게 원시 값과 객체로 나뉩니다. 또한 변수에 값을 할당하거나 복사할 때, 값의 타입이 무엇인지에 따라 동작이 다릅니다.
JavaScript의 타입을 잘 모르겠다면 MDN 자료를 참고해보세요.
원시 값 (Primitive Values)
원시 값은 변수에 ‘값 자체’가 저장됩니다.
-
종류:
string,number,bigint,boolean,undefined,symbol,null가 있습니다. - 복사 시 완전히 독립된 새로운 값이 생성됩니다.
let a = 100;
let b = a; // a의 값(100)이 복사되어 b에 저장
b = 50; // b만 변경됨
console.log(a); // 100
console.log(b); // 50 (서로 독립적임)
객체 (Object)
객체는 변수에 값이 아닌 ‘메모리 주소(참조)’가 저장됩니다.
-
종류:
object,array가 있으며,function,Date또한 객체의 일종입니다. - 복사 시 객체를 가리키는 주소가 복사됩니다.
let myArr = [];
let copyArr = myArr; // 배열을 가리키는 주소가 복사됨
copyArr.push("hello"); // 복사본을 수정
console.log(copyArr); // ["hello"]
console.log(myArr); // ["hello"] (원본도 변경됨)
2. 객체 복사 시 주의점: 참조 공유 문제
const a = b;와 같이 단순 대입으로 객체를 복사하면 참조만 복사되므로, 원본과 복사본이 공유됩니다.
이를 피하려면 새로운 객체를 생성해야 합니다.
동일한 프로퍼티를 가지면서 새로운 객체를 생성하는 방법으로는 Object.assign()이나 배열의 경우 Array.prototype.slice() 등이 있지만, 글에선 전개 연산자를 사용해보겠습니다.
전개 연산자로 새 객체 만들기
const user = { name: 'Lee', age: 30 };
const copy = { ...user }; // 새로운 객체 생성
copy.age = 31;
console.log(user.age); // 30 (원본 영향 없음)
console.log(copy.age); // 31
원본 객체의 속성들을 새로운 객체에 복사했고, 각 속성의 값은 원시 값이므로 독립적으로 수정할 수 있습니다.
중첩 객체를 가진 경우
이번에는 속성의 값으로 객체가 있는 경우를 같은 방식으로 복사해보겠습니다.
const user = {
name: 'Lee',
address: { city: 'Seoul' } // 중첩 객체
};
const copy = { ...user };
copy.address.city = 'Busan';
console.log(user.address.city); // 'Busan' (원본도 변경됨)
중첩 객체는 참조가 복사되므로 원본이 변경되었습니다.
user → { name: 'Lee', address: → [0x001] }
copy → { name: 'Kim', address: → [0x001] } (같은 주소 공유)
↓
{ city: 'Busan' }
왜 이런 일이 일어났을까요?
데이터 복사는 값의 타입에 따라 달라진다는 것, 기억하시나요?
전개 연산자를 통해 원본 객체의 속성을 복사하는 과정에서 원시 값은 ‘값 자체’를 복사했지만, 중첩 객체는 ‘메모리 주소(참조)’를 복사하면서 속성 값 변경 시 서로 영향을 미치게 되는 것입니다.
이처럼 최상위 속성(= 1단계 깊이의 속성)만 복사하는 것을 복사 깊이가 얕다는 의미로, "얕은 복사(Shallow Copy)"라고 합니다.
3. 중첩 객체를 안전하게 복사하는 방법
얕은 복사로 인한 문제를 해결하고, 원본을 보호하면서 복사하는 방법은 크게 두 가지가 있습니다.
3-1. 전개 연산자를 중첩해 필요한 부분만 새로 만들기
변경하려는 속성의 경로를 따라 전개 연산자를 중첩해서 사용하면, 필요한 부분만 새로 만들 수 있습니다.
const user = {
name: 'Lee',
address: { city: 'Seoul', zip: '12345' }
};
// address.city만 변경하고 싶을 때
const updated = {
...user, // user의 최상위 속성들 복사
address: { // address를 새 객체로 덮어쓰기
...user.address, // 기존 address 속성들 복사 (zip 유지)
city: 'Busan' // city만 변경
}
};
console.log(user.address.city); // 'Seoul' (원본 유지)
console.log(updated.address.city); // 'Busan'
배열 내부 객체를 수정할 때는 Array.prototype.map()을 활용할 수도 있습니다.
const users = [
{ id: 1, name: 'Lee', active: false },
{ id: 2, name: 'Kim', active: false }
];
// id가 1인 사용자의 active만 변경
const updated = users.map(user =>
user.id === 1
? { ...user, active: true }
: user
);
이처럼 전개 구문을 중첩해서 사용하면 필요한 부분만 새로 만들어 복사할 수 있습니다. 하지만 이 방법은 어떤 속성을 변경할지 미리 알고 있을 때 유용합니다.
3-2. 모든 속성을 독립적으로 복사하기: 깊은 복사
사용자가 자유롭게 데이터를 바꾸는 환경에서는, 편집 시작 시점의 원본 데이터를 통째로 보관해두는 것이 효율적입니다.
또한 히스토리를 유지하거나 로그를 남겨야 하는 경우에도 각 시점의 상태를 독립적으로 저장해야 정확한 기록을 유지할 수 있습니다.
예시 시나리오: 텍스트 에디터의 취소 기능
사용자가 문서를 편집하다가 "실행 취소(ctrl+z)"를 하면 이전 상태로 되돌아가야 합니다. 사용자가 어떤 부분을 수정할지 예측할 수 없으므로, 편집 전 문서 상태를 통째로 저장해두어야 합니다.
// 현재 문서 상태
const currentDocumentState = {
content: 'Hello World',
formatting: {
fontSize: 14,
styles: { bold: false, italic: false }
}
};
// 얕은 복사로는 안전하지 않음
const snapshot = { ...currentDocumentState }; // formatting은 여전히 공유됨
// 사용자가 여러 작업 수행
currentDocumentState.content = 'Hello JavaScript';
currentDocumentState.formatting.fontSize = 16;
currentDocumentState.formatting.styles.bold = true;
// 실행 취소 시도
Object.assign(currentDocumentState, snapshot); // formatting은 이미 변경되어 복구할 수 없는 문제 발생
이런 경우 모든 중첩된 객체를 완전히 새로운 메모리 공간에 복사해야 합니다. 이를 "깊은 복사(Deep Copy)"라고 합니다.
그 중 structuredClone()은 최신 브라우저에서 지원하는 표준 API로, structured clone 알고리즘을 사용하여 깊은 복사를 수행합니다.
// 편집 전 깊은 복사로 스냅샷 저장
const snapshot = structuredClone(currentDocumentState);
// 사용자가 수정
currentDocumentState.formatting.fontSize = 16;
// 실행 취소 시 안전하게 복원
Object.assign(currentDocumentState, snapshot);
console.log(currentDocumentState.formatting.fontSize); // 14 (원본 상태 유지)
깊은 복사 방법 비교표
| 방법 | 장점 | 단점 |
|---|---|---|
| structuredClone() | 빠르고 표준 | 호환성 확인 필요 |
| _.cloneDeep() | 모든 타입 복사 가능 | 번들 사이즈 증가 |
| Immer | 불변 업데이트 간결 | 러닝커브 있음 |
| JSON.parse(JSON.stringify()) | 라이브러리 없이 간단 사용 | Date, function, undefined 손실 |
요약
-
타입에 따라 복사 방식이 다름
- 원시 값: 값 자체 복사 → 독립적
- 객체: 참조 복사 → 공유됨
-
객체 복사의 기본은 "얕은” 복사 방식
- 전개 연산자(
{...obj},[...arr])는 1단계만 새로 만듦 - 필요한 경로까지 중첩 전개로 해결 가능
- 전개 연산자(
-
필요시 "깊은" 복사 방식 적용
- 중첩 객체 + 원본 보호 필요 시
structuredClone()등 API 사용
- 중첩 객체 + 원본 보호 필요 시
퀴즈
- 다음 코드의 결과를 예측해보세요.
const user = { name: "Lee", age: 30 };
const copy = { ...user };
copy.age = 31;
console.log(user.age); // ?
- 다음 코드의 결과를 예측해보세요.
const user = { name: "Lee", info: { age: 30 } };
const copy = { ...user };
copy.info.age = 31;
console.log(user.info.age); // ?


Top comments (0)