
얕은 복사와 깊은 복사는 원시값이 아닌 참조형 데이터(배열,객체 등)에 적용되는 개념이다.
얕은 복사는 참조형 데이터의 내부 구조는 복사하지 않고 해당 데이터의 참조만을 복사한다. 원본 데이터의 메모리 주소를 참조하기 때문에 만약 얕은 복사를 통해 만든 데이터를 수정하면 원본 데이터 역시 수정된다.
const a = { name: 'a'};
const b = a;
b.name = 'b';
console.log(a.name); // b
console.log(b.name); // b
얕은 복사는 복사하고자 하는 객체의 속성이 원시 값일 때 사용하거나, 복사와는 개념이 조금 다르지만 특정 데이터의 내부 데이터를 참조할 때 사용할 수 있다.
const a = { names : ['chris','james','tom']};
const names = a.names;
names.push('peter');
console.log(a.names); // ['chris','james','tom','peter'];
이처럼 a의 names 속성을 참조하는 변수를 만들고 이 변수에 데이터를 추가하면 a.names에도 데이터가 추가된다. 만약 이러한 데이터를 추가하는 작업이 반복적으로 일어난다면 매번 a.names를 입력하는 것 보다 names를 사용하는 것이 효율적이다.
깊은 복사는 참조형 데이터의 내부 구조를 복사해서 완전히 새로운 데이터를 생성한다. 새로운 메모리 주소에 생성된 데이터이기 때문에 깊은 복사를 통해 만든 데이터를 수정해도 원본 데이터는 수정되지 않는다.
const a = { name: 'a'};
const b = {...a};
b.name = 'b';
console.log(a.name); // a
console.log(b.name); // b
참조형 데이터 내에 참조형 데이터가 존재하는 경우가 있다. 예를 들어 객체 내에 한 속성이 배열로 이루어지는 경우인데, 이러한 경우 위와 같이 a에 대한 스프레드 문법만으론 완전한 깊은 복사가 이루어지지 않는다.
const classA = { names : ['tom','chris', 'angela']};
const classB = { ...classA};
classB.names.push('anne');
console.log(classA.names); // ['tom','chris','angela','anne'];
console.log(classB.names); // ['tom','chris','angela','anne'];
이처럼 객체 내부에 있는 배열은 얕은 복사가 이루어진 상태이다. 이를 해결하기 위해선 해당 속성을 다시 한번 깊은 복사를 해야한다.
classB.names = [...classB.names];
classB.names.push('anne');
console.log(classA.names); // ['tom','chris','angela'];
console.log(classB.names); // ['tom','chris','angela','anne'];
여기서 더 나아가 만약 배열 내에 원소가 객체로 이루어져 있다면 배열 내의 객체에 대해서도 깊은 복사가 필요하다.
const classA = { students : [
{name:'chris',age:15},
{name:'anne',age:16}
]
const classB = { ... classA};
classB.students = classB.students.map((student)=> ({...student}));
이와 같이 기존의 배열을 map을 통해 새로운 데이터로 생성하고 할당하면 깊은 복사가 이루어진다.
배열이 객체로 이루어져 있고 원소인 객체들에는 또 새로운 객체로 된 속성이 존재할 경우 어떻게 해야할까?
const a = {
students :
[
{name:'chris',class:{name:'A',location:'a1'}},
{name:'tom',class:{name:'B',location : 'a2'}}],
teachers:
[
{name:'anne',class:{name:'A',location:'a1'}},
{name:'angela',class:{name:'B',location:'a2'}}
]
}
const b = {...a};
b.students = b.students.map((student)=>{
student.class = {...student.class};
return {...student};
})
b.teachers = b.teachers.map((teacher)=>{
teacher.class = {...teacher.class};
return {...teacher};
})
사실 데이터 관리 측면에서 이러한 구조는 효과적이지 못하다고 생각한다. students.class에는 객체가 아닌 class name의 string 원시값을 입력하고 class 데이터에 name과 location등과 같은 정보를 따로 저장하는 편이 관리에 더 효율적일 것이다.
chat gpt에게 깊은복사에 대해 질문했을 때 재귀함수를 통해 깊은 복사를 실행할 수 있다는 답변을 받았다. 함수의 코드는 아래와 같다.
const deepcopy=(obj)=>{
if (typeof obj !== 'object' || obj === null) {
return obj;
}
let result;
if (Array.isArray(obj)){
result = [];
} else {
result = {};
}
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
result[key] = deepcopy(obj[key]);
}
}
return result;
}
만약 깊은복사를 하려는 객체의 타입이 object이거나 객체가 null일 경우 객체를 그대로 반환한다.
그리고 결과를 반환할 때 사용할 result 변수를 선언한 다음 원본 객체가 배열일 경우 배열로, 객체일 경우 객체로 설정한다.
원본 객체의 key를 순회하며 key에 해당하는 속성을 깊은 복사한 데이터를 반환한다.
이 함수를 사용하면 일일히 깊은 복사를 할 필요 없이 모든 속성이 깊은 복사가 되는 것을 확인할 수 있었다.
여기서 실제 프로젝트에 사용해보니 eslint가 경고하는 부분이 있었다.
먼저 for .. in 루프를 사용해 obj의 키를 순회할 경우 객체의 프로토타입을 따라 순회하기 때문에 상속된 속성도 함께 순환할 수 있기 때문에 이보단 순회하려는 속성에 따라 Object.keys,Object.values,Object.entries를 통해 순회하는 것이 좋다. 이 경우 key를 순회하기 때문에 Object.keys를 통해 키를 배열로 가져오고 forEach를 통해 순회한다.
그 다음은 obj.hasOwnProperty인데, 이 메서드는 프로토타입 체인을 통해 상속된 속성을 확인하지 않기 때문에 객체가 다른 객체를 상속받은 경우 정상적으로 작동하지 않을 수 있다. 그렇기 때문에 Object.prototype.hasOwnProperty.call(obj, key)를 통해 해당 속성이 존재하는지 확인하는 것이 좋다.
수정된 코드는 아래와 같다.
const deepCopy = (obj) => {
if (typeof obj !== 'object' || obj === null) {
return obj;
}
let result;
if (Array.isArray(obj)) {
result = [];
} else {
result = {};
}
Object.keys(obj).forEach((key) => {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
result[key] = deepCopy(obj[key]);
}
});
return result;
};