최근 react, vue 개발을 하면서 객체의 얕은 복사(깊은 복사인 줄..) 때문에 시간을 많이 허비했다. 그렇기 때문에 블로그를 통해 개념을 다시 정리해보는 시간을 갖게되었다.
기본적으로 자바스크립트에서
const obj = { a: 1, b: 2}
const obj2 = obj
console.log(obj === obj2) // true
객체를 대입하게 되면, 참조에 의한 할당이 이루어지므로(얕은 복사) 둘은 같은 객체이다.
다른 사람에게 설명할 때, 껍데기라는 표현을 쓰는데 이 객체는 같은 껍데기를 참조하기 때문에, 둘은 같은 객체라고 빗대서 표현하는데 좀 어색한 부분도 있긴 하지만 나는 이런식으로 이해하고 있다.
얕은 복사가 이루어졌을 때, 주로 발생하는 문제점은 다음과 같다.
const obj = { a:1, b:2 }
const obj2 = obj
obj2.a = 100
console.log(obj.a) // 100
놀랍게도, obj2
객체를 수정했는데, obj
도 수정된 효과를 볼 수 있었다. 얕은 복사는 참조에 의한 할당이기 때문에, 내부 객체의 property
를 공유하게 된다.
그렇다면, 깊은 복사를 하기 위해서는 어떻게 해야할까?
const obj = { a:1, b:2 }
const obj2 = {...obj}
obj2.a = 100
console.log(obj === obj2) // false
console.log(obj.a) // 1
...(spread) 연산자를 통해 obj의 key-value 쌍을 복사하여 obj2에 할당한다. 결국 새로운 껍데기를 갖게 되었으므로, obj2와 obj는 다른 id를 갖게되고, 이는 깊은 복사라 할 수 있다.
라고, 알고 있었지만 전개 구문, 객체, 깊은 복사에 대해 잘못 이해하였기 때문에, 앞서 말했던 개발도중 대참사가 일어났다.
const obj = { a: { b:1, c:1 }, d: 2}
const obj2 = {...obj}
obj.a.b = 100
console.log(obj === obj2) // false
console.log(obj2.a.b) // 100
분명 앞에서 ...
연산자로 복사를 하고, 새로운 껍데기를 부여했기 때문에, 앞서 정의한 깊은 복사가 이루어진게 맞다. 하지만 obj2.a.b
의 결과를 봤을 때, 이는 앞에서 본 얕은 복사에서 일어났던 현상과 매우 비슷하다. 이게 어떻게 된 일일까?
사실 ...
연산자를 통해 복사한 건, 놀랍게도 얕은 복사이다. 앞서 ...
연산자를 활용해 복사를 했을 때, 깊은 복사가 된 것은 depth
가 한단계 깊이 였기 때문에 가능한 일이었다.
MDN의 전개 구문을 다시 확인해보니,
참고: Spread 문법은 배열을 복사할 때 1 레벨 깊이에서 효과적으로 동작합니다. 그러므로, 다음 예제와 같이 다차원 배열을 복사하는것에는 적합하지 않을 수 있습니다. (Object.assign() 과 전개 구문이 동일합니다)
var a = [[1], [2], [3]];
var b = [...a];
b.shift().shift(); // 1
// 이제 배열 a 도 영향을 받음: [[], [2], [3]]
라고 작성되어 있었다. 역시 공식문서에는 해답이 있다. 찾지 못했을 뿐...
결국 1레벨 깊이 이상의 객체를 깊은 복사하려면,
const obj = { a: { b:1, c:1 }, d: 2}
const obj2 = {...obj, a:{...obj.a}}
obj.a.b = 100
console.log(obj === obj2) // false
console.log(obj2.a.b) // 1
이런 식으로, 한 단계 더 깊이 들어가서 ...
연산자를 다시 활용해주어야 한다.
실제 개발에서 배열안에 객체를 다루는 일이 매우 많기 때문에, 이 점을 주의해서 개발해야 한다.
결론
1. ...
연산자는 얕은 복사이다.
2. 객체가 서로 다르다고 깊은 복사가 이루어진건 아니다.
3. 객체를 깊은 복사하려면, 객체의 깊이 끝까지 복사하거나, lodash
라이브러리를 사용하여 deep copy하자!