코드를 작성하다보면 object를 복사해서 쓸 일이 있다. 앝은 복사(직접 할당)을 하게 되면 같은 주소를 공유하게 되어 한 값을 수정시 이가 다른 값에도 반영되게 된다. 정말 값만을 복사해서 쓸 수 있도록 깊은 복사에 대해 알아보자. 가장 쉬운 방법부터 조금 어려운(?) 조금 더 완벽한(?) 방법 순으로 진행해보겠다.
shellow copy
• spread operator
const obj1 = { a:1, b:2 };
const obj2 = { ...obj };
obj2.a = 3
console.log( obj1 === obj2 ) // false
console.log( obj1.a ) // 1
spread 연산자를 통해 값을 복사하는 방법이다. 단, 이 방법은 1depth까지만 가능하다.
Spread 문법은 배열을 복사할 때 1 레벨 깊이에서 효과적으로 동작합니다. 그러므로, 다음 예제와 같이 다차원 배열을 복사하는것에는 적합하지 않을 수 있습니다. . (Object.assign()과 전개 구문이 동일합니다)
• Object.assign()
const obj1 = { a:1, b:2 };
const obj2 = Object.assign({}, obj1);
obj2.a = 3;
console.log( obj1 === obj2 ) // false
console.log( obj1.a ) // 1
MDN에 나와있듯이 spread operator와 동일하게 1depth까지만 작동한다. 목표 객체의 속성 중 출처 객체와 동일한 키를 갖는 속성의 경우, 그 속성 값은 출처 객체의 속성 값으로 덮어쓴다. 항상 뒤에 오는 값이 앞 값을 덮어쓴다고 생각하면 된다.
Object.assign()은 단지 값을 복사할 뿐만 아니라 get(접근자), set(생성자)을 호출한다. Object.assign()은 속성을 단순히 복사하거나 새로 정의하는 것이 아니라, 할당(assign)하는 것이다.
❗️ 주의
위 두 방법은 1 depth까지만 동작한다고 했다. 그렇다면 depth가 1이상인 객체에 이를 사용하면 어떻게 될까?
const obj1 = { a: { b:1, c:1 }, d: 2};
const obj2 = { ...obj1};
obj1.a.b = 100;
console.log(obj1 === obj2) // false
console.log(obj2.a.b) // 100
obj1과 obj2를 비교해보면 주소 값이 다른 것처럼 나온다. 하지만 이는 완벽한 깊은 복사에 의한 값이 아니다. 깊은 복사는 가장 바깥쪽 depth만 되었을뿐이다. 두번째 이상의 depth들은 얕은복사가 된다. 따라서 depth가 2이상일때는 깊은복사를 하고 싶다면 이와 같은 방법을 사용하면 원하는 값을 얻지 못한다.
진짜 Deep Copy
원본 객체를 완전하게 연결고리 없이 복사하는 방법을 알아보자.
• JSON.stringfy(), JSON.parse()
JSON.parse(JSON.stringify(obj))
문자열로 변환하는 순간(stringify) 참조 값이 끊기게 된다. 이후 이 문자열을 parse하여 새로운 Object를 만들 수 있다.
const obj1 = {
a: 1,
b: 2,
c: function() {
return 1;
}
};
const obj2 = JSON.parse(JSON.stringify(obj1));
console.log(obj2); // {a: 1, b: 2}
다만 이 방법에 한가지 문제가 있다. stringify 메소드는 기본적으로 function 의 경우 undefined 로 처리한다. 따라서 Object내에 function이 있을 경우 이 방법은 좋은 방법이 아닐 수 있다.
const mySet = new Set([1,2,3,1]);
const obj3 = {
a: 1,
b: 2,
c: mySet
}
const obj4 = JSON.parse(JSON.stringify(obj3));
console.log(obj4) // {a: 1, b: 2, c: [1, 2, 3]}
객체 내에 set이나 map이 있을 경우에도 이를 배열로 복사하기 때문에 원하는 결과물을 얻을 수 없다. 만약 이를 set이나 map으로 다시 변환하고 싶다면 new Set이나 new Map에 배열값을 넣는 과정을 거쳐야한다.
만약 엄청나게 많은 양을 JSON방법으로 복사한다면 stringfy->parse하는 과정에서 성능상으로 좋은 결과를 내지 못할 수 있으므로 이를 고려해야한다.
• 재귀함수
function deepCopy(obj) {
if (typeof obj !== "object" || obj === null) {
return obj;
}
if (obj instanceof Date) {
return new Date(obj.getTime());
}
if (obj instanceof Array) {
return obj.reduce((arr, item, i) => {
arr[i] = deepCopy(item);
return arr;
}, []);
}
if (obj instanceof Object) {
if (obj instanceof Set) {
const copySet = new Set();
obj.forEach((value) => {
copySet.add(deepCopy(value));
});
return copySet;
}
if (obj instanceof Map) {
const copyMap = new Map();
obj.forEach((key, value) => {
copyMap.set(deepCopy(key), deepCopy(value));
});
return copyMap;
}
return Object.keys(obj).reduce((newObj, key) => {
newObj[key] = deepCopy(obj[key]);
return newObj;
}, {});
}
}
가장 확실한 방법은 직접 복사하는 함수를 만드는 것이다. 우선은 내가 필요한 정도의 deep copy를 구현해보았다. JSON을 사용하는 것과 동일하게 함수는 복사하지 못한다. 결과는 아래와 같다.
const mySet = new Set([1, 2, 3, 4, 3]);
const myMap = new Map([
["cucumber", 500],
["tomatoes", 350],
["onion", 50],
]);
const obj = {
a: 1,
b: 2,
c: {
d: "hello",
e: 3,
},
testSet: mySet,
testMap: myMap,
};
console.log(deepCopy(obj));
//{a: 1, b: 2, c: {…}, testSet: Set(4), testMap: Map(3)}
이제 방법을 알았으니 가장 편하고 확실한 방법인 lodash를 사용하면 된다. lodash의 경우 함수까지 복사를 해준다. lodash의 clonedeep을 살펴보자.
https://github.com/lodash/lodash
참고
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/Spread_syntax
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
'JavaScript' 카테고리의 다른 글
[Javascript] Event Loop (0) | 2022.04.24 |
---|---|
[Javascript] Event (0) | 2022.03.20 |
웹팩(webpack) 알러지 치료하기 - 1 (0) | 2022.02.27 |
[javaScript] export와 export default의 차이점 (0) | 2021.11.30 |
[Javascript] Async, Await (0) | 2021.11.01 |