본문 바로가기

FrontEnd/JavaScript

[JS] Shallow Copy Deep Copy

얕은 복사(Shallow Copy)와 깊은 복사(Deep Copy)

 

오늘은 자바스크립트 깊은 복사와 얕은 복사에 대해서 알아보고자 합니다.

 

복사면 복사지 어째서 얕은 복사 , 깊은 복사지?

이제 이 이유에 대해서 알려면 자바스크립트 객체가 어떻게 저장되는지 따져봐야 하는데 , 

 

자바스크립트 객체는 힙 메모리라는 공간에 저장되게 됩니다.

 

변수는 컬스택 메모리라는 공간에 저장되는데 , 변수에 접근하면 변수에 값이 있는 게 아니라 힙 메모리 주소를 참조하게 됩니다.

 

말로는 이해가 잘 안 될 수도 있으니 코드로 보겠습니다.

 

얕은 복사

const Hwanobj = {
  name: 'HwanMin',
  age: 19,
};

console.log(Hwanobj);
// { name: 'HwanMin', age: 19 }

이렇게 찍어보면 잘 객체가 찍힙니다.

 

근데 어떤 나쁜 녀석이 객체를 만들기 귀찮다면서 제 객체를 가져갔다고 가정해 봅시다. 

(참조에 의한 전달)

// 히히 이  객체는 이제 제겁니다.
const otherobj = Hwanobj;

otherobj.name = 'otherName';
otherobj.age = 31;

console.log(otherobj)
console.log(Hwanobj);
//{ name: 'otherName', age: 31 }
//{ name: 'otherName', age: 31 }

이런 원래 잘 나오던 저의 객체도 다른 나쁜 녀석에 의해서 변질당하고 말았습니다..! 

 

사실 이 나쁜 녀석도 만들기 귀찮아서 제 객체를 가져간 것이지 , 제 객체까지 변질 생각은 없었을 겁니다. 그렇지?....

 

이 이유가 지금은 Hwanobj , otherobj 둘 다 힙 메로리에 한 주소를 가리키고 있기 때문에 이런 일이 벌어진 겁니다.

즉 다른 복사한 녀석에서 원본 배열에 값에 영향을 주면 이건 깊은 복사가 아니라 얕은 복사라고 부릅니다.

 

객체를 복사하는 방법은 그냥 할당하는 게 아니라 , 자바스크립트에서 만들어 놓은 메서드가 있습니다. 

Object.assign()

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/assign

 

사용법은 mdn 사이트 참고하였습니다.

const assignObj = Object.assign({}, Hwanobj);

assignObj.name = 'assign!!';
assignObj.age = 1000;
console.log(Hwanobj);
console.log(assignObj);
// { name: 'HwanMin', age: 19 }
// { name: 'assign!!', age: 1000 }

오... 위와 같이 코드를 작성하니 원본 객체를 손상시키지 않았습니다. 

완벽한 것 같군요! 

 

이와 비슷한 두 번째 방법을 사용해 보겠습니다.

 

... Spread 연산자.

const spredObj = { ...Hwanobj };

spredObj.name = 'spread!!';
spredObj.age = 500;

console.log(spredObj);
console.log(Hwanobj);
// { name: 'spread!!', age: 500 }
// { name: 'HwanMin', age: 19 }

오 Object.assign()과 마찬가지로 더 이상 원본 객체인 Hwanobj를 건들지 않습니다..! 

 

이 방법들을 권유해주면 더이상 나쁜 녀석이 제 원본 객체를 건들지 않겠군요! 

 

🥺 assign , spread 연산자 한계

근데 만약 객체 주인이 프로퍼티를 조금 더 추가했습니다.

const Hwanobj = {
  name: 'HwanMin',
  age: 19,
  favorGame: ['StarCraft', 'LoL', 'BG', 'OverWatch'],
};

console.log(Hwanobj);

아하! favorGame을 추가했군요..! 

스타크래프트가 있다니 나이가 조금 있을 수 있을 수도...? 

 

assignObj 가 제 객체를 복사해서 좋아하는 게임을 자기한테 맞게 수정할 생각인가 봅니다.

후훗..! 맘껏 바꿔가라고 ~ 

const assignObj = Object.assign({}, Hwanobj);

assignObj.name = 'assign!!';
assignObj.age = 1000;
assignObj.favorGame[0] = '지뢰찿기!!';

console.log(Hwanobj);
console.log(assignObj);
/* 
{
  name: 'HwanMin',
  age: 19,
  favorGame: [ '지뢰찿기!!', 'LoL', 'BG', 'OverWatch' ]
}
{
  name: 'assign!!',
  age: 1000,
  favorGame: [ '지뢰찿기!!', 'LoL', 'BG', 'OverWatch' ]
}
*/

?????????????

 

아니 이게 무슨 일이자 분명 위에서는 완벽했던 복사인데 제 원본 객체에 첫 번째 게임인 스타크래프트를 지뢰 찾기 같은 게임으로 바꿔버렸습니다. 

 

