깊은 복사와 얕은 복사에 대해 알아보자.
얕은 복사 (shallow copy)
얕은 복사는 참조(주소)값의 복사를 나타낸다.
const obj = { value: 1 }
const newobj = obj;
newObj.value = 2;
console.log(obj.value); //2
console.log(obj === newObj); // true
obj객체를 새로운 newObj 객체에 할당하였으며 이를 참조 할당이라 불른다. 복사 후 newObj객체에 value값을
변경하니 기존의 obj.value 값도 같이 변경된 것을 알 수 있다. 두 객체를 모두 비교해도 true로 나온다.
이렇게 자바스크립트의 참조 타입은 얕은 복사가 된다고 볼 수 있으며, 이는 데이터가 그대로 생성되는 것이 아닌
해당 데이터의 참조 값(메모리 주소)를 전달하여 결국 한 데이터를 공유하는 것이다.
깊은 복사 (deep copy)
깊은 복사는 값 자체의 복사를 나타낸다.
let a = 1;
let b = 2;
b = 2;
console.log(a); // 1
console.log(b); // 2
console.log(a === b); // false
변수 a를 새로운 b에 할당하였고 b값을 변경하여도 기존의 a값은 변경되지 않는다. 두 값을 비교하면 false가 출력되어
서로의 값은 단독으로 존재한다는 것을 알 수 있다. 이렇게 자바스크립트 원시 타입에는 깊은 복사가 되며, 이는
독립적인 메모리에 값 자체를 할당하여 생성하는 것이라 볼 수 있다.
객체의 깊은 복사
객체를 그대로 복사하여 사용할 경우 기존 객체의 원본 데이터가 더럽혀 질 수 있기 때문에 객체의 깊은 복사는 매우
중요하다. 객체를 깊이 복사하는 방법에 대해 몇가지 알아보자.
Object.assign()
Object.assign() 메소드를 활용하는 방법이다.
문법
Object.assign(생성할 객체, 복사할 객체) 메서드의 첫번째 인수로 빈 객체를 넣어주며,
두번째 인수로 할당할 객체를 넣으면 된다.
const obj = { a: 1 };
const newObj = Object.assign({}, obj);
newObj.a = 2;
console.log(obj); // { a: 1 }
console.log(obj === newObj); // false
새로운 newObj 객체를 Object.assign() 메소드를 사용해 생성했으며, newObj.a 값을 변경하더라도 기존의
obj는 변하지 않았다. 서로의 객체를 비교해도 false로 뜨고 서로 참조값이 다르다는 것을 알 수 있다.
Object.assign()는 2차원 객체는 깊은 복사가 이루어지지 않는다.
하지만 Object.assign()을 활용한 복사는 완벽한 깊은 복사가 아니다.
const obj = {
a: 1,
b: {
c: 2,
};
};
위 처럼 obj객체의 b프로퍼티의 값으로 { c : 2 } 객체를 가진 2차원 객체의 경우는 어떨까?
const obj = {
a: 1,
b: {
c: 2,
},
};
const newObj = Object.assign({}, obj);
newObj.b.c = 3;
console.log(obj); // { a: 1, b: { c: 3 } }
console.log(obj.b.c === newObj.b.c); // true
2차원 객체를 newObj에 복사하고 newObj.b.c의 값을 변경하였다. 기존 obj객체를 출력해보면
newObj.b.c의 값도 3으로 변경되었다. 복사된 하위 객체 { c : 2 }도 결국 객체이기 때문에 얕은 복사가 이루어진 것.
이는 Object.assign()메서드의 한계이며, 전개 연산자(Spread Operator)를 이용한 객체의 복사에도 같은 문제가 있다.
전개 연산자 (Spread Operator)
const obj = { a: 1 };
const newObj = Object.assign({}, obj);
newObj.a = 2;
console.log(obj); // { a: 1 }
console.log(obj === newObj); // false
전개 연산자를 활용해도 객체의 깊은 복사가 가능하다.
const obj = {
a: 1,
b: {
c: 2,
},
};
const newObj = { ...obj };
newObj.b.c = 3;
console.log(obj); // { a: 1, b: { c: 3 } }
console.log(obj.b.c === newObj.b.c); // true
하지만 Object.assign() 과 마찬가지로 2차원 객체는 얕은 복사가 되는 것을 확인할 수 있다.
JSON 객체 메소드 이용
객체의 깊은 복사를 위한 JSON 객체의 stringify(), parse() 메소드를 사용할 수 있다.
문법
JSON.stringify() 메소드는 인수로 객체를 받으며 받은 객체는 문자열로 치환되며,
JSON.parse() 메소드는 문자열을 인수로 받으며, 받은 문자열을 객체로 치환한다.
const obj = {
a: 1,
b: {
c: 2,
},
};
const newObj = JSON.parse(JSON.stringify(obj));
newObj.b.c = 3;
console.log(obj); // { a: 1, b: { c: 2 } }
console.log(obj.b.c === newObj.b.c); // false
obj 객체를 JSON.stringify() 메소드를 이용하여 문자열로 변환한 뒤 다시 JSON.parse()메소드로 객체를 변환하였다.
문자열로 변환한 뒤 다시 객체로 변환하였기에 2차원 객체에 대한 참조가 사라졌다. 하지만 이 방법도 2가지 문제가
있는데, 다른 방법에 비해 성능이 느린 점과 JSON.stringify() 메소드는 함수를 만났을 때 undefined로 처리한다는 점
const obj = {
a: 1,
b: {
c: 2,
},
func: function() {
return this.a;
}
};
const newObj = JSON.parse(JSON.stringify(obj));
console.log(newObj.func); // undefined
복사된 newObj는 func가 없고 undefined로 출력되고 있다.
커스텀 재귀 함수
이 문제를 원칙적으로 해결하려면 직접 깊은 복사를 구현하는 커스텀 재귀 함수를 사용하는 것이다.
function deepCopy(obj) {
if (obj === null || typeof obj !== "object") {
return obj;
}
let copy = {};
for (let key in obj) {
copy[key] = deepCopy(obj[key]);
}
return copy;
}
const obj = {
a: 1,
b: {
c: 2,
},
func: function () {
return this.a;
},
};
const newObj = deepCopy(obj);
newObj.b.c = 3;
console.log(obj); // { a: 1, b: { c: 2 }, func: [Function: func] }
console.log(obj.b.c === newObj.b.c); // false
deepCopy 함수의 인수로 obj 객체를 넣었다. 인수값이 객체가 아닌 경우는 그냥 반환하며, 객체인 경우 객체의 값 만큼
루프를 돌며 재귀를 호출하여 복사된 값을 반환한다. 복사된 newObj 객체를 보면 2차원 객체의 값도 깊은 복사가
이루어 졌으며, 객체의 함수도 제대로 표현되는 것을 확인할 수 있다.
하지만 이미 객체의 깊은 복사를 위한 오픈 소스가 존재하며 lodash 모듈의 cloneDeep()을 이용하면 된다.
lodash 모듈의 cloneDeep()
lodash 모듈의 cloneDeep() 메소드를 이용하여 객체의 깊은 복사가 가능하다. 해당 모듈을 설치해 준뒤 아래코드를
실행해보자.
& npm i lodash
References
'프론트 엔드 > Javascript' 카테고리의 다른 글
[Javascript] Scope(스코프) 란? (0) | 2022.01.16 |
---|---|
[Javascript] 호이스팅 이란? (0) | 2022.01.16 |
[Javscript] 원시 자료형, 참조 자료형 (0) | 2022.01.16 |