본문 바로가기

Javascript/Javascript

[Javascript] 객체 복사 방법, 얕은 복사와 깊은 복사

728x90
반응형

 

 

 

예를 들어 아래와 같은 객체가 있다고 가정해 보자.

const original = {
  num: 1000,
  bool: true,
  str: "test",
  func: function () {
    console.log("func");
  },
  obj: {
    x: 1,
    y: 2,
  },
  arr: ["A", "B", "C"],
};

 

위 객체를 복제하여 새로운 변수에 복제본을 할당해야 한다면 어떻게 하겠는가? 원본에 영향이 없도록 복제해야 한다면 어떻게 하겠는가?

 

이번 글에서는 자바스크립트에서 객체를 복제하는 다양한 방법에 대해서 정리해 보려 한다.

 

 


 

 

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

 

원시값

  • Number
  • String
  • Boolean
  • Null
  • Undefined

 

참조값

  • Object
  • Symbol

 

원시값은 값을 복사할 때 복사된 값을 다른 메모리에 할당하기 때문에 원래의 값과 복사된 값이 서로 영향을 미치지 않는다.

const original = 1;
let clone = original;

clone = 2;

console.log(original);  // 1
console.log(clone);  // 2

 

 

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

const original = {number: 1};
let clone = original;

clone.number = 2;

console.log(original)  // {number: 2}
console.log(clone)  // {number: 2}
console.log(original === clone)  // true

 

객체를 복제할 때 초보자들이 가장 많이 하는 실수는 위와 같이  연산자를 통해 새로운 변수에 복제할 객체를 할당하는 것이다. 그러나 위 코드는 동일한 객체를 가리키는 변수를 하나 더 만드는 것뿐이다. 그러니까 변수  original 가 가리키던 객체를  clone 도 가리키게 되는 것이다.

 

이렇게 하나의 객체를 가리키는 변수가 2개가 생기면 어디서 어떻게 해당 객체의 속성이 변경될지 예측하기 어려워지고 자연스럽게 버그가 생기기 쉬워진다. 또한 Immutable, 불변하는 코드를 선호하는 최근 경향과도 거리가 멀어지게 된다.

 

 


 

 

복제의 깊이

자바스크립트로 복제를 하는 방법에 대해 본격적으로 알아보기 전에 얕은 복사(Shallow Clone)깊은 복사(Deep Clone)에 대한 이해가 필요하다. 얕은 복제로 충분한 상황에서 깊은 복제를 하게 되면 성능 문제로 이어질 수 있고 깊은 복제를 해야 하는 상황에서 얕은 복제를 하게 되면 데이터 문제를 일으킬 수 있기 때문이다.

 

객체는 하나의 트리로 이해할 수 있다. 객체는 여러 개의 속성을 가질 수 있고 각 속성이 숫자, 문자열과 같은 값일 수도 있지만 또 다른 객체일 수도 있다.

 

얕은 복제에서는 최상위 레벨의 속성만 복제되는 반면, 깊은 복제에서는 객체 트리의 최말단 노드의 속성까지 연쇄적으로 복제가 일어난다.

 

이해를 돕기 위해 처음의 객체 예시를 다시 가져와 보겠다.

const original = {
  num: 1000,
  bool: true,
  str: "test",
  func: function () {
    console.log("func");
  },
  obj: {
    x: 1,
    y: 2,
  },
  arr: ["A", "B", "C"],
};

 

 

위 객체의 obj 속성에는 x 속성과 y 속서응로 이루어진 객체가 할당되어 있다. 얕은 복사에서는 원본 객체의 속성에 객체가 할당되어 있다면 그 속성에 대한 참조를 그대로 복제본 객체의 속성이 가리키게 한다. 따라서 아래와 같이 이름이 다른 두 개의 변수가 동일한 객체를 참조하게 되는 것이다.

original.obj 👉 {x: 1, y: 2} 👈 clone.obj

 

 

반면 깊은 복사에서는 원본 객체의 속성에 객체가 할당되어 있을 때 그 속성과 동일한 구조의 객체가 생성되어 복제본 객체의 속성에 할당된다. 

original.obj 👉 {x: 1, y: 2}
clone.obj 👉 {x: 1, y: 2}

 

깊은 복사를 하게 되면 원본 객체를 변경했을 때 복제본 객체에 영향을 주지 않으며 복제본 객체를 변경했을 때에도 원본 객체가 영향을 받지 않는다. 따라서 의도치 않은 데이터 변경으로부터 보다 안전한 프로그램을 작성할 수 있다는 장점이 잇다. 그러나 그만큼 깊은 복사는 메모리를 많이 소모하게 된다.

 

그리고 이러한 깊은 복사의 단점은 곧 얕은 복사의 장점이 된다. 얕은 복사를 하게 되면 메모리를 효율적으로 쓸 수 있고 데이터를 한 곳에서 바꿔도 쉽게 여러 곳으로 전파할 수 있다는 장점이 있다. 따라서 얕은 복사를 하는 경우도 있지만 상대적으로 데이터 버그 발생의 위험으로 인해 각별한 주의가 필요하다.

 

 


 

 

얕은 복사(Shallow Clone)

