Javascript:Shallow and Deep Copy :: 마이구미
이 글은 자바스크립트에서 객체 복사에 대해 다룬다.
크게는 얕은 복사(Shallow Copy)와 깊은 복사(Deep Copy) 로 분류된다.
본인은 단순히 두가지의 복사에 대해 참조 여부로 판단하고있었다.
그로 인해, 이번 포스팅 과정에서 오해와 명확하지 않은 것들이 많이 존재했다.
글을 읽는다면, 복사에서 더 나아가 조금 더 자바스크립트를 이해할 수 있을 것이다.
참고 링크 - https://scotch.io/bar-talk/copying-objects-in-javascript
단순 복사
객체 복사에 있어, 가장 순수하게 생각할 수 있는 것은 대입 연산자(=) 를 통한 복사이다.
이것은 단순 복사라고 칭하겠다.
let mainObject = { a: 1, b: 2 }; let copyObject = mainObject; copyObject.a = 5; console.log(mainObject.a) // 5 console.log(copyObject.a) // 5 | cs |
copyObject 객체는 mainObject 와 동일한 key-value 를 가지고 있다.
하지만 값을 수정할 때, 두 객체 모두 동일하게 변화하는 모습을 볼 수 있다.
왜냐하면 대입 연산자는 원본 객체의 주소를 할당한다.
그 결과 원본 객체의 주소를 참조하게 하는 행위가 된다.
위 그림처럼 서로 다른 변수는 같은 객체를 바라보고 있다.
주소를 참조하고 있다는 것을 이해하고 있지 않다면, 원하는 결과를 얻을 수 없다.
이러한 방식은 객체의 불변성을 깨뜨리고, 버그를 유발할 수 있다.
단순 복사는 실질적인 복사(얕은 복사, 깊은 복사)의 이해에 도움을 주기 위해 사용한 의미이다.
사실상 "복사"보다는 "참조"에 가깝다.
우리가 원하는 복사는 원본 객체를 기준으로 생성되는 새로운 객체를 의미한다.
위처럼 단순 복사는 원하는 결과를 만들어낼 수 없다.
가장 순수하게 생각할 수 있는 방식은 새로운 객체에 원본 객체의 각 속성과 값을 그대로 생성해버리면 된다.
단순하게 루프를 통해 해결할 수 있다.
function copy(mainObject) { let newObject = {}; for (let key in mainObject) { newObject[key] = mainObject[key]; } return newObject; } const mainObject = { a: 1, b: 2, c: { x: 3, y: 4 } }; let copyObject = copy(mainObject); copyObject.a = 100; console.log(copyObject.a); // => 100; console.log(mainObject.a); // => 1; | cs |
이는 순수하고 조금 오래된 방식의 복사의 한 예제 코드이다.
결과적으로 새로운 객체는 원본 객체의 값을 정확히 복사한 상태이다.
그로 인해, 값을 변경해도 서로 영향을 끼치지 않고 변경된 사실을 알 수 있다.
하지만 다음과 같은 경우에는 또 원치 않는 결과가 발생한다.
copyObject.c.x = 100; console.log(copyObject.c.x); // => 100; console.log(mainObject.c.x); // => 100; | cs |
지금까지는 본 주제를 위한 밑바탕을 그렸다고 보면 된다.
객체 복사에 대한 이해와 문제점들을 어렴풋이 알 수 있다.
이러한 것을 바탕으로 실질적인 복사 방식을 통해 더 자세히 들어가본다.
실제 복사는 목적에 따라 "얕은 복사"와 "깊은 복사" 방식으로 분류된다.
단순 복사에서 발생하는 문제점들을 두 가지 방식으로 해결해보자.
얕은 복사(Shallow Copy)
가장 순수하게 생각할 수 있는 방식은 새로운 객체에 원본 객체의 각 속성과 값을 그대로 생성해버리면 된다.
위에서 언급한 순수한 방식으로 볼 수 있다.
이 방식을 전문 용어로 얕은 복사라고 칭한다.
얕은 복사란 새로운 객체에 원본 객체의 프로퍼티의 값을 정확히 복사한다.
하지만 만약 프로퍼티의 값이 객체 형태라면, 객체의 주소를 복사한다.
즉, 복사된 객체는 원본 객체와 동일한 프로퍼티와 값들을 새롭게 가지지만, 주소가 복사된 프로퍼티는 새로운 형태가 아닌 같은 것을 공유하게 된다.
위와 같은 흐름을 얕은 복사라고 칭한다.
이를 구현하기 위해 현재는 주로 Object.assign() 을 사용한다.
Object.assign 메소드는 원본 객체의 열거가능한 모든 프로퍼티를 복사해준다.
const mainObject = { a: 1, b: 2, c: { x: 3, y: 4 } }; let copyObject = Object.assign({}, mainObject); | cs |
얕은 복사의 가장 큰 이슈는 서로 공유하는 객체가 존재한다는 것이다.
이와 같은 이슈들을 정리하면 다음과 같다.
- Property Descriptors 가 복사되지 않는다.
- 프로토타입 체인 또는 열거가능하지 않은 프로퍼티는 복사되지 않는다.
- 기본형 타입은 변경해도 서로 영향을 끼치지 않지만, 참조형 타입인 객체는 공유하고 있기 때문에 영향을 끼치게된다.
1. Property Descriptors
const bike = { name: 'C0101010', maker:'BMW', engine:'1000cc' }; Object.defineProperty(bike, 'engine', {writable: false}); Object.getOwnPropertyDescriptor(bike, 'engine'); // {value: "1000cc", writable: false, enumerable: true, configurable: true} let copy = Object.assign({}, bike); Object.getOwnPropertyDescriptor(copy, 'engine'); // {value: "1000cc", writable: true, enumerable: true, configurable: true} | cs |
보다시피 얕은 복사를 통해서는 descriptor 는 복사되지 않는다.
이것은 Object.assign() 뿐만 아니라 루프 방식으로 해도 동일하다.
얕은 복사 방식은 위와 같은 결과를 낸다.
2. Prototype Chain or enumerable property
const chainObject = { a: 1 }; let newObject = Object.create(chainObject, { b: { value: 2 }, c: { value: 3, enumerable: true // defalut => false } }); console.log(newObject); // a => __proto__.a let copyObject = Object.assign({}, newObject); console.log(copyObject); // => {c: 3} | cs |
Object.create() 를 통해 새로운 객체는 chainObject 객체를 프로토타입으로 생성된다.
결과적으로 newObject 의 a 프로퍼티는 사실상 newObject.__proto__.a 라는 것을 콘솔을 통해 확인할 수 있다.
또한 c 프로퍼티만이 열거가 가능하게 생성되었다.
최종적으로 copyObject 객체는 c 프로퍼티만을 복사되었다.
a 프로퍼티는 prototype chain, b 프로퍼티는 enumerable: false 로써, 복사되지 않는다는 것을 알 수 있다.
언급한 이슈들을 모두 해결하기 위한 완벽한 복사는 깊은 복사를 통해 해결한다.
깊은 복사(Deep Copy)
깊은 복사는 원본 객체를 완전히 복사하게 된다.
얕은 복사에서 주소를 복사하는 것 또한 주소를 복사하지 않고, 새로운 메모리 공간을 확보해 생성하게 된다.
새로운 메모리 공간을 확보해서 생성하기 때문에, 완전히 다른 것을 의미한다.
위 그림을 보다시피 얕은 복사처럼 공유하는 문제가 없어지게 된다.
주로 깊은 복사를 구현하는 방식은 다음과 같다.
- 루프 => 재귀
- JSON
- 라이브러리(jQuery, lodash...)
처음에 언급한 순수한 방식의 루프를 재귀로 변경해주면 된다.
다음과 같은 형태의 코드를 작성할 수 있다.
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; } | cs |
하지만 짧고 간결하게 하고 싶을 것이다.
단순하게는 JSON 방식을 사용하기도 한다.
let copyObject = JSON.parse(JSON.stringify(mainObject)); | cs |
JSON 방식은 주의해서 사용해야한다.
stringify 메소드는 기본적으로 function 의 경우 undefined 로 처리한다.
const mainObject = { a: 1, b: 2, c: function() { return 1; } }; let copyObject = JSON.parse(JSON.stringify(mainObject)); // {a: 1, b: 2} | cs |
stringify => parse 과정을 보더라도 성능적으로도 좋지 않아 보일 것이다.
또한 JSON 이 지원되지 않는 환경에서는 당연히 사용하지못한다.
이외에도 관련된 많은 이슈들이 존재한다.
가장 쉽게 사용하는 법은 jquery, lodash 를 사용하는 것이다.
// jQuery let copyObject = $.extend(true, {}, mainObject); // lodash let copyObject = _.cloneDeep(mainObject); | cs |