얕은복사? 깊은복사?
이것 때문에 당황한 경험이 있다.
로직을 잘 짰고, 재귀를 통해 내가 원하는 값이 담긴 배열을 열심히 정답 배열에 넣었는데,
정답 배열을 살펴보니 죄다 똑같은 낱개 배열들이 들어 있던 것...
아무튼, 얕은 복사의 쓴맛을 먼저 경험해보고 작성하는 글이다.
* 주소 참조는 알고 있었으나, 각각 push 명령을 해주기 때문에 문제가 될 것이라고 생각을 못했다.
* 그래서, 어떤게 문제인지 조차 모르겠어서 배열 관련 연관검색어를 뒤져봤었다,,
자료형
자바스크립트는 얕은 복사(= 참조 복사)와 깊은 복사(=값 복사)가 가능하다.
자바스크립트의 자료형을 먼저 소개하면,
원시형 | 참조형 |
---|---|
Number | Object |
String | |
Boolean | |
Null | |
Undefined | |
Symbol |
으로 나뉜다.
원시형
원시형은 복사할 때 값을 완전히 복사한다.
let A = 1;
let B = A;
A = 100;
A의 값을 복사해서 새로운 변수 B에 할당하였다. A,B 변수는 독립적이다.
그러므로 A 변수의 값을 바꿔도, 이미 생성된 B 변수와는 관련이 없다.
이렇게 두 변수 A, B가 완전히 독립성을 가지면 깊은 복사(값 복사) 라고 한다.
참조형
참조형은 선언한 변수가 값 자체가 아닌 주소값을 가지고 있기 때문에, 똑같이 복사되지만 주소값을 완전히 복사한다.
let A = [1,2,3,4,5];
let B = A;
A[0] = 100;
B[4] = 300;
A라는 객체(배열)을 선언하고, 값을 할당한다.
여기서 우리는 A 그 자체가 [1,2,3,4,5] 를 의미할 것이라고 착각하기 쉽지만,
사실 메모리 어딘가에 [1,2,3,4,5]를 담은 배열이 생성되고, 우리는 그 주소 값을 A 변수에 가지고 있을 뿐이다.
따라서 사실은 A는 주소값 0x010을 의미한다.
그러므로, B를 A와 같다고 선언하면 새로운 배열이 메모리에 생성되어 그 배열을 참조하려는 것이 아닌,
A의 주소값을 복사하므로 결과적으로 A와 B는 같은 주소값을 가지고 같은 배열을 참조한다.
그러므로 A에서 참조한 배열의 변화는 B에 영향을 미친다.
이와 같이 A,B는 독립적이지 않고 이런 유형을 얕은 복사(참조복사) 라고 한다.
이는 종종 프로그래밍을 할 때 당황하게 만든다.
서두에 언급 했듯이, 종종 코드를 열심히 작성하고 콘솔을 보면 끔찍할 정도로 똑같은 답들이 나와 있을 때가 있거나,
뜬금없이 Cannot read property '0' of undefined ! 와 같은 오류를 내뿜기도 한다.
보통 이럴 경우 3번 중 2번은 얕은 복사가 문제였던 것 같다.
아무튼, 어떻게 깊은 복사로 독립성을 가질 수 있을까?
1. 가장 쉬운 반복문
배열을 새로 선언해서 복사하려는 배열의 원소를 하나씩 옮기면 된다.
let A = [1,2,3,4,5];
let B = [];
for(let x of A){
B,push(x);
}
이렇게 하면 B 배열의 독립성이 보장되기 때문에, A 배열의 원소 값을 바꾼다고 해서 B 배열에 영향을 줄 수 없다.
2. JSON 객체 메서드를 이용하기
let A = [1,2,3,4,5];
let B = JSON.parse(JSON.stringify(A));
JSON.stringify 메서드로 객체를 JSON 문자열로 변환 시키고,
JSON.parse로 JSON 문자열을 자바스크립트 객체로 변환 시킨다.
위와 같은 방법으로 독립성을 보장할 수 있으나, 시간이 오래걸린다.
※ 이건 TMI지만, 이것도 완벽한 독립성을 보장하진 않는다.
단적인 예로,
다음과 같이 배열 내에 함수를 선언했다면, 변환 과정에서 함수가 사라진다.
이유는 JSON.stringify를 참고해보면 좋을 것 같다.
정확성을 위해 꼭 써야 한다면, Lodash 라이브러리의 deepclone 메소드를 사용할 수도 있다.
주의할 메서드
1. Array.slice
보통의 상황에서 배열을 깊은 복사를 하고자 할 때 간단하게 쓰는 배열 메서드이다.
일반적인 상황에서는 문제가 발생하지 않지만, 복사하고자 하는 배열 내에 또 다른 객체가 있을 때 문제가 발생한다.
let A = [1,2,3];
let B = A.slice();
// OK
let A = [1,2,3,[4,5]];
let B = A.slice();
// NO
slice 메서드는 start, end를 인자로 받아서 배열의 start부터 end-1 까지의 원소를 가지고 새로운 배열을 만든다.
start, end 값을 설정하지 않으면 전체 배열을 복사한다.
하지만 배열 내에 또 다른 참조형이 존재한다면, slice로 복사할 때 얕은 복사와 같이 주소값을 복사하므로
A와 B 내의 배열은 독립성을 가지지 못하기 때문에 이 경우에는 얕은 복사에 해당하게 된다.
즉, 간단히 말해서 B[3] = [4,5]에 해당하는 배열에 대해 원소값 4에 해당하는 B[3][0] = "k" 로 바꾸면,
A[3] 값 또한 같은 주소를 참조하므로 ["k",5]를 가리킨다는 것이다.
그러므로 복사하고자 하는 배열 내에 또 다른 객체가 있다면 모든 값을 독립적으로 복사할 수 없다.
2. Spread Operator
iterable이 가능한 것들을 가지고 모두 펼쳐주는 이 연산자 또한 자주 이용하는 요소 중 하나인데, 이 또한 마찬가지이다.
let A = {
name : "Tom",
age : 24,
fav : ["soccer", "programming"]
};
let B = {...A};
B.name = "Jane";
B.fav[0] = "banana";
console.log(A.fav[0]) => ?
너무 배열만을 가지고 하는 것 같아서, 객체를 사용해 보았다.
A변수는 Tom이라는 사람의 정보가 담고 있다.
spread operator(...)를 이용하였는데, 이는 변수 A를 unpack 한다고 생각하면 된다.
A의 {}를 택배 박스라고 생각하고, key:value 쌍들을 물건이라 생각하면
택배 박스 안에 있는 물건들을 죽 늘여놓은 모습이다.
그것을 B 변수를 선언하면서 {} 택배 박스 안에 선언 함으로서, A박스의 물건들을 그대로 B 택배 박스 안에 넣은 것이 된다.
아무튼 그래서 다음과 같이 복사되는데, 위의 코드 그림에서도 알 수 있고 slice 에서 소개한 것과 같이
이 또한 객체 내의 객체는 주소값을 복사하므로 유의해야 한다.
결론
사실 생각해보면 아직 많은 프로젝트를 경험해보진 못해서 그렇겠지만,
철저한 깊은 복사만을 요구하는 경우가 별로 없던 듯 하다.
대부분 얕은 복사 문제는 slice를 사용하는 정도의 선에서 해결이 되었었다.
이 문제는, 구현하고자 하는 아키텍처를 생각하고 그에 맞게 잘 선택하면 될 듯 하다.
항상 느끼는 점이지만,
프로그래밍을 할 때는 문제가 발생해도 뭐가 문제인지를 몰라서 답답한 경우가 많다.
그래서 문제가 발생하게 될 때 마다 하나씩 차근차근 정리하고 알아갈 생각이다.
출처
을 참고 하였습니다.
'JavaScript > theory' 카테고리의 다른 글
[자바스크립트] ?. 연산자, 옵셔널 체이닝 (0) | 2021.07.08 |
---|---|
[자바스크립트] let, const 키워드를 사용하자. (0) | 2021.05.21 |
[자바스크립트] var 키워드를 사용하지 말아야 하는 이유 (0) | 2021.05.21 |
[자바스크립트] 렉시컬 스코프 (0) | 2021.05.19 |
[자바스크립트] 동등 비교 연산자 "==" 와 일치 비교 연산자 "===" (0) | 2021.05.03 |