JS 얕은 복사와 깊은 복사, 데이터 저장 방식
얕은 복사는 메모리 주소값을 복사
하는 개념이고, 깊은 복사는 새로운 메모리 공간을 확보해 완전히 복사
하는 개념이다. 얕은 복사와 깊은 복사는 데이터 저장 방식과 깊은 관련이 있다.
데이터 저장 방식
let A = 컵;
let C = 우산;
let D = 우산;
let B = 컵 + 우산;
변수 선언이 일어나면 컴퓨터의 메모리에서 비어있는 공간 하나를 확보한다. 그 공간에 이름을 지정하고, 데이터를 또 다른 공간에 저장한 후, 데이터가 담긴 공간 정보를 저장한다. 다소 복잡해보이는데 왜 이렇게 할까?
- 데이터 변환을 자유롭게
- 메모리 관리를 더욱 효율적으로 하기 위해서
다음과 같이 사물함에 물건을 저장하는 그림에 비유해보았다. (실제 변수저장 메커니즘은 변수영역과 데이터영역, 메모리주소, 식별자와 값 등의 개념이 있다.)
- 데이터 변환을 자유롭게
만약 사물함에 주소 정보를 저장하는 것이 아닌, 물건을 직접 저장한다고 한다면 A에처음에 컵을 담았다가 우산으로 바꾸려고 할 때
, 크기가 작아서 안들어갈 것이다. A사물함 크기를 직접 늘리는 작업이 필요하고 만약 인접 사물함에 물건이 담겨있다면 밀리거나 변형되는 등의 영향을 끼칠 것이다. 아예 새로 다른 칸에 담는 방식도 생각할 수 있는데 이는 이어질 2번의 메모리 관리의 측면에서 비효율적이다. 실제 방식은 다른 칸 O에 우산을 담고'I로 가세요'를 'O로 가세요'로 주소 정보를 수정
하여 A는 컵에서 우산으로 변경된다. - 메모리 관리를 더욱 효율적으로 하기 위해서
데이터를 직접 저장하는 방식은 C, D, E, F, ... 등무수한 500개의 변수에 '우산'이라는 동일한 값을 넣어야 하는 경우
에 500개의 큰 사물함에 우산을 각각 다 담아야 할 것이다. 공간이 낭비된다. 실제 방식은 O사물함에 우산을 담고 500개의 변수들이 O사물함을가리킨다
. 우산처럼 큰 사물함이 아닌 주소 정보만 담을 수 있는 작은 사물함들을 사용해도 같은 결과를 가질 수 있다.
또는, B에컵 + 우산을 저장하려고 할 때
이미 컵과 우산이 들어있는 칸이 있는데 별도로 큰 사물함에 컵과 우산을 함께 담아서 생성해줘야 할 것이다. 실제 방식은 별도 생성할 필요없이 컵과 우산이 들어있는 칸 모두의주소
를 저장한다.
원시자료형과 참조자료형의 저장
let a = 'hi';
a = 'hello';
위의 그림은 해당 코드의 과정을 그린 것이다. 원시자료형의 값 @5002
또는 @5003
을 보니 값이 담겨있다.
const obj = {
a: 'hi',
b: 10
};
obj.a = 'hello';
참조자료형은 값 @5004
를 보니 또 다른 곳 @7102
와 @7103
을 참조하고 있다.
얕은 복사
얕은 복사는 한 단계 아래까지만 복사하는 것이다. 위 원시자료형과 참조자료형의 저장에서 a의 값(@5002
@5003
) 또는 obj의 값(@5004
)을 복사한다.
// 원시자료형의 얕은 복사
let a = 'hi';
let b = a;
a = 'hello';
console.log(a, b); // 'hello' 'hi'
원시자료형의 경우,
- b가 a의 값
@5002
를 복사하여 가진다. - a는 값을
@5003
으로 교체한다.
a와 b가 복사 한 후에 독립적이다. ctrl+C
ctrl+V
한 것 같은 결과이다.
// 참조자료형의 얕은 복사
const obj1 = {
a: 'hi',
b: 10
}
const obj2 = obj1;
obj2.a = 'hello';
console.log(obj1.a, obj2.a); // 'hello' 'hello'
참조자료형의 경우,
- obj2는 obj1의 값
@5004
를 복사하여 가진다. - obj2.a의 값
@5002
를@5003
으로 교체한다. - 여전히 obj1과 obj2의 값은
@5004
이고 데이터@7102~?
를 가지고(참조하고) 있다.
복사본의 값을 바꾸면 원본의 값도 같이 바뀐다. obj2가 복사한 것은 값이라기보다는 @7102~@7103을 참조하고있음
을 복사했다. 같은 것을 참조하게 되었기 때문에 복사보다는 공유라고 볼 수 있다. obj2.a를 바꾸었는데 이들은 공유되고 있기 때문에 obj1.a도 같이 바뀐다.
= (할당, assign)
let a = 100;
let b = a; // 얕은 복사
a = 50;
console.log(a===b); // false
const obj1 = {
c: 'c',
d: 'd'
}
const obj2 = obj1; // 얕은 복사
obj2.c = 'zzz';
console.log(obj1===obj2); // true
참조자료형의 경우 원본이 같이 변경된다.
Array.slice()
const arr1 = [ 1, 2, 3, [4, 5] ];
const arr2 = arr1.slice();
arr2[0] = 0;
arr2[3][0] = 0;
console.log(arr1); // [ 1, 2, 3, [0, 5] ]
console.log(arr2); // [ 0, 2, 3, [0, 5] ]
1단계 레벨까지는 잘 복사가 되지만 중첩된 구조에서는 원본이 영향을 받는다.
Object.assign()
const obj1 = {
a: 0,
b: 0,
c: {
d: 0
}
};
const obj2 = Object.assign({}, obj1);
obj2.a = 1;
console.log(obj1); // { a: 0, b: 0, c: { d: 0 } }
console.log(obj2); // { a: 1, b: 0, c: { d: 0 } }
obj2.c.d = 1;
console.log(obj1); // { a: 0, b: 0, c: { d: 1 } }
console.log(obj2); // { a: 1, b: 0, c: { d: 1 } }
복사한 객체 자체는 깊은 복사가 되지만, 내부의 객체에 대해서는 얕은 복사가 된다. 다음의 ...(전개구문)과 동일하게 1레벨 깊이에서만 효과적인 복사 방법이다.
... (전개구문, spread operator)
const arr1 = [ 1, 2, [ 0 ] ];
const arr2 = [ ...arr1 ];
arr2[0] = 0;
console.log(arr1); // [ 1, 2, [ 0 ] ]
consolt.log(arr2); // [ 0, 2, [ 0 ] ]
arr2[2].push(1);
console.log(arr1); // [ 1, 2, [ 0, 1 ] ]
consolt.log(arr2); // [ 0, 2, [ 0, 1 ] ]
Array.slice()와 다른 결과가 나왔다. Object.assign()과 동일하게 1레벨에서만 효과적으로 복사할 수 있다.
깊은 복사
복사를 하는 것은 기존의 값과 독립적으로 영향이 없도록 하기 위함일 것이다. 객체가 얕은 복사로도 복사의 기능을 할 수 있다면 문제가 없을 것이다. 그러나 JavaScript가 이런 원리로 작동하므로 이곳저곳에서 원본데이터를 동시에 변형하여 프로그램이 의도한대로 동작하지 않을 수 있다. 깊은 복사를 해주어야 한다.
재귀함수 구현 (recursive function)
function isCopyObj(origin) {
let result = {};
for (let key in origin) {
if (typeof origin[key] === 'object') {
result[key] = isCopyObj(obj[key]);
} else {
result[key] = origin[key];
}
}
return result;
}
const obj1 = {
a: 0,
b: 0,
c: {
d: 0
}
};
const obj2 = isCopyObj(obj1);
위 함수 isCopyObj()로 깊은 복사를 할 수 있다. 서로 참조관계가 없는 다른 객체이다.
JSON객체 이용
const obj1 = {
a: 0,
b: 0,
c: {
d: 0
}
};
const obj2 = JSON.parse(JSON.stringify(obj));
JSON객체의 copyTarget = JSON.parse(JSON.stringify(target))
를 이용한다. obj1과 obj2는 서로 참조관계가 없는 다른 객체이다.
이전 블로그(velog.io/@hahagarden)에서 이전해온 글입니다.