본문 바로가기
Javascript/Javascript

원시값과 참조값, 얕은복사와 깊은복사, Object 깊은복사 방법

by img 2021. 11. 20.

얕은복사와 깊은복사의 차이점을 이해하려면 우선 원시값과 참조값의 차이에 대한 이해가 필요하다.

원시값

원시값이란 모든 연산이 "실제값 자체"를 갖고 이루어진다. 자바스크립트의 원시값으로는 String, Number, Boolean, Null, Undefined가 있다.

let num1 = 1,
const num2 = num1;
console.log(num2); // 1
num1 = 20;
console.log(num2); // 1

다음과 같이 num1의 값만을 선언해주고, num2가 num1이 갖고 있는 "값 자체인" 1을 할당받았으므로 num2에 할당한 뒤 num1의 값을 바꿔주더라도 num2에서는 아무런 영향을 받지 않는다.

참조값

참조값이란 대표적으로 객체, 배열, 함수가 있으며, 모든 연산이 해당 실제값이 아닌 값을 갖고 있는 "주소"를 "참조"하게 된다. 대표적인 참조타입으로는 객체, 배열, 함수가 있다. 

const obj1 = {
    num: 1
}
const obj2 = obj1;
console.log(obj2.num); // 1
obj1.num = 2
console.log(obj2.num); // 2

다음과 같이 obj1이 참조하고 있는 {객체}를 obj2에게 할당해준다면, obj2는 객체의 실제값을 할당받은 것이 아니라 객체를 가르키고 있는 "주소"값만을 할당받은 것이다. 때문에 obj1가 가르키고 있는 객체의 값이 바뀐다면, obj2도 obj1과 같은 객체의 주소를 가르키고 있기 때문에 obj2의 값도 같이 바뀌게 된다. 결국 데이터를 공유하고 있게 되는 것이다.


그럼 이제 참조값을 복사하게 될때, 참조값의 주소만을 복사해서 같은 데이터를 공유할 것인가, 해당 주소가 가르키고 있는 데이터 자체를 복사해와서 개별적으로 사용할것인가에 대한 의문에 발생한다. 

얕은복사 ( Shallow Copy )

기본적으로  위 코드와 같이 참조값을 그대로 할당해버리는 경우 객체의 주소만을 복사해와서 사용하기 때문에, 원래의 데이터가 의도치 않게 같이 변해버릴 수 있기 때문에 해당 주소의 데이터 자체를 가져와서 사용하는 깊은복사의 사용이 중요하다. 

깊은 복사 ( Deep Copy )

1. Object.assign()

Object의 assing 메소드는 Object.assign( 새로 만들 object, 복사해올 object) 와 같이 사용되는데, 깊은 복사를 하고 싶다면 첫번째 인수에는 빈 객체{} 를 넣고 두번째 인수에는 복사해올 객체를 넣으면 된다.

const myObj = { num : 1}
const yourObj = Object.assign({}, myObj);

yourObj.num = 2

console.log(myObj); // 1
console.log(yourObj); // 2
console.log(myObj === yourObj) // false

myObj를 복사해와서 yourObj에 새롭게 값을 할당해줬지만 myObj가 참조하고 있는 값인 { num : 1 } 은 그대로 유지가 된다.

* 다만 Object.assign()을 사용할 경우 깊이(depth)가 2 이상인 객체일 경우, {객체 안에 {객체} } 가 있는 형태이기 때문에 깊이가 2 이상인 값은 얕은 복사가되는 문제가 있다. 

 

2. 전개연산자  ( Spread Operator)

const myObj = {
  a: 1
};

const yourObj = { ...myObj };

yourObj.a = 2;

console.log(myObj.a); // 1
console.log(yourObj.a); // 2
console.log(myObj.a === yourObj.a); // false

이처럼 전개연산자 (...) 를 사용해서 깊은 복사를 구현할 수도 있지만,

const myObj = {
  a: 1,
  b: {
    c: 2,
  },
};

const yourObj = { ...myObj };

yourObj.b.c = 3;

console.log(myObj.b.c); // 3
console.log(yourObj.b.c); // 3
console.log(myObj.b.c === yourObj.b.c); // true

 

* Object.assign()과 마찬가지로 깊이가 2 이상일 경우 얕은복사가 발생한다.

 

3. JSON.stringify(), JSON.parse()

만약 복사하고자 하는 객체가 JSON타입의 객체라면, JSON.stringify()와 JSON.parse()를 사용해서도 깊은 복사를 구현할 수 있다. JSON.stringify()는 JSON형태의 객체를 string화 시키는 것이며, JSON.parse()는 string화 되어있는 JSON객체를 JSON형태로 만들어주는 것이다. string타입은 참조타입이 아닌 원시타입이기때문에 주소를 복사하는 것이 아닌 값 자체를 복사하는 특성을 이용한 것이다.

const myObj = {
  a: 1,
  b: {
    c: 2,
  },
};

const newObj = JSON.parse(JSON.stringify(myObj));

newObj.b.c = 3;

console.log(myObj.b.c); // 2
console.log(newObj.b.c);  // 3 
console.log(myObj=== newObj); // false

* 다만 해당 방법은 JSON객체만 가능하며, 함수는 undefined로 반환되어 함수는 복사가 안되고,  다른방법에 비해 속도가 떨어질 수 있다. 

4. lodash 모듈 사용

사용하기 전 npm install lodash 명령어를 이용해 모듈을 설치하고,  require("lodash") 해서 가져온 뒤 lodash의 .cloneDeep()함수를 사용하는 방법도 있다. 

const lodash = require("lodash");

const myObj = {
  a: 1,
  b: {
    c: 2,
  },
  myFunction: function () {
    return this.a;
  },
};

const newObj = lodash.cloneDeep(myObj);

newObj.b.c = 3;


console.log(myObj); // { a: 1, b: { c: 2 }, myFunc: [Function: func] }
console.log(myObj.b.c === newObj.b.c); // false

해당 방법은 앞서 설명한 3가지 단점을 모두 보완할 수 있다.  깊이가 2 이상인 객체도 복사할 수 있고, 함수또한 함수 형태 그대로 복사해올 수 있다. 

* 다만 lodash 모듈을 설치해야만 사용할 수 있다. 

댓글