"깊은 복사와 얕은 복사에 대해 설명해주세요."
이 질문은 면접에서 자주 나오는 질문은 아니다.
그러나 객체를 다루려면 반드시 알아야 하는 개념이라고 생각한다.
게다가 이번 포스팅에서 설명하는 개념은 '불변성'과도 관련된 개념이다.
불변성을 유지하는 방법에 관해서는 면접에서 자주 나오는 질문이니 개념을 잘 알아두자.
JavaScript의 데이터는 크게 두 종류로 분류할 수 있다.
원시 타입에는 문자(string)
, 숫자(number)
, bigint
, 불린(boolean)
, 심볼(symbol)
, null
, undefined
가 있다.
예시:
let message = "Hello!"; let phrase = message;
예시를 실행하면 두 개의 독립된 변수에 각각 문자열 "Hello!"가 저장됩니다.
참조 타입에는 Object
, Array
, Function
, Date
, RegExp
, 그리고 ES6에서 추가된 Map
, WeakMap
, Set
, WeakSet
이 있다.
참조 타입의 동작 방식은 원시 타입과 다르다.
변수에 참조 타입의 데이터가 그대로 저장되는 것이 아니라, 참조 타입의 데이터가 저장되어 있는 '메모리 주소'인 '참조 값'이 저장된다.
그림을 통해 변수 user에 객체를 할당할 때 무슨 일이 일어나는지 알아봅시다.
let user = { name: "John" };
객체는 메모리 내 어딘가에 저장되고, 변수 user엔 객체를 '참조’할 수 있는 값이 저장됩니다.
따라서 객체가 할당된 변수를 복사할 땐 객체의 참조 값이 복사되고 객체는 복사되지 않습니다.
예시:let user = { name: "John" }; let admin = user; // 참조값을 복사함
변수는 두 개이지만 각 변수엔 동일 객체에 대한 참조 값이 저장되죠.
따라서 객체에 접근하거나 객체를 조작할 땐 여러 변수를 사용할 수 있습니다.let user = { name: 'John' }; let admin = user; admin.name = 'Pete'; // 'admin' 참조 값에 의해 변경됨 alert(user.name); // 'Pete'가 출력됨. 'user' 참조 값을 이용해 변경사항을 확인함
객체를 서랍장에 비유하면 변수는 서랍장을 열 수 있는 열쇠라고 할 수 있습니다. 서랍장은 하나, 서랍장을 열 수 있는 열쇠는 두 개인데, 그중 하나(admin)를 사용해 서랍장을 열어 정돈한 후, 또 다른 열쇠로 서랍장을 열면 정돈된 내용을 볼 수 있습니다.
➕보충 설명
위의 코드에서 admin.name = 'Pete'
으로 변경했는데, 왜 user.name
도 Pete가 나오는지 이해가 안 될 수도 있다.
우선 admin.name = 'Pete'
코드 이후, user
와 admin
을 다음과 같이 비교해보면 true
가 나올 것이다.
console.log(user === admin); //true
이런 결과가 나온 이유는 '프로퍼티'를 변경시켰기 때문이다.
프로퍼티 name
만 변경되었고 (엄밀히 말하면 프로퍼티 name의 주소가 변경된 것) user
, admin
이 바라보는 프로퍼티의 그룹인 '객체(그림에서의 서랍)'의 주소 자체는 변경되지 않은 것이다.
이처럼, 객체를 복사할 때 =
을 사용하면, 기존 값과 복사된 값이 동일한 객체를 참조한다는 것을 알게 되었다.
이것이 바로 '얕은 복사'이다.
얕은 복사란 객체를 복사할 때 기존 값과 복사된 값이 같은 참조를 가리키고 있는 것을 말합니다.
객체 안에 객체가 있을 경우 한 개의 객체라도 기존 변수의 객체를 참조하고 있다면 이를 얕은 복사라고 합니다.
=
을 사용하는 것 외에 얕은 복사를 할 수 있는 다른 방법을 소개하겠다.
얕은 복사 방법의 대표적인 예다.
arr.slice([start], [end])
이 메서드는 "start" 인덱스부터 ("end"를 제외한) "end"인덱스까지의 요소를 복사한 새로운 배열을 반환한다.
start와 end를 설정하지 않는다면, 기존 배열의 전체를 얕은 복사한 새로운 배열을 만들 수 있다.
const original = ['a',2,true,4,"hi"];
const copy = original.slice();
console.log(JSON.stringify(original) === JSON.stringify(copy)); // true
console.log(original === copy); //false
어라? 배열의 내용은 같은데 original과 copy가 다른 배열이라고 한다.
그러면 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 ] ]출력
copy
배열에만 변경 및 추가를 했는데, original
배열도 같이 변경되었다.
어떻게 된 일일까?
1차원 배열이 아닌 중첩 구조를 갖는 2차원 배열이면 얕은 복사를 수행하게 됩니다.
우리는 앞서 '얕은 복사'의 정의를 다음과 같이 정의했다.
객체 안에 객체가 있을 경우 한 개의 객체라도 기존 변수의 객체를 참조하고 있다면 이를 얕은 복사라고 합니다.
copy
배열과 original
배열 자체는 같은 배열을 참조하는 것이 아니다.
그러나 copy
와 original
안에 있는 배열은 같은 배열을 참조하고 있다.
하나의 데이터라도 기존 변수의 데이터를 참조하고 있다면 이는 얕은 복사다.
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(dest, [src1, src2, src3...])
- 첫 번째 인수 dest는 목표로 하는 객체입니다.
- 이어지는 인수 src1, ..., srcN는 복사하고자 하는 객체입니다. ...은 필요에 따라 얼마든지 많은 객체를 인수로 사용할 수 있다는 것을 나타냅니다.
- 객체 src1, ..., srcN의 프로퍼티를 dest에 복사합니다. dest를 제외한 인수(객체)의 프로퍼티 전부가 첫 번째 인수(객체)로 복사됩니다.
- 마지막으로 dest를 반환합니다.
- 출처: 모던 JavaScript 튜토리얼 - 참조에 의한 객체 복사
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); // tru
복사된 객체
copy
자체는 기존object
와 다른 객체지만 그 안에 들어가 있는 값은 기존object
안의 값과 같은 참조 값을 가리키고 있습니다.
spread 연산자를 사용하면 얕은 복사가 된 것을 확인할 수 있다.
const object = {
a: "a",
number: {
one: 1,
two: 2,
},
};
const copy = {...object}
console.log(object === copy); // false
console.log(object.number === copy.number); // true
object와 copy는 다른 객체지만 object와 copy 안의 number 객체는 동일한 객체를 참조하는 것을 알 수 있다.
그러면 참조를 모두 끊어내고, 참조 타입 데이터의 내부에 있는 모든 값을 새로운 값으로 만드는 방법은 없을까? 있다! 그건 바로 '깊은 복사' 방법이다.
깊은 복사란, 기존 값의 모든 참조가 끊어지는 것을 말한다. 특히 복사할 때, 참조형 타입 값(객체)에서 내부에 있는 모든 값이 새로운 값이 되는 것을 말한다.
깊은 복사된 객체는 객체 안에 객체가 있을 경우에도 원본과의 참조가 완전히 끊어진 객체를 말합니다.
JSON.parse(JSON.stringify(obj))
JSON.stringify()
는 JavaScript 값이나 객체를 json 문자열로 변환한다.
이 과정에서 원본 객체와의 참조가 모두 끊어진다!
객체를 json 문자열로 변환 후, JSON.parse()
를 이용하여 다시 객체로 변환한다.
이 방법의 단점은 다른 방법에 비해 느리고, 객체가 function일 경우 undefined
로 처리한다 것이다.
var obj1 = {
a: 10,
b: 'abc',
};
var obj2 = JSON.parse(JSON.stringify(obj1));
obj2.b = 3;
console.log(obj1); // {a: 10, b: 'abc'}
console.log(obj2); // {a: 10, b: 3}
var deepCopy = function (obj) {
var result = {};
if (typeof obj === 'object' && obj !== null) {
for (var prop in obj) {
result[obj] = deepCopy(obj[prop]);
}
} else {
result = obj;
}
return result;
};