이번에는 spread 연산자가 제 객체를 가져가서 자기한테 맞게 수정을 해봅니다.

const spredObj = { ...Hwanobj };

spredObj.name = 'spread!!';
spredObj.age = 500;
spredObj.favorGame[1] = '포트리스!!';

console.log(spredObj);
console.log(Hwanobj);

/* 
{
  name: 'spread!!',
  age: 500,
  favorGame: [ '지뢰찿기!!', '포트리스!!', 'BG', 'OverWatch' ]
}
{
  name: 'HwanMin',
  age: 19,
  favorGame: [ '지뢰찿기!!', '포트리스!!', 'BG', 'OverWatch' ]
}
*/

아니 이게 무슨 마른하늘에 날벼락인지... 완벽한 줄 알았던 두 녀석이 저를 배신했습니다. 

 

제가 좋아하는 게임 목록이 완전히 바뀌어 버렸어요.

 

📌 이유 

이렇게 원본 객체가 변질돼버린 이유는 assign() , spread 연산자 둘 다 deapth 가 1일 때까지만 적용이 됩니다.

 

즉 객체에 깊이가 2가 되는 순간 그것까지는 완벽하게 커버 치지 못하는 것입니다.

 

그래서 name , age는 잘 적용이 돼서 원본 객체가  보존되었지만 , favorGame은 막을 수 없던 것입니다.

 

완벽한 복사 해보기 

사실 애니메이션을 보면 한 명을 해치우면 그다음 보스가 나오지 않습니까? 

assign() , spread 녀석의 두목님을 데려왔습니다.

const Hwanobj = {
  name: 'HwanMin',
  age: 19,
  favorGame: ['StarCraft', 'LoL', 'BG', 'OverWatch'],
};

const jsonObj = JSON.parse(JSON.stringify(Hwanobj));

jsonObj.name = 'jsonObj';
jsonObj.age = 2000;
jsonObj.favorGame[0] = '내가 게임 그 자체다..';

console.log(jsonObj);
console.log(Hwanobj);

/*
{
  name: 'jsonObj',
  age: 2000,
  favorGame: [ '내가 게임 그 자체다..', 'LoL', 'BG', 'OverWatch' ]
}
{
  name: 'HwanMin',
  age: 19,
  favorGame: [ 'StarCraft', 'LoL', 'BG', 'OverWatch' ]
}
*/

결과는 놀랍습니다.!! 

 

더 이상 뎁스가 2인 객체도 건들지 않습니다. 

 

그러면 3은 안전할까요? 확인해 보겠습니다. 

 

const Hwanobj = {
  name: 'HwanMin',
  age: 19,
  favorGame: ['StarCraft', 'LoL', 'BG', 'OverWatch'],
  three: {
    arr: ['one!! 빠빠빠'],
  },
};

const jsonObj = JSON.parse(JSON.stringify(Hwanobj));

jsonObj.name = 'jsonObj';
jsonObj.age = 2000;
jsonObj.favorGame[0] = '내가 게임 그 자체다..';
jsonObj.three.arr[0] = '크레용팝 !!';

console.log(jsonObj);
console.log(Hwanobj);

/*
{
  name: 'jsonObj',
  age: 2000,
  favorGame: [ '내가 게임 그 자체다..', 'LoL', 'BG', 'OverWatch' ],
  three: { arr: [ '크레용팝 !!' ] }
}
{
  name: 'HwanMin',
  age: 19,
  favorGame: [ 'StarCraft', 'LoL', 'BG', 'OverWatch' ],
  three: { arr: [ 'one!! 빠빠빠' ] }
}
*/

 

더 이상 객체의 뎁스가 깊어져도 원본 배열과는 상관이 없어졌습니다. 

 

그러면 나쁜 녀석한테는 앞으로 완전한 카피를 위해서 JSON을 활용한 카피를 추천해 줘야겠군요! 

 

그러면 더 이상 제 객체에 상태를 건드리는 일 없이 서로 안전하게 객체를 생성할 수 있을 거 같습니다! 

 

이상 얕은 복사와 깊은 복사에 대해서 알아보았습니다.

 

 

 

궁금점 

깊은 복사 간의 차이점은 무엇인가 ? 

 

일단 깊은 복사를 하는 방법으로는 3가지 정도로 분류된다.

 

1. 자체 내장 JSON 사용

2. 재귀함수로 Copy

3. 라이브러리 사용

 

근데 여기서 차이점은 일단 라이브러리를 쓰냐 기본내장 JSON을 쓰냐 차이인데 

만약 속도가 중요하지 않다면 , 기본 JSON 모듈로 충분할 것이다.

 

하지만 속도를 중요시 한다면 라이브러리를 쓰는 편이 유리하다 왜 그러냐면 아래 표를 보면 확인할 수 있다.

 

 

속도 측면에서는 JSON 모듈로 깊은 복사 하는 것이 불리한 것을 결과로 볼 수 있다.

 

각자 용도에 맞게 사용하면 될 것같다 !!