Javascript/Typescript

얕은 복사, 깊은 복사

범데이 2022. 4. 10. 16:32
728x90

1. 개요

 

자바스크립트에서 값은 원시 값과, 참조 값으로 나뉜다.

원시 값

  • Number
  • String
  • Boolean
  • Null
  • Undefined

 

참조 값

  • Object
  • Symbol

 

원시 값은 값을 복사할 때 복사된 값을 다른 메모리에 할당하기 때문에

아래와 같이 원래의 값과 복사된 값이 서로에게 영향을 미치지 않는다.

const a = 1;
let b = a;

b = 2

console.log(a); //1
console.log(b); //2

 

반면에, 참조 값은 변수가 객체의 주소를 가리키는 값이기 때문에 복사된 값(주소)이 같은 값을 가리킨다.

const a = {number: 1};
let b = a;

b.number = 2

console.log(a); // {number: 2}
console.log(b); // {number: 2}

 

이러한 객체의 특징 때문에 객체를 복사하는데에 있어서 유의해야 한다.

 


2. 객체의 복사 종류

2.1 얕은 복사(Shallow Copy)

얕은 복사는 참조(주소)값의 복사를 나타낸다.

onst obj = { vaule: 1 }
const newObj = obj;

newObj.vaule = 2;

console.log(obj.vaule); // 2
console.log(obj === newObj); // true

obj 객체를 새로운 newObj 객체에 할당하였으며 이를 참조 할당이라 부른다. 복사 후 newObj객체의 value값을 변경하였더니 기존의 obj.value 값도 같이 변경된 것을 알 수 있다. 두 객체를 비교해도 true로 나온다. 이렇게 자바스크립트의 참조 타입은 얕은 복사가 된다고 볼 수 있으며, 이는 데이터가 그대로 생성되는 것이 아닌 해당 데이터의 참조 값(메모리주소)를 전달하여 결국 한 데이터를 공유하는 것이다. 

 

 

 

2.2 깊은 복사(Deep Copy)

위와 같이 객체를 얕은 복사하여 사용할 경우 기존 객체의 원본 데이터가 변경되어 더럽혀 질 수 있다. 

깊은 복사는 값 자체의 복사를 나타낸다. 따라서 객체를 복사하는데에 있어서 깊은 복사는 매우 중요하다. 객체를 깊이 복사하는 방법에 대해 몇가지 알아보자.

 

 

3. 객체를 깊이 복사하는 방법들

3.1 커스텀 재귀 함수

이 문제를 원칙적으로 해결하려면 직접 깊은 복사를 구현하는 커스텀 재귀 함수를 사용하는 것이다.

function deepCopy(obj) {
  if (obj === null || typeof obj !== "object") {
    return obj;
  }

  let copy = {};
  for (let key in obj) {
    copy[key] = deepCopy(obj[key]);
  }
  return copy;
}

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

const newObj = deepCopy(obj);

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

deepCopy 함수의 인수로 obj객체를 넣었다. 인수값이 객체가 아닌 경우는 그냥 반환하며, 객체인 경우 객체의 값 만큼루프를 돌며 재귀를 호출하여 복사된 값을 반환한다. 복사된 newObj객체를 보면 2차원 객체의 값도 깊은 복사가 이루어 졌으며, 객체의 함수도 제대로 표현되는 것을 확인할 수 있다.

 

하지만 이미 객체의 깊은 복사를 위한 오픈 소스가 존재하며 lodash 모듈의 cloneDeep() 을 이용하면 된다.

 

3.2 lodash 모듈의 cloneDeep()

lodash 모듈의 cloneDeep() 메서드를 이용하여 객체의 깊은 복사가 가능하다. 해당 모듈을 설치해 준 뒤 아래 코드를 실행시켜 보자.

$npm i lodash
const lodash = require("lodash");

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

const newObj = lodash.cloneDeep(obj);

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