얕은 복사란 객체를 복사할 때 위의 예시처럼 원본값과 복사된 값이 같은 참조를 가리키고 있는 것을 말한다. 객체 안에 객체가 있을 경우 하나라도 원본 객체를 참조하고 있다면 이를 얕은 복사라고 한다.

 

 

1. Object.assign()

 

자바스크립트에서는 얕은 복사를 위해 Object.assign() 함수를 많이 사용해 왔다. 이는 첫 번째 인자로 넘어온 객체에 두 번째 인자의 속성들을 추가하여 반환하기 때문에 첫 번째 인자로 빈 객체를 넘기고 두 번째 인자로 원본 객체를 넘기면 얕게 복사된 객체를 얻을 수 있다.

const clone = Object.assign({}, original);

 

얕게 복사된 객체이기 때문에 객체가 아닌 속성을 변경할 때에는 원본과 복사본이 서로 영향을 주지 않는다.

 

original.num = 3000;
console.log(clone.num); // 2000

clone.bool = true;
console.log(original.bool); // false

console.log(original === clone); // false

 

하지만 객체나 배열로 된 속성을 변경하면 서로 영향을 준다(자바스크립트에서는 배열도 객체임).

 

original.obj.x = 3;
console.log(clone.obj.x); // 3

clone.arr.push("D");
console.log(original.arr); // ["A", "B", "C", "D"]

console.log(original.obj === clone.obj); // true
console.log(original.arr === clone.arr); // true

 

 

2. ... (전개 연산자)

 

ES6부터는 얕은 복사를 할 때  ...  (Spread Operator, 전개 연산자)를 사용하는 개발자들이 많아지고 있다. 코드가 상대적으로 간결해지고 읽기 쉬워지기 때문이다.

const clone = { ...original };

original.num = 4000;
console.log(clone.num); // 3000

clone.arr.push("E");
console.log(original.arr); // ["A", "B", "C", "D", "E"]

console.log(original === clone); // false
console.log(clone.arr === original.arr); // true

 

 


 

 

깊은 복사(Deep Clone)

깊은 복사는 객체 안에 객체가 있을 경우에도 원본과의 참조가 완전히 끊어진 객체를 말한다.

 

 

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

 

예전부터 널리 사용되던 편법은 JSON 내장 객체를 사용하는 것이다. 아래와 같이 JSON의 parse() 함수와 stringify() 함수를 연달아 호출하면 동일한 객체 트리를 가지는 새로운 객체가 복제된다.

const clone = JSON.parse(JSON.stringify(original));

original.obj.x = 4;
console.log(clone.obj.x); // 3

clone.arr.push("F");
console.log(original.arr); // ["A", "B", "C", "D", "E"]

console.log(original.obj === clone.obj); // false
console.log(original.arr === clone.arr); // false

 

그러나 엄밀히 말하면 이 방법에도 약간의 주의가 필요하다. 첫 번째는 JSON에는 함수 데이터 타입이 없기 때문에 함수 속성들은 누락된다는 점이다.

console.log(original.func); // function func()
console.log(clone.func); // undefined

 

이 외에도 객체 트리 내에 순환 참조가 있는 경우 stringify() 함수에서 TypeError: Converting circular structure to JSON 과 같은 오류가 발생한다는 문제도 있다.

 

 

2. 재귀함수를 이용한 복사

 

자바스크립트 객체를 완벽하게 복제하는 것은 생각보다 쉽지 않다. 결국 깊은 복제를 하려면 재귀적으로 객체 트리를 따라 말단 노드까지 모조리 복제를 해주는 함수가 필요하다. 직접 코드를 작성하면 아래와 같을 것이다.

 

function copyObject(obj) {
  const result = {};
  
  for(let key in obj){
    if(obj[key] != null &&  typeof obj[key] === 'object') {
      result[key] = copyObject(obj[key]);  // recursion
    } else {
      result[key] = obj[key];
    }
  }
  return result;
}

const clone = copyObject(original);

clone.b.c = 0;

console.log(original.b.c === clone.b.c);  // false
console.log(original.func); // function func()
console.log(clone.func); // function func()

 

 

그러나 사실 위에서 작성한 코드가 수많은 경우의 수의 입력에 대해 100% 작동할지에 대해서는 자신이 없다. 따라서 많은 개발자들이 속 편하게 Lodash라는 외부 라이브러리의 cloneDeep(obj)이라는 함수를 사용하기도 한다. 아무래도 오픈 소스 커뮤니티의 고수들이 오랜 시간에 걸쳐 다듬어온 코드이기 때문에 직접 구현하는 것보다 훨씬 더 검증이 되어 있기 때문이다.

 

const _ = require("lodash");

const clone = _.cloneDeep(original);

console.log(original.func); // function func()
console.log(clone.func); // function func()

 

 

 

 

 

 

 

728x90
반응형

'Javascript > Javascript' 카테고리의 다른 글

[Javascript] JSON 다루기  (0) 2024.12.04
[Javascript] Axios  (0) 2024.11.22
[Javascript] innerHTML, innerText, textContent  (1) 2024.11.19
[Javascript] 즉시 실행 함수(IIFE)  (2) 2024.10.30
[Javascript] this  (0) 2024.10.29