깊은 복사, 얕은 복사
자바스크립트에서 값은 원시값과 참조값 두 가지 데이터 타입의 값이 존재한다.
원시값은 기본 자료형(단순한 데이터)을 의미한다. Number, String, Boolean, Null, Undefined 등이 해당한다. 변수에 원시값을 저장하면 변수의 메모리 공간에 실제 데이터 값이 저장된다. 할당된 변수를 조작하려고 하면 저장된 실제 값이 조작된다.
참조값은 여러 자료형으로 구성되는 메모리에 저장된 객체다. Object, Symbol등이 여기에 해당된다. 변수에 객체를 저장하면 독립적인 메모리 공간에 값을 저장하고, 변수에 저장된 메모리 공간의 참조(위치 값)를 저장하게 된다. 그래서 할당된 변수를 조작하는 것은 사실 객체 자체를 조작하는 것이 아닌, 해당 객체의 참조를 조작하는 것이다.
원시값을 복사할 때 그 값은 또 다른 독립적인 메모리 공간에 할당하기 때문에, 복사를 하고 값을 수정해도 기존 원시값을 저장한 변수에는 영향을 끼치지 않는다. 이처럼 실제 값을 복사하는 것을 깊은 복사라고 한다. 하지만 이것은 자료형을 깊은 복사한 것이다.
const a = 'a';
let b = 'b';
b = 'c';
console.log(a); // 'a';
console.log(b); // 'c';
// 기존 값에 영향을 끼치지 않는다.
참조값을 복사할 때는 변수가 객체의 참조를 가리키고 있기 때문에 복사된 변수 또한 객체가 저장된 메모리 공간의 참조를 가리키고 있다. 그래서 복사를 하고 객체를 수정하면 두 변수는 똑같은 참조를 가리키고 있기 때문에 기존 객체를 저장한 변수에 영향을 끼친다. 이처럼 객체의 참조값(주소값)을 복사하는 것을 얕은 복사라고 한다.
const a = {
one: 1,
two: 2,
};
let b = a;
b.one = 3;
console.log(a); // { one: 3, two: 2 } 출력
console.log(b); // { one: 3, two: 2 } 출력
// 기존 값에 영향을 끼친다.
일반적으로 복사라는 개념을 생각한다면 깊은 복사가 떠오를 것이다.
하지만 객체를 복사할 때 '=' 키워드를 사용해서 복사하면 얕은 복사가 돼서 기존 변수 또한 수정된다. 그렇다면 자바스크립트에서 얕은 복사와 깊은 복사를 어떻게 할 수 있을까?
얕은 복사 (shallow copy) 방법
" 얕은 복사란 객체를 복사할 때 기존 값과 복사된 값이 같은 참조를 가리키고 있는 것을 말한다. 객체 안에 객체가 있을 경우 한 개의 객체라도 기존 변수의 객체를 참조하고 있다면 이를 얕은 복사라고 한다. "
얕은 복사 방법의 대표적인 예라고 할 수 있다.
start부터 end 인덱스까지 기존 배열에서 추출해서 새로운 배열을 리턴하는 메소드이다.
만약 start와 end를 설정하지 않는다면, 기존 배열 전체를 얕은 복사한다.
const original = ['a',2,true,4,"hi"];
const copy = original.slice();
console.log(JSON.stringify(original) === JSON.stringify(copy)); // true
copy.push(10);
console.log(JSON.stringify(original) === JSON.stringify(copy)); // false
console.log(original); // [ 'a', 2, true, 4, 'hi' ]
console.log(copy); // [ 'a', 2, true, 4, 'hi', 10 ]
기존 배열에는 영향을 끼치지 않아서 깊은 복사로 보일 수 있지만, 원시값을 저장한 1차원 배열일 뿐이다. 원시값은 기본적으로 깊은 복사이다.
Slice() 메소드는 기본적으로 얕은 복사를 수행한다.
const original = [
[1, 1, 1, 1],
[0, 0, 0, 0],
[2, 2, 2, 2],
[3, 3, 3, 3],
];
const copy = original.slice();
console.log(JSON.stringify(original) === JSON.stringify(copy)); // true
// 복사된 배열에만 변경과 추가.
copy[0][0] = 99;
copy[2].push(98);
console.log(JSON.stringify(original) === JSON.stringify(copy)); // true
console.log(original);
// [ [ 99, 1, 1, 1 ], [ 0, 0, 0, 0 ], [ 2, 2, 2, 2, 98 ], [ 3, 3, 3, 3 ] ] 출력
console.log(copy);
// [ [ 99, 1, 1, 1 ], [ 0, 0, 0, 0 ], [ 2, 2, 2, 2, 98 ], [ 3, 3, 3, 3 ] ] 출력
만약 1차원 배열이 아닌 중첩 구조를 갖는 2차원 배열이면 얕은 복사를 수행하게 된다.
const original = [
{
a: 1,
b: 2,
},
true,
];
const copy = original.slice();
console.log(JSON.stringify(original) === JSON.stringify(copy)); // true
// 복사된 배열에만 변경.
copy[0].a = 99;
copy[1] = false;
console.log(JSON.stringify(original) === JSON.stringify(copy)); // false
console.log(original);
// [ { a: 99, b: 2 }, true ]
console.log(copy);
// [ { a: 99, b: 2 }, false ]
이건 또 다른 예시이다. 배열 안에 객체를 수정하려고 하면 얕은 복사를 수행하는 것을 볼 수 있다. 하지만 원시값은 기본적으로 깊은 복사라 기존 변수에 있는 값과는 다른 값을 도출하는 것을 볼 수 있다.
Object.assign(생성할 객체, 복사할 객체)
메소드의 첫번째 인자로 빈 객체를 넣어주고, 두번째 인자로 복사할 객체를 넣어주면 된다.
const object = {
a: "a",
number: {
one: 1,
two: 2,
},
};
const copy = Object.assign({}, object);
copy.number.one = 3;
console.log(object === copy); // false
console.log(object.number.one === copy.number.one); // true
복사된 객체 copy 자체는 기존 object와 다른 객체지만 그 안에 들어가 있는 값은 기존 object안의 값과 같은 참조 값을 가리키고 있다.
MDN에서는 Object.assign()에 대해 이렇게 설명했다.
마찬가지로 얕은 복사다.
const object = {
a: "a",
number: {
one: 1,
two: 2,
},
};
const copy = {...object}
copy.number.one = 3;
console.log(object === copy); // false
console.log(object.number.one === copy.number.one); // true
깊은 복사 (Deep copy) 방법
" 깊은 복사된 객체는 객체 안에 객체가 있을 경우에도 원본과의 참조가 완전히 끊어진 객체를 말한다. "
복사를 하는 목적은 기존 객체의 값만 복사본으로 가져와 별도로 활용하기 위함이 대부분이라고 생각한다. 기존 객체까지 건드린다면 이것은 복사를 하는 목적에 벗어난다고 생각한다. (개인적인 생각)
JSON.stringify()는 객체를 json 문자열로 변환하는데, 이 과정에서 원본 객체와의 참조가 모두 끊어진다.
객체를 json 문자열로 변환 후, JSON.parse()를 이용해 다시 원래 객체(자바스크립트 객체)로 만들어준다.
이 방법이 가장 간단하고 쉽지만 다른 방법에 비해 느리다는 것과 객체가 function일 경우, undefined로 처리한다는 것이 단점이다.
const object = {
a: "a",
number: {
one: 1,
two: 2,
},
arr: [1, 2, [3, 4]],
};
const copy = JSON.parse(JSON.stringify(object));
copy.number.one = 3;
copy.arr[2].push(5);
console.log(object === copy); // false
console.log(object.number.one === copy.number.one); // false
console.log(object.arr === copy.arr); // false
console.log(object); // { a: 'a', number: { one: 1, two: 2 }, arr: [ 1, 2, [ 3, 4 ] ] }
console.log(copy); // { a: 'a', number: { one: 3, two: 2 }, arr: [ 1, 2, [ 3, 4, 5 ] ] }
복잡하다는 것이 단점이다.
const object = {
a: "a",
number: {
one: 1,
two: 2,
},
arr: [1, 2, [3, 4]],
};
function deepCopy(object) {
if (object === null || typeof object !== "object") {
return object;
}
// 객체인지 배열인지 판단
const copy = Array.isArray(object) ? [] : {};
for (let key of Object.keys(object)) {
copy[key] = deepCopy(object[key]);
}
return copy;
}
const copy = deepCopy(object);
copy.number.one = 3;
copy.arr[2].push(5);
console.log(object === copy); // false
console.log(object.number.one === copy.number.one); // false
console.log(object.arr === copy.arr); // false
console.log(object); // { a: 'a', number: { one: 1, two: 2 }, arr: [ 1, 2, [ 3, 4 ] ] }
console.log(copy); // { a: 'a', number: { one: 3, two: 2 }, arr: [ 1, 2, [ 3, 4, 5 ] ] }
라이브러리를 사용하면 더 쉽고 안전하게 깊은 복사를 할 수 있다. 설치를 해야 한다는 점과 일반적인 개발에는 효율적이겠지만, 코딩 테스트에는 사용할 수 없다는 것이 단점이다.
const deepCopy = require("lodash.clonedeep")
const object = {
a: "a",
number: {
one: 1,
two: 2,
},
arr: [1, 2, [3, 4]],
};
const copy = deepCopy(object);
copy.number.one = 3;
copy.arr[2].push(5);
console.log(object === copy); // false
console.log(object.number.one === copy.number.one); // false
console.log(object.arr === copy.arr); // false
console.log(object); // { a: 'a', number: { one: 1, two: 2 }, arr: [ 1, 2, [ 3, 4 ] ] }
console.log(copy); // { a: 'a', number: { one: 3, two: 2 }, arr: [ 1, 2, [ 3, 4, 5 ] ] }![](https://velog.velcdn.com/images/sehee-xx/post/09fd85aa-bf45-4fad-ad63-b206cfc2980a/image.png)