이전에 자바를 공부하면서도 만난적이 있고, 자바스크립트 공부를 하면서 다시 만나게 된 개념인 얕은 복사와 깊은복사...
직감적으로 중요한 개념이라는 생각이 들어서 정리해보고자 한다 🙃
우선 두 개념을 소개하기 전에 짚고 넘어가야 할 부분이 있다
관점에 따라서 얕은 복사와 깊은 복사의 개념이 다르게 해석될 수 있다는 점이다
원시 값을 할당한 변수를 다른 변수에 할당하는 것을 깊은 복사, 객체를 할당한 변수를 다른 변수에 할당하는 것을 얕은 복사라고 부르기도 하기 때문이다
let x = 1;
const y = x;
x = 2;
console.log(x); // 2
console.log(y); // 1
위의 경우에 대해 간단하게 설명하자면 원시 값을 할당한 변수를 다른 변수에 할당하는 경우, 새로운 메모리 공간에 값을 저장하고 해당 메모리 공간의 주소를 가리키게 한다. 그렇기 때문에 x값을 변경하여도 y에는 영향이 가지 않게 된다.
즉, y는 x와는 다른 별개의 값을 가리킨다.
const obj = { x: 1 };
const obj2 = obj;
console.log(obj); // { x: 1 }
console.log(obj2); // { x: 1 }
obj.x = 2;
console.log(obj); // { x: 2 }
console.log(obj2); // { x: 2 }
반면 객체를 할당한 변수를 다른 변수에 할당한 경우, 객체를 가리키는 참조값을 복사한다. 그렇게 되면 obj와 obj2는 같은 객체를 가리키게 되고, obj의 프로퍼티를 변경하게 되면 같은 객체를 가리키는 obj2의 프로퍼티 역시 변하게 된다.
즉, obj와 obj2는 같은 값을 가리키고 이는 의도치 않은 변경을 발생시킬 수 있다.
내가 이해하기에 깊은 복사란 대상을 완전하게 복사하여 둘 중 하나를 변경해도 영향을 주지 않도록 값을 할당하는 것을 의미한다.
반대로 얕은 복사란 대상의 값을 복사하였으나 참조값을 일부 공유하여 둘 중 하나의 값을 변경한 경우 영향이 발생하도록 값을 할당하는 것을 의미한다.
이러한 개념을 기반으로 객체에서의 얕은 복사와 깊은 복사에 대해 알아보자 🤔
우리는 위에서 객체를 할당 연산자인 =
으로 복사했을 때, 완전히 복사가 되지 않는다는 사실을 알게되었다
사실 복사라는 단어보다는 새로운 식별자가 똑같은 객체를 참조했다고 볼 수 있다 🙁
그렇다면 서로 변경했을 때 영향을 주지 않도록 복사하기 위해서는 어떻게 해야할까?
객체의 얕은 복사는 객체를 프로퍼티 값으로 갖는 객체의 경우 한 단계(1 depth)까지만 복사하는 것을 말한다.
얕은 복사 방법을 소개하고, 예시를 통해 깊은 복사와의 차이점을 보여주겠다.
Object.assign()
메서드는 출처 객체로부터 모든 열거할 수 있는(enumerable) 하나 이상의 속성들을 목표 객체로 복사한다.
Object.assign(target, ...sources)
const data = { x: { xx: 1 }, y: 2 };
const copy = Object.assign({}, data);
console.log(data === copy); // false
data.y = 3;
console.log(data); // { x: { xx: 1 }, y: 3 }
console.log(copy); // { x: { xx: 1 }, y: 2 }
할당 연산자로 객체를 복사했을 때와는 다르게 기존의 데이터를 변경하여도 복사본에는 영향이 가지 않는다.
const data = { x: { xx: 1 }, y: 2 };
const copy = { ...data };
console.log(data === copy); // false
마찬가지로 ES6의 스프레드 문법을 사용했을 경우에도 원하는대로 객체가 복사되었다.
const data = { x: { xx: 1 }, y: 2 };
const copy = { ...data };
console.log(data.x === copy.x); // true
data.x.xx = 2;
console.log(data); // { x: { xx: 2 }, y: 2 }
console.log(copy); // { x: { xx: 2 }, y: 2 }
예상과는 다르게 객체의 프로퍼티 값으로 객체가 할당된 경우, 완전히 복사하지 못하고 참조값을 복사한 것을 확인할 수 있었다
Object.assign()
메서드를 사용했을 때와 스프레드 연산자를 사용했을 때 모두 객체의 depth가 1일 때까지만 변경에 영향없이 완전히 복사되고 depth가 2 이상일 때부터는 원치 않는 변경이 발생하였다
그렇다면 객체의 중첩 정도와 관계없이 완전한 객체 복사본을 만들기 위해서는 어떻게 해야할까 🙄
const deepCopy = function (obj) {
const clone = {};
for (let key in obj) {
clone[key] = typeof obj[key] === 'object' && obj[key] !== null ? deepCopy(obj[key]) : obj[key];
}
return clone;
};
const data = { x: { xx: 1 }, y: 2 };
const copy = deepCopy(data);
console.log(data.x === copy.x); // false
data.x.xx = 2;
console.log(data); // { x: { xx: 2 }, y: 2 }
console.log(copy); // { x: { xx: 1 }, y: 2 }
복사의 대상이 객체이면 재귀적으로 함수를 호출하여 프로퍼티 값으로 객체가 온 경우까지 완전히 복사한다
직접 코드를 구현하여 해결하는 방법
lodash를 사용하기 위해서는 터미널에서 npm install lodash
를 설치한 후, Node.js 환경에서 실행해야 한다
const lodash = require('lodash');
const data = { x: { xx: 1 }, y: 2 };
const copy = lodash.cloneDeep(data);
console.log(data.x === copy.x); // false
재귀 함수를 이용한 복사와 마찬가지로 깊은 복사가 원하는대로 실행됐다 🥳
얕은 복사와 깊은 복사를 학습하면서 들었던 가장 궁금한 점은 '얕은 복사가 활용될 일이 있을까?'였다
얕은 복사는 의도치 않은 변경이 생겨 에러를 발생시킬 수 있다
하지만 개발자의 의도하에 얕은 복사를 활용해서 프로그래밍 하는 경우가 있을 수도 있지 않을까 하는 생각이 들었다
만약 그것이 아니라면 얕은 복사는 지양해야 하는 것이고, 깊은 복사를 사용하는 법만 배우면 될테니 말이다
reference
이웅모, 모던 자바스크립트 Deep Dive
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
https://falsy.me/javascript-6-%EA%B0%9D%EC%B2%B4%EC%9D%98-%EC%96%95%EC%9D%80-%EB%B3%B5%EC%82%AC%EC%99%80-%EA%B9%8A%EC%9D%80-%EB%B3%B5%EC%82%AC%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B4%85%EB%8B%88%EB%8B%A4/