얕은 복사 vs 깊은 복사

수툴리 양·2021년 6월 1일
0
post-thumbnail

data type에 대해.. 포스트
📝 참조형 자료

Shallow Copy vs Deep Copy

우선 우리는 number, string, undefined, null 등의 원시형 자료(primitive type data)를 복사할 수 있다고 알고 있다.
원시형 데이터는 그 값 자체를 복사해온다. '값'이라는 표현과 '복사'라는 표현이, 정확히 들릴지 모르겠다.
영어로 value 와 copy, 이렇다하더라도 잘 모르겠다.
(우리는 이 글 동안 함깨 고민을 해볼 것이지만, 원시형 자료처럼 데이터 실체, 값 자체가 복사되는 것을 deep copy로 생각해두면 헷갈리지 않을 것이다.)

그렇다면 비교해보며 가늠해보는 것도 좋을 것이다.

배열을 포함한 객체 Object 는 참조형 자료이다. Reference type data. 데이터 저장소에 값 자체가 아닌 메모리 주소가 담긴다.

let arr1 = [1, 2, 3];
let arr2 = arr1; 

arr1.pop();

console.log(arr1); // [1, 2]
console.log(arr2); // [1, 2]

console.log(Boolean(arr1 === arr2)); // true

* ___.pop()은 배열의 마지막 요소를 뱉어내는(배열 관점에서는 삭제) 메소드

위 코드에서 arr2arr1복사 했다. 그리고 배열arr1의 요소에 손을 좀 댔다.
마지막 줄에서 arr1 과 arr2 를 엄격비교 하였다. 출력값은 true이다.
우리는 arr1만 건드렸는데 arr2도 같이 손봐진(?) 것이다.
그럼 배열 arr1arr2같은 것 일까? 동일한 객체여서, 분신이어서 하나만 변경해도 둘 다 변경이 되는 것일까? 하나를 혼쭐내줬더니 둘이 무릎 꿇는 것일까?
우리가 "같다"고 하는 것을 객체의 복사에서는 좀 더 상세히 살펴보아야 한다.
(길을 잃기 전 가는 길에 힌트를 두자면 .. 위 코드가 shallow copy를 설명하는 셈)

let arr3 = [1, 2, 3]; // arr1 ---> 메모리주소 5000
let arr4 = [1, 2, 3]; // ---> 메모리주소 5801

arr3.pop();

console.log(arr3); // [1, 2]
console.log(arr4); // [1, 2, 3]

console.log(Boolean(arr1 === arr2)); // false

위 코드의 배열 arr3arr4 는 그 안의 요소가 쏙 빼닮았어도, 서로 다른 메모리 주소를 참조하고 있다. 둘은 엄연히 다른 놈이다. *빈 배열이어도 마찬가지이다.(당연)

.push()메소드로 arr3을 건드렸고, arr4는 알 바 없으니 아무 변화가 없다.

과거의 컴퓨터가 변수 하나에 데이터 값 한가지만 넣을 수 있던 것에서 공학자들은 그 데이터 보관함을 부수지 않고 더 유동적인(?) 방법을 생각해냈다.

이처럼 다량의 데이터를 한 번에, 한 곳에 저장하기 위해 그 실체를 저장하는 것이 아닌, 주소를 참조하는 것이다.
나는 이것을 네 락커에 집 주소를 적은 쪽지를 넣어뒀다, 라고 상상했다. 또는 물건을 찾으려면 xx창고로 와, 같은.

배열도 객체이고, 객체 Object 는 참조형 데이터 타입 reference data type 이다. 다시 말하면 참조할 메모리 주소를 저장한다.

그렇다면, 객체의 참조값/메모리 주소가 적힌 쪽지가 아닌 데이터 값을 가져와 저장해두려면 어떻게 "복사"해야 할까?

물건의 실체를 두 눈으로 확인하지 않아도 된다면 우리는 얕은 복사 shallow copy로 만족할 수 있다.
하지만 물건을 확실히 확인하려면?

⁍ Object.assign();

우리는 Object.assign({}, obj); 로 물건을 deep copy할 수 있다.

let targetObj = {name: 'jay', age: 22, city: 'Seoul'};
let sourceObj = {name: 'steve', city: 'Seattle', favorite: 'surfing'};

