JavaScript의 얕은 복사와 깊은 복사 차이를 쉽게 정리했습니다.
JavaScript로 개발하다 보면 객체를 복사해야 하는 상황이 자주 생깁니다. 하지만 단순히 = 연산자로 복사하면 예상치 못한 버그가 발생할 수 있어요. 왜냐하면 객체는 참조 타입이기 때문이죠.
이번 글에서는 얕은 복사와 깊은 복사의 차이점, 각각의 사용 사례, 그리고 실무에서 바로 활용할 수 있는 구현 방법을 알아보겠습니다.
얕은 복사(Shallow Copy)란?
얕은 복사는 객체의 최상위 속성만 복사하는 방식입니다. 복사된 객체와 원본 객체가 같은 참조를 공유하게 되어, 중첩된 객체를 수정하면 원본에도 영향을 미칩니다.
얕은 복사의 동작 원리
얕은 복사를 수행하면 새로운 객체가 생성되지만(o1 !== o2), 내부의 중첩된 객체나 배열은 같은 메모리 주소를 가리킵니다.
const original = {
name: "홍길동",
age: 30,
address: {
city: "서울",
district: "강남구"
}
};
const shallowCopy = Object.assign({}, original);
// 최상위 속성 변경 - 원본에 영향 없음
shallowCopy.name = "김철수";
console.log(original.name); // "홍길동"
// 중첩된 객체 변경 - 원본에도 영향!
shallowCopy.address.city = "부산";
console.log(original.address.city); // "부산"
얕은 복사 구현 방법
JavaScript에서 얕은 복사를 수행하는 방법은 여러 가지가 있습니다:
1. Object.assign() 사용
const user = {
name: "김민수",
role: "개발자"
};
const clone = Object.assign({}, user);
2. 전개 구문(Spread Syntax) 사용
const user = {
name: "박지영",
role: "디자이너"
};
const clone = { ...user };
3. 배열의 경우
const items = [1, 2, { value: 3 }];
// Array.from()
const copy1 = Array.from(items);
// slice()
const copy2 = items.slice();
// concat()
const copy3 = [].concat(items);
// 전개 구문
const copy4 = [...items];
얕은 복사의 특징
얕은 복사된 객체의 최상위 속성을 재할당하면 원본 객체에 영향을 주지 않습니다. 하지만 중첩된 객체의 속성을 변경하면 원본도 함께 변경됩니다.
const ingredientsList = ["국수", { list: ["계란", "밀가루", "물"] }];
const ingredientsListCopy = Array.from(ingredientsList);
// 최상위 요소 변경
ingredientsListCopy[0] = "쌀국수";
console.log(ingredientsList[0]); // "국수" (영향 없음)
// 중첩된 객체 변경
ingredientsListCopy[1].list = ["쌀가루", "물"];
console.log(ingredientsList[1].list); // ["쌀가루", "물"] (영향 있음!)
깊은 복사(Deep Copy)란?
깊은 복사는 객체의 모든 중첩된 속성까지 완전히 새로운 복사본을 만드는 방식입니다. 복사본을 수정해도 원본 객체에 전혀 영향을 주지 않습니다.
깊은 복사의 정의
두 객체가 깊은 복사 관계에 있으려면 다음 조건을 만족해야 합니다:
- 두 객체는 서로 다른 객체여야 합니다 (
o1 !== o2) - 속성의 이름과 순서가 같아야 합니다
- 속성 값들이 서로의 깊은 복사본이어야 합니다
- 프로토타입 체인이 구조적으로 동일해야 합니다
깊은 복사의 동작 원리
깊은 복사는 중첩된 모든 객체를 재귀적으로 복사합니다. 따라서 복사본의 어떤 부분을 변경하더라도 원본에 영향을 주지 않습니다.
const original = {
name: "이수진",
projects: {
current: ["프로젝트 A", "프로젝트 B"],
completed: ["프로젝트 X"]
}
};
const deepCopy = JSON.parse(JSON.stringify(original));
deepCopy.projects.current.push("프로젝트 C");
console.log(original.projects.current); // ["프로젝트 A", "프로젝트 B"] (영향 없음)
깊은 복사 구현 방법
1. JSON.parse(JSON.stringify()) 활용
가장 간단하고 널리 사용되는 방법입니다.
const ingredientsList = ["국수", { list: ["계란", "밀가루", "물"] }];
const ingredientsListDeepCopy = JSON.parse(JSON.stringify(ingredientsList));
ingredientsListDeepCopy[1].list = ["쌀가루", "물"];
console.log(ingredientsList[1].list); // ["계란", "밀가루", "물"] (영향 없음)
장점:
- 구현이 간단하고 직관적입니다
- 별도의 라이브러리가 필요 없습니다
단점:
- 함수는 복사되지 않습니다
Date객체는 문자열로 변환됩니다undefined,Symbol같은 특수 값은 무시됩니다- 순환 참조가 있으면 오류가 발생합니다
const obj = {
date: new Date(),
func: () => console.log("hello"),
undef: undefined
};
const copy = JSON.parse(JSON.stringify(obj));
console.log(copy);
// { date: "2024-01-15T10:30:00.000Z" }
// func와 undef는 사라집니다
2. structuredClone() 사용
JavaScript에서 기본적으로 제공하는 메서드입니다.
const original = {
name: "최유진",
date: new Date(),
nested: {
data: [1, 2, 3]
}
};
const deepCopy = structuredClone(original);
deepCopy.nested.data.push(4);
console.log(original.nested.data); // [1, 2, 3] (영향 없음)
console.log(deepCopy.date instanceof Date); // true
장점:
Date,RegExp,Map,Set등 다양한 내장 객체를 정확히 복사합니다- 순환 참조도 처리할 수 있습니다
- 성능이 우수합니다
단점:
- 함수는 여전히 복사되지 않습니다
- 오래된 브라우저에서는 지원하지 않을 수 있습니다
3. 재귀 함수를 이용한 커스텀 구현
완전한 제어가 필요한 경우 직접 구현할 수 있습니다.
function deepClone(obj, hash = new WeakMap()) {
// null이나 원시 타입은 그대로 반환
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 순환 참조 처리
if (hash.has(obj)) {
return hash.get(obj);
}
// Date, RegExp 등 특수 객체 처리
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
// 새 객체 생성
const cloneObj = Array.isArray(obj) ? [] : {};
hash.set(obj, cloneObj);
// 재귀적으로 복사
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
cloneObj[key] = deepClone(obj[key], hash);
}
}
return cloneObj;
}
const original = {
name: "정민호",
skills: ["JavaScript", "React"],
experience: {
years: 5,
companies: ["A사", "B사"]
}
};
const copy = deepClone(original);
copy.skills.push("TypeScript");
console.log(original.skills); // ["JavaScript", "React"] (영향 없음)
장점:
- 필요에 따라 커스터마이징 가능합니다
- 특수한 타입도 원하는 대로 처리할 수 있습니다
단점:
- 구현과 유지보수가 복잡합니다
- 엣지 케이스를 모두 고려해야 합니다
4. Lodash 라이브러리 활용
lodash는 실무에서 많이 사용하는 자바스크립트 유틸리티 라이브러리입니다.
import _ from 'lodash';
const original = {
name: "강서연",
metadata: {
tags: ["frontend", "react"],
createdAt: new Date()
}
};
const deepCopy = _.cloneDeep(original);
장점:
- 검증된 구현으로 안정적입니다.
- 대부분의 엣지 케이스를 처리합니다.
단점:
- 추가 라이브러리를 설치해야 합니다.
- 번들 크기가 증가할 수 있습니다. (물론 lodash의 용량이 크지는 않지만…)
언제 얕은 복사를 사용할까?
얕은 복사는 다음과 같은 상황에 적합합니다:
1. 성능이 중요한 경우
중첩된 객체를 독립적으로 수정할 필요가 없다면, 얕은 복사가 더 빠르고 메모리 효율적입니다.
const userConfig = {
theme: "dark",
language: "ko"
};
// 최상위 속성만 변경하므로 얕은 복사로 충분
const newConfig = { ...userConfig, theme: "light" };
2. 최상위 속성만 변경하는 경우
중첩된 객체를 건드리지 않는다면 얕은 복사만으로도 충분합니다.
const product = {
id: 1,
name: "노트북",
price: 1500000
};
const updatedProduct = { ...product, price: 1400000 };
3. React에서 Props 전달
React에서는 불변성을 유지하기 위해 얕은 복사를 자주 사용합니다.
const handleUpdate = (newData) => {
setUser({ ...user, ...newData });
};
언제 깊은 복사를 사용할까?
깊은 복사는 다음 상황에서 필요합니다:
1. 완전히 독립적인 복사본이 필요한 경우
원본과 완전히 분리된 객체가 필요할 때 깊은 복사를 사용합니다.
const template = {
title: "기본 템플릿",
sections: [
{ type: "header", content: "제목" },
{ type: "body", content: "본문" }
]
};
// 각 사용자마다 독립적인 템플릿 필요
const userTemplate = structuredClone(template);
userTemplate.sections[0].content = "사용자 제목";
2. 중첩된 객체를 수정해야 하는 경우
복잡한 데이터 구조를 다룰 때 깊은 복사가 필수입니다.
const projectData = {
name: "신규 프로젝트",
team: {
frontend: ["김개발", "이디자인"],
backend: ["박서버", "최디비"]
}
};
const archivedProject = structuredClone(projectData);
archivedProject.team.frontend.push("신입사원");
// original의 team은 영향받지 않음
3. 상태 히스토리를 관리하는 경우
실행 취소(Undo) 기능을 구현할 때 유용합니다.
const history = [];
function saveState(currentState) {
history.push(structuredClone(currentState));
}
function undo() {
return history.pop();
}
성능 고려사항
깊은 복사와 얕은 복사는 성능 차이가 있습니다.
얕은 복사의 성능
얕은 복사는 최상위 속성만 복사하므로 매우 빠릅니다. 객체의 크기가 클수록 깊은 복사 대비 성능 이점이 큽니다.
// 빠름
const shallowCopy = { ...largeObject };
깊은 복사의 성능
깊은 복사는 모든 중첩 구조를 순회하므로 당연히 시간이 더 오래 걸립니다.
// 상대적으로 느림
const deepCopy = structuredClone(largeObject);
성능 최적화 팁
대용량 객체를 다룰 때는 다음을 고려하세요:
- 필요한 부분만 복사하기
const { metadata, ...essentialData } = largeObject;
const copy = structuredClone(essentialData);
- 메모이제이션 활용
const cache = new WeakMap();
function getCachedCopy(obj) {
if (!cache.has(obj)) {
cache.set(obj, structuredClone(obj));
}
return cache.get(obj);
}
- 적절한 방법 선택
- 간단한 객체:
JSON.parse(JSON.stringify()) - 복잡한 객체:
structuredClone() - 커스텀 로직 필요: 직접 구현 또는 Lodash
자주 묻는 질문
Q: 배열도 같은 방식으로 복사하나요?
네, 배열도 객체이므로 동일한 원리가 적용됩니다. 얕은 복사는 전개 구문이나 slice()를, 깊은 복사는 structuredClone()을 사용하면 됩니다.
Q: React에서는 어떤 복사 방식을 주로 사용하나요?
React에서는 대부분 얕은 복사를 사용합니다. 상태 업데이트 시 { ...state } 같은 방식으로 새 객체를 만들어 불변성을 유지합니다.
Q: JSON 방식의 깊은 복사로 충분하지 않나요?
간단한 데이터 구조에는 충분하지만, Date, 함수, undefined 등을 다뤄야 한다면 structuredClone()이나 Lodash를 사용하는 것도 좋은 방법입니다 !