본문 바로가기

Javascript

[JavaScript] 객체 복사하기 <옅은 복사 vs 깊은 복사>

반응형

JavaScript, 자바스크립트에서 객체 복사하는 여러 방법들이 헷갈려서 직접 공부겸 글로 남기기 

 

긴 글 짧게 요약하면

clone() : 복사를 하고 참조도 한다. (옅은 복사)

deepClone() : 복사를 하고 참조는 없다.  (깊은 복사)

 

참조란 ? 

= > 같은 객체를 바라보고 있는 것 

const example1 = { a:1, b:2};

const copied = example1;

console.log(example1);  // { a:1, b:2}
console.log(copied);    // { a:1, b:2}

// example1의 'a'의 값을 100으로 변경 
example1.a = 100;

console.log(example1);  // { a:100, b:2}
console.log(copied);  // { a:100, b:2}

처음 선언한 example1을 copied라는 변수에 넣고 example1의 내용만 바꾸어도 example1, copied의 내용까지 모두 바뀌었다. 변수 copied와 example1 모두 같은 객체를 바라보고 있다고 할 수 있다. 

 

 

 

1. 얕은 복사, Shallow clone 

객체를 복사한다. 하지만 복사하는 객체 내부의 객체까지 완전히 복사되지 않는다. 

잘 이해가 안가서 아래 예문으로 이해해보았다. 

 

1) Object.assign() 

먼저 일반적인 Object.assign 예시

const example2 = { a:1, b:2 };
const extra = { c: 3};

const together = Object.assign(example2, extra);
console.log(together); // { a:1, b:2, c:3 }

const together2 = Object.assign(extra, example2);
console.log(together2);  // { c:3, a:1, b:2 }

Object.assign을 쓰면 두개의 객체를 인자값 담은 순서대로 복사해 합쳐준다.

 

 

그런데 합치는 객체 안에 또 객체가 있다면 ? => 객체 안의 객체는 완전 복사가 되지 않고 참조한다. 

const example3 = {
  a: 1,
  b: {
    x: 'XXX',
    y: 'YYY',
  },
}

const copy = Object.assign({}, example3);
console.log(copy); //{a:1, b: {x:'XXX', y:'YYY'}}

example3.a = 1000;
example3.b.x = 'changed';

console.log(copy); //{a:1, b: {x:'changed', y:'YYY'}}
console.log(example3); //{a:1000, b: {x:'changed', y:'YYY'}}

copy.a = 1000000;
copy.b.x = 'again changed';

console.log(copy); //{a:1000000, b: {x:'again changed', y:'YYY'}}
console.log(example3); //{a:1000, b: {x:'again changed', y:'YYY'}}

요 위의 내용을 잘 살펴보면,

객체를 품고있는 객체인 example3을 Object.assign()으로 copy라는 변수에 담았다. 

Object.assign()은 얕은 복사이므로 { } 큰 객체 안의 값은 복사해서 다른 메모리에 담지만 

{  b: {  } } 큰 객체 안의 객체는 참조한다. 

 

example3.a 와 copy.a는 서로 다른 메모리에 담아져있어서 참조되지 않는다. ( 아예 다른 값들!) 

example3.b 와 copy.b는 같은 객체를 참조해서 어느 쪽을 수정하더라도 b의 값은 달라지고 모두 같은 결과가 나온다. 

 

즉, Object.assign()으로 복사하는 객체는 복사가 되긴 하지만 얕은 복사로 {} 객체 안의 {} 객체까지 완전히 복사하지 않는다. 

 

 

2) Spread Operator, 전개 구문

ES6 환경에서 위의 Object.assign()을 편리하게 동일한 기능으로 쓸 수 있는 Spread Operator, 전개구문을 사용할 수 있다. 

const example4 = {
  a: 1,
  b: 2,
  c: 3,
  d: {
    x: 100,
    y: 200,
  },
};

const copy2 = { ...example4 };

console.log(example4); // {a:1, b:2, c:3, d: { x:100, y:200 }}
console.log(copy2); // {a:1, b:2, c:3, d: { x:100, y:200 }}

위의 Object.assign()과 동일하다. 현재 copy2 값은 

{ a:1, b:2, c:3, d: { x: 100, y: 200 }} 이며 여기에서 example4값과 따로 "완전히" 복사된 값은 a:1, b:2, c:3 이다. 

example4나 copy2가 a,b,c값을 어떻게 변경시켜도 서로에게 영향끼치지 않는다. 

example4.a = 100000;
example4.b = 200000;
copy2.c = 888;

console.log(example4); // {a:100000, b:200000, c:3, d: { x:100, y:200 }}
console.log(copy2); // {a:1, b:2, c:888, d: { x:100, y:200 }}

=> 각자의 것만 변경됨 

 

 

하지만 d: {x:100, y:200} 는 객체 안 객체이므로 이 값은 example4의 d와 같은 곳을 바라본다.

( 둘 중 하나라도 변경되면 example4, copy2 의 d 값 모두 변경된다. ) => 옅은 복사 특징 

example4.d.x = 0;
copy2.d.y = 200000;

console.log(example4); // {a:100000, b:200000, c:3, d: { x:0, y:200000 }}
console.log(copy2); // {a:1, b:2, c:888, d: { x:0, y:200000 }}

 

 

3) Lodash 라이브러리의 clone() 함수 

⇩⇩ 아래에 내용이 있습니다 

 

 

 

 

 

2. 깊은 복사, Deep clone 

깊은 복사는 복사하는 객체, 또는 그 객체의 객체까지 모두 "완전히" 복사해버립니다. 참조도 물론 없어서 복사한뒤 각각의 값을 변형시켜도 서로에게 영향을 미치지 않습니다. 

 

1) JSON.parse(JSON.stringify())

JSON.stringify()  = 자바스크립트 객체를 JSON 문자열로 변환

JSON.parse() = JSON 문자열을 자바스크립트 객체로 변환

JSON.parse(JSON.stringify()) => JSON 문자열로 변환한 것을 다시 객체로 변환 -> 참조가 끊어지고 새로운 객체가 됨

 

const example5 = {
  a:1,
  b:2,
  c: {
    x: 10,
    y: 20,
  },
};

const copyByJSON = JSON.parse(JSON.stringify(example5));
console.log(copyByJSON); // {a:1, b:2, c: { x:10, y:20 }}

example5.a = 100;
example5.c.x = 1000;

console.log(example5); // {a:100, b:2, c: { x:1000, y:20 }}
console.log(copyByJSON); // {a:1, b:2, c: { x:10, y:20 }}

copyByJSON.b = 200;
copyByJSON.c.y = 2000;

console.log(example5); // {a:100, b:2, c: { x:1000, y:20 }}
console.log(copyByJSON); // {a:1, b:200, c: { x:10, y:2000 }}

위의 예시처럼 example5와 copyByJSON은 서로 아예 다른 변수가 된다. 

 

JOSN객체 메서드들로 깊은 복사를 할 경우의 두 가지 단점은 

- 비교적 느림

- JSON.stringify() 는 function 함수 인자값으로 받는 경우 undefined로 처리 ( 어떻게 보면 함수를 문자열로 만드는 것이니 당연한 것 같기도 하다.) 

 

* JSON.stringify() 에서 함수를 인자값으로 넘기는 경우 예시 

const example6 = {
  a:1,
  b:2,
  c: {
    x: 10,
    y: 20,
  },
  d: () => {
    console.log('난 함수야');
    return 1;
  },
};

const copiedOne = JSON.parse(JSON.stringify(example6));
console.log(copiedOne); // {a:1, b:2, c: { x:10, y:20 }} <- 여기에서부터 d가 없음 

console.log(copiedOne.d); // undefined

함수값을 가진 프로퍼티 d는 아예 카피가 되지도 않는다. 

 

 

2) Lodash 라이브러리의 deepclone 함수 

lodash는 실무에서 자주 사용하는 라이브러리이다. 나도 입사 후에 처음 써보게 되었는데 매우 편하고 익숙해져서 가끔 바닐라 자바스크립트를 까먹으면 어떡하지 걱정이 들 때도 있다. 

lodash 를 설치하고 import 해오고 아래처럼 사용하면 깊은 복사가 간편하게 된다. 

import _ from 'lodash';

const example7 = {
  a:1,
  b:2,
  c: {
    x: 10,
    y: 20,
  },
};

const copiedByLodash = _.cloneDeep(example7);
console.log(copiedByLodash); // {a:1, b:2, c: { x:10, y:20 }}

example7.a = 1000000;
example7.c.x = 1000000000;

console.log(copiedByLodash); // {a:1, b:2, c: { x:10, y:20 }}

 

lodash 라이브러리에 clone() 함수도 있다. 

clone() -> 옅은 복사 

cloneDeep() -> 깊은 복사 

 

위의 코드에서 cloneDeep() -> clone으로만 바꿔주면 아래처럼 얕은 복사가 된다. 

import _ from 'lodash';

const example7 = {
  a:1,
  b:2,
  c: {
    x: 10,
    y: 20,
  },
};

const copiedByLodash = _.clone(example7);
console.log(copiedByLodash); // {a:1, b:2, c: { x:10, y:20 }}

example7.a = 1000000;
example7.c.x = 1000000000;

console.log(example7); // {a:1000000, b:2, c: { x:1000000000, y:20 }}
console.log(copiedByLodash); // {a:1, b:2, c: { x:1000000000, y:20 }}

 

 

 

 

정리

 

얕은 복사는 -> 말그대로 얕게 복사해서 "완전한" 복사본이 안되고 일부는 원본 객체와 같은 곳을 참조한다. 

ex) 원본의 객체 안의 객체를 수정하면 or 복사본 객체 안의 객체를 수정하면 -> 복사본의 객체 안의 객체도 수정된다. (동일한 곳이기 때문)

 

깊은 복사 -> 깊게 "완전히" 복사해서 원본과 아예 분리되는 독립적인 객체가 된다. 원본, 사본 서로 참조하거나 영향을 끼치는 곳이 없다. 

ex) 원본 or 복사본을 수정해도 서로에게 영향을 미치지 않는다. 

 

 

 

 

 

 

 

 

 

 

References: 

https://junwoo45.github.io/2019-09-23-deep_clone/

반응형