let new01Obj = Object.assign(targetObj, sourceObj);

console.log(targetObj); // {name: "steve", age: 22, city: "Seattle", favorite: "surfing"}
console.log(sourceObj); // {name: "steve", city: "Seattle", favorite: "surfing"}
console.log(new01Obj); // {name: "steve", age: 22, city: "Seattle", favorite: "surfing"}

위 코드를 크롬 개발자 도구 콘솔에서 찍어보면,
sourceObj의 name key value와 city key 밸류가 targetObj에 덮어씌워졌다. 그리고 targetObj에 없던 favorite key와 값도 복사되었다.
Object.assign(target{}, source{})은 target에 source를 복사deep copy하는 하나의 방법이다.

let new02Obj = Object.assign({}, sourceObj);

console.log(new02Obj); // {name: "steve", city: "Seattle", favorite: "surfing"}

new02Obj.name = 'Karl';
new02Obj.phone = 'iPhone';

console.log(sourceObj); // {name: "steve", city: "Seattle", favorite: "surfing"}
console.log(new02Obj); // {name: "Karl", city: "Seattle", favorite: "surfing", phone: "iPhone"}

그럼 deep copy되었는지 확인삼아 객체의 key: value를 변경해본다.
이번엔 빈 객체에 sourceObj를 복사하여 새로운 변수로 선언 및 할당을 한다.
복사된 new02Obj의 name key 밸류와 phone 밸류를 변경 및 추가하였다.
콘솔로그로 찍힌 출력을 보니 복사된 new02Obj만 변경되었고, sourceObj는 아무 변화가 없다.

와! 객체의 deep copy를 해냈다!


❖ Object.assign()과 함께라면 어떤 객체든 deep copy를 할 수 있는 것일까?

컴퓨터 언어도, 자바스크립트도 완벽하지 않다.
인간인 개발자들이 만들고, 자기들이 만든 게 어려워서 다시 해독하고, 문법을 망가뜨렸다가 다시 규칙을 세우고.. 인간은 편리하기 위해 요령도 부려보고, 그 와중에 컴퓨터의 언어 속에는 숨겨진 본능을 찾고..

결론부터 말하면 객체의 depth 가 2이상일 때, 다른 말로 2차원 이상일 때에는 Object.assign()도 deep copy를 실패한다.
실패라는 표현이 맞을까? 우리의 입장에서라고 생각한다.

let newSourceObj = { 
  name: 'Theo', 
  age: { international: 39 }, 
  city:'Moscow'
}

let brandnewObj = Object.assign({}, newSourceObj);

delete brandnewObj.age.international;

console.log(newSourceObj); // {name: "Theo", age: {}, city: "Moscow"}
console.log(brandnewObj); // {name: "Theo", age: {}, city: "Moscow"}

위 코드는 객체 안에 객체를 품고 있는 depth 2이상인 객체의 복사 를 보여주는 예제이다.
앞의 예제 코드와 같이 Object.assign()를 사용하여 newSourceobj를 brandnewObj로 복사 하였다.
age key의 밸류는 {international: 39}이다. 이 밸류의 international key 값을 삭제해본다.
newSourceObj의 age. international key 값도 삭제된 것을 확인할 수 있다.
아아 shallow copy가 다시 온 것이다.

1차 결론

Object.assign()으로 객체의 deep copy가 가능하나,(즉 참조값 주소가 아닌 데이터 값 자체를 clone)
depth 2이상인(2차원 이상, 객체 안에 객체가 있는) 경우엔 불가능하다.

let newSourceObj = { 
  name: 'Theo', 
  age: { international: { inUSA : 39} }, 
  city:'Moscow'
}

let brandnewObj = Object.assign({}, newSourceObj);

delete brandnewObj.age.international.inUSA;

console.log(newSourceObj); // {name: "Theo", age: {international: {} }, city: "Moscow"}
console.log(brandnewObj); // {name: "Theo", age: {international: {} }, city: "Moscow"}

위의 예제는 슥 보고 넘어가면 된다, 객체 안의 객체 안의 객체, 이런 걸 쓰나싶긴 하지만 콘솔에 찍어보려 써봤다.
똑같이 shallow copy되어, 참조값 주소가 복사된다. 원본이 수정된다.

⁍ Spread {...obj}

그럼 객체의 deep copy는 정말 보장할 수 없는 것일까?
guarantee it
또 다른 방법 하나가 있다. spread문법을 통해 객체를 copy할 수 있다.

let obj1 = { keyA: 1, keyB: 30, keyC: 'honey'};
let obj2 = {...obj1};

console.log(obj1); // { keyA: 1, keyB: 30, keyC: 'honey'}

오, 여기까진 shallow copy인지 deep copy인지 알 수 없다.
조금 더 살펴본다.

let arr1 = [0, 1, 1, 2, 3, 5, 8, 13]
let arr2 = [...arr1];

console.log(arr2); // [0, 1, 1, 2, 3, 5, 8, 13]

배열에도 가능하다. '[ ]'대괄호를 사용한다.


❖ 그렇다면 Spread{...obj} 는 어때 완전 deep copy?

let obj1 = { keyA: 1, keyB: 30, keyC: 'honey'};
let obj2 = {...obj1};

console.log(obj2);

delete obj2.keyA;

console.log(obj2); // {keyB: 30, keyC: "honey"}
console.log(obj1); // {keyA: 1, keyB: 30, keyC: "honey"}

위 코드에서 obj1을 복사해 obj2로 선언 및 할당하였다.
*비교를 위해 obj2를 먼저 console에 출력하였다.
우리는 obj2의 키 keyA와 그 값을 삭제하였고,
원본인 obj1에는 아무런 변화가 없다. 고요하다.
deep copy 인정. 완전 가능!


함부로 단정지으면 안된다. 이번에도 depth 2 이상인 객체의 deep copy를 확인해본다.

const originObj = { keyA: {a:1, b:200}, keyB: 3};
const copiedObj = {...originObj};

copiedObj.keyA.a = 100;

console.log(originObj === copiedObj); // false, 즉 메모리 주소가 다르다.
console.log(originObj.keyA.a); // 100

copiedObj의 키 keyA 값에 담긴 객체{a:1, b:200}의 키 a의 값을 100으로 변경하고 있다.
originalObj의 keyA.a값도 100으로 바뀌었다.
아아 참조값 주소를 복사하는 shallow copy인 것이다!

const originArr = [1, 2, [300, 400] ];
const copiedArr = [...originArr];

copiedArr[2][0] = 3 ;

console.log(originArr === copiedArr); // false, 즉 메모리 주소가 다르다.
console.log(copiedArr); // [1, 2, [3, 400]]
console.log(originArr[2][0]); // 3
console.log(originArr); // [1, 2, [3, 400]]

The shallow copy came to town. 이 몸 등장.

2차 결론이자 최종

spread 구문{...obj}으로도 객체의 deep copy가 가능한 것을 확인하였다. 하지만 역시 spread로도 depth 2이상인(2차원 이상, 객체 안에 객체가 있는) 경우엔 deep copy가 불가능하다.
객체의 deep copy는 Object.assign()과 Spread{...}로 가능하다.
하지만 완벽하지는 않다!



눈치채었을 수도 있지만 위 글에서 나는 shallow copy를 "복사" (옅은 회색, 이탤릭체)로, deep copy를 "복사" (진한 검정, 볼드체)로 표시하였다.

🖊
deep copy가 가능하다는 것만 알고 지나칠 뻔 하였는데, 완전한 방법은 없음을 알게되어 MDN문서부터 여러 글들을 찾아보고 직접 생각해보았다. 콘솔에 찍어보며 납득할 수 있었다. (누군가의 말로 전해들은 거라 오기가 생겼다. 물건을 직접 내 두눈으로 확인하리라..)
우스울 수 있는 비유를 하며 글이 길어졌지만, 자신 나름대로 정리할 수 있어 좋았다. 또 헷갈릴 것 같기도 하고.
이 때의 나와 같이 고민하는 사람들이 내 글을 보고 함께 생각해볼 수 있다면 좋을 것이다.
보다 이론적인 원리로 설명할 수 있으면 좋겠지만, 차차 더 공부해서 나의 스코프를 넓혀나가야 하는 일이다.

profile
developer; not kim but Young

0개의 댓글