객체를 복사하는 과정에 있어서 객체가 프로퍼티의 값으로 존재해 이중객체의 형태를 띄는 경우, 복사하는 방법은 얕은 복사
와 깊은 복사
두 가지 방법으로 나뉜다.
const obj = {
a: 1,
b: { c: 1 },
c: 3
};
위와 같은 객체를 예로 들면, 프로퍼티 키(이하 키)가 a와 c의 경우 원시값을 프로퍼티 값(이하 값)으로 갖는데 키가 b인 경우는 객체를 값으로 갖는다. 원시값을 값으로 가진 a와 c를 일반 객체, 객체를 값으로 가진 b를 이중객체라고 볼 수 있다.
얕은 복사는 원시값을 값으로 갖는 a와 c는 복사해 새로운 객체를 생성하지만 객체를 값으로 갖는 b의 경우 참조값을 복사해 참조에 의한 전달 상태를 유지한다.
깊은 복사는 원시값을 값으로 갖는 a와 c 뿐만 아니라 객체를 값으로 갖는 b의 값까지 복사해 새로운 객체를 생성한다.
예시 코드를 통해 얕은 복사와 깊은 복사의 차이점을 알아보자.
얕은 복사를 하는 방법은 3가지 정도가 있다. 어떤 방식을 선택하든 모두 같은 결과를 보여준다.
// 스프레드 연산자로 변수 obj를 복사해 변수 objCopy에 할당한다
const objCopy = { ...obj };
// 콘솔에 찍었을 때, 두 객체는 같은 값을 가진 것처럼 보이지만 실제로는 다른 객체이다
console.log(obj); // {a: 1, b: { c: 1 }, c: 3}
console.log(objCopy); // {a: 1, b: { c: 1 }, c: 3}
console.log(obj === objCopy); // false
// 그러나 객체를 값으로 갖는 객체를 비교하면 같은 값이라고 판단한다
console.log(obj.b === objCopy.b); // true
console.log(obj.b.c === objCopy.b.c); // true
Object.assign()
으로 객체 복사하기const objCopy = Object.assign({}, obj);
console.log(objCopy); // {a: 1, b: { c: 1 }, c: 3}
console.log(obj === objCopy); // false
console.log(obj.b === objCopy.b); // true
console.log(obj.b.c === objCopy.b.c); // true
for ... in
문 사용해서 객체 복사하기function shallow(obj) {
const copy = {};
for (const key in obj) {
copy[key] = obj[key];
}
return copy;
}
const objCopy = shallow(obj);
console.log(objCopy); // {a: 1, b: { c: 1 }, c: 3}
console.log(obj === objCopy); // false
console.log(obj.b === objCopy.b); // true
console.log(obj.b.c === objCopy.b.c); // true
변수 obj라는 객체 자체는 새로운 객체로 복사되어 변수 objCopy에 다른 메모리 주소를 갖는 객체를 형성하지만, 객체를 값으로 갖는 키는 다른 메모리 주소를 갖는 객체로 복사되지 않고 변수 obj와 같은 메모리 주소를 공유하는 방식으로 복사된다.
쉽게 말하면 겉만 복사되는 것인데, 이 방법은 아래와 같은 문제를 일으킬 수 있다.
objCopy.b.f = 4;
console.log(obj); // {a: 1, b: { c: 1, f: 4 }, c: 3}
console.log(obj.b === objCopy.b); // true
console.log(obj.b.f === objCopy.b.f); // true
복사한 변수 objCopy에 f: 4
라는 프로퍼티를 추가했는데 복사했던 변수 obj는 다른 메모리 주소를 갖고 있음에도 불구하고 f:4
라는 프로퍼티가 추가된 것을 볼 수 있다.
원본을 변경하지 않기 위해서 새로운 메모리 주소를 갖도록 객체를 복사한 경우라면 변경하고자 했던 객체만 변경되는 것이 아니라 원본까지 변경되는 이런 상황이 큰 문제가 될 수 있다. 따라서 객체를 값으로 갖는 프로퍼티까지 다른 메모리 주소를 갖는 완전히 새로운 객체를 복사하고자 할 때는 깊은 복사를 사용해야 한다.
lodash
라이브러리$ npm install lodash
// lodash 명령어 입력
const _ = require('lodash');
console.log(_);
// lodash가 적용된 변수를 활용해 깊은 복사하기
const objCopy = _.cloneDeep(obj);
console.log(objCopy); // {a: 1, b: { c: 1 }, c: 3}
console.log(obj === objCopy); // false
console.log(obj.b === objCopy.b); // false
// 객체의 내부에 있는 객체의 값들을 비교했을 때만 true가 나온다.
console.log(obj.c === objCopy.c); // true
Json.parse(Json.stringify())
이용해 깊은 복사하기const objCopy = JSON.parse(JSON.stringify(obj));
console.log(objCopy); // {a: 1, b: { c: 1 }, c: 3}
console.log(obj === objCopy); // false
console.log(obj.b === objCopy.b); // false
console.log(obj.b.c === objCopy.b.c); // true
for ... in
문 사용해서 깊은 복사하기function deep(obj) {
const copy = {};
for (const key in obj) {
if (typeof obj[key] === 'object') {
copy[key] = deep(obj[key]);
} else {
copy[key] = obj[key];
}
}
return copy;
}
const objCopy = deep(obj);
console.log(objCopy); // {a: 1, b: { c: 1 }, c: 3}
console.log(obj === objCopy); // false
console.log(obj.b === objCopy.b); // false
console.log(obj.b.c === objCopy.b.c); // true
깊은 복사를 사용한 방법의 결과를 보면 객체를 값으로 갖는 프로퍼티를 직접 비교(obj.b.c === objCopy.b.c
)하지 않으면 모두 false를 반환하는 것을 볼 수 있다.
때문에 아래와 같이 복사된 객체에 프로퍼티를 추가하더라도 원본 객체는 영향을 받지 않아 undefined
를 반환하는 것을 볼 수 있다. 서로 영향을 주고 받지 않는 아예 다른 객체이기 때문이다.
objCopy.b.f = 4;
console.log(obj); {a: 1, b: { c: 1 }, c: 3}
console.log(obj.b === objCopy.b); // false
console.log(obj.b.f === objCopy.b.f); // false
console.log(obj.b.f); // undefined
console.log(objCopy.b.f); // 4
즉, 객체를 복사해서 사용할 때는 객체 내부에 값을 객체로 갖는 프로퍼티가 있는지를 확인해서 어떤 방식으로 복사할지 판단해야 한다. 이중 배열 등의 객체 내부에 객체가 있는 형태의 객체를 얕은 복사로 사용하면 뜻하지 않게 값이 바뀌는 일이 발생할 수 있기 때문에 주의해야 한다.