간단히 객체의 깊은 복사를 구현할 수 있다. 실제로 웹 개발을 하다보면 lodash 모듈은 흔히 사용되며, 가장 손쉽게 객체의 깊은 복사를 해결하는 방법이라 할 수 있다.

 

 

 

4. 그외의 객체 깊은 복사 방법들과 그 문제점..

4.1 Object.assign()

Object.assign() 메서드를 활용하는 방법이다.

문법
Object.assign(생성할 객체, 복사할 객체) 메서드의 첫번째 인수로 빈 객체를 넣어주며, 두번째 인수로 할당할 객체를 넣어주면 된다.

 

const obj = { a: 1 };
const newObj = Object.assign({}, obj);

newObj.a = 2;

console.log(obj); // { a: 1 }
console.log(obj === newObj); // false

새로운 newObj 객체를 Object.assign() 메서드를 사용하여 생성하였으며, newObj.a 값을 변경하여도 기존의 obj는 변하지 않았다. 서로의 객체를 비교해도 false로 뜨며, 서로 참조값이 다르다는 것을 알 수 있다.

 

하지만 Object.assign()는 2차원 객체의 깊은 복사가 이루어지지 않는다.

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

const newObj = Object.assign({}, obj);

newObj.b.c = 3;

console.log(obj); // { a: 1, b: { c: 3 } }
console.log(obj.b.c === newObj.b.c); // true

2차원 객체를 newObj에 복사하고, newObj.b.c의 값을 변경하였다. 기본 obj 객체를 출력해보면 newObj.b.c의 값도 3으로 변경되었다. 복사된 하위 객체 { c: 2 } 도 결국 객체이기 때문에 얕은 복사가 이루어진 것이다. 이는 Object.assign() 메서드의 한계이다.

 

 

4.2 전개 연산자(Spread Operator)

전개 연산자를 활용해도 객체의 깊은 복사가 가능하나, Object.assign()과 같이 2차원 객체의 깊은복사가 이루어지지 않는다.

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

const newObj = { ...obj };

newObj.b.c = 3;

console.log(obj); // { a: 1, b: { c: 3 } }
console.log(obj.b.c === newObj.b.c); // true

 

4.3 JSON 객체 메서드 이용

객체의 깊은 복사를 위해 JSON 객체의 stringify(), parse() 메서드를 이용할 수 있다.

 

문법
JSON.stringify() 메서드는 인수를 객체로 받으며 받은 객체는 문자열로 치환되며,
JSON.parse() 메서드는 문자열을 인수로 받으며, 받은 문자열을 객체로 치환한다.
const obj = {
  a: 1,
  b: {
    c: 2,
  },
};

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

newObj.b.c = 3;

console.log(obj); // { a: 1, b: { c: 2 } }
console.log(obj.b.c === newObj.b.c); // false

obj 객체를 JSON.stringify() 메서드를 이용하여 문자열로 변환한 뒤 다시 JSON.parse() 메서드로 객체로 변환하였다. 문자열로 변환한 뒤 다시 객체로 변환하였기에 2차원 객체에 대한 참조가 사라졌다.

이 방법은 2가지 문제가 있는데 다른 방법에 비해 성능이 좋지 못한 점과, JSON.stringify() 메서드는 함수를 만났을 때 undefined로 처리한다는 점이다.

 

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

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

console.log(newObj.func); // undefined

복사된 newObj func가 없고 undefined 로 출력되고 있다.

 

 

 

 


#Refences

https://velog.io/@th0566/Javascript-%EC%96%95%EC%9D%80-%EB%B3%B5%EC%82%AC-%EA%B9%8A%EC%9D%80-%EB%B3%B5%EC%82%AC

https://velog.io/@recordboy/JavaScript-%EC%96%95%EC%9D%80-%EB%B3%B5%EC%82%ACShallow-Copy%EC%99%80-%EA%B9%8A%EC%9D%80-%EB%B3%B5%EC%82%ACDeep-Copy

 

반응형