얕은 복사 vs 깊은 복사

얕은 복사(Shallow Copy)와 깊은 복사(Deep Copy)는 과연 무엇일까? 다음과 같이 객체 안에 중첩된 객체(Nested object)가 있다고 해 보자.

const obj1 = { obj2 : { a: 1 }};

이 때 얕은 복사한 단계까지만 복사하는 것을 말하고, 깊은 복사객체에 중첩된 객체까지 모두 복사하는 것을 말한다. 다음 코드를 보자.

// 얕은 복사
const c1 = Object.assign({}, obj1);
console.log(c1 === obj1); // false
console.log(c1.obj2 === obj1.obj2) // true

// 깊은 복사
const c2 = JSON.parse(JSON.stringify(obj1));
console.log(c2 === obj1); // false
console.log(c2.obj2 === obj1.obj2) // false

얕은 복사와 깊은 복사로 만든 객체는 원본과는 별개의 객체다. 즉, 원본과의 참조 값이 다른 새로운 객체다. 그러나 얕은 복사는 중첩된 객체의 경우 참조 값을 복사한다. 그러므로 c1.obj2는 원본인 obj1.obj2와 동일한 객체를 가리킨다.

반면에, 깊은 복사는 중첩된 객체까지 모두 복사해서 완전한 별개의 복사본을 만든다. c2.obj2와 원본인 obj1.obj2는 다른 객체이다.

깊은 복사 방법

얕은 복사는 Object.assign()이나 Spread Syntax가 대표적이다. 그렇다면 깊은 복사를 구현하는 방법에는 무엇이 있을까?

  1. JSON 메서드 가장 간단한 방법이다. JSON.stringify와 JSON.parse를 조합하여 구현할 수 있다.
    • JSON.stringify: 값이나 객체를 JSON 문자열로 변환 (직렬화)
    • JSON.parse: JSON 문자열을 값이나 객체로 변환 (역직렬화)
const obj1 = {
  obj2: {
    obj3: {
      a : 1
    }
  }
};

const deep = JSON.parse(JSON.stringify(obj1));
console.log(deep === obj1); // false
console.log(deep.obj2 === obj1.obj2); // false
console.log(deep.obj2.obj3 === obj1.obj2.obj3); // false

객체를 JSON 문자열로 변환하였다가, 다시 객체로 변환하므로 이전 객체에 대한 참조가 사라지는 원리이다. 다만, 이 방식은 속도가 느리다는 단점이 있으며, 이 방식을 사용하면 함수, Date 객체, 정규표현식 등의 데이터는 복사되지 않고 유실된다. 그러니 가급적 사용하지 않는 편이 좋다.

  1. 재귀 함수 직접 함수를 작성하여 사용할 수 있다. 복사를 실행하다가 value가 객체일 경우, 함수를 재귀적으로 호출하는 방식이다.
function copyDeep(obj) {
  const clone = {};
  for (const key in obj) {
    if (typeof obj[key] === 'object' && obj[key] !== null) {
      clone[key] = copyDeep(obj[key]);
    } else {
      clone[key] = obj[key];
    }
  }
  return clone;
}

obj를 순환하면서 원시값인 value는 빈 객체에 그대로 복사한다. value가 객체이면 재귀적으로 함수를 호출하여 새로운 객체를 만들어 복사한다.

const obj1 = {
  obj2: {
    obj3: {
      a : 1
    }
  }
};

const copy = copyDeep(obj1);
console.log(copy === obj1); // false
console.log(copy.obj2 === obj1.obj2); // false
console.log(copy.obj2.obj3 === obj1.obj2.obj3); // false

깊은 복사가 잘 수행되었음을 확인할 수 있다.

  1. cloneDeep() lodash 라이브러리의 cloneDeep()을 사용하는 방법이다. 외부 라이브러리를 사용하므로 순수하지 못하다(?)는 단점은 있지만, 사실 가장 보편적이고 간명한 방식이라고 할 수 있다.
const obj1 = { obj2: { a : 1 } };
const newObj = _.cloneDeep(obj1);

console.log(newObj === obj1); // false
console.log(newObj.obj2 === obj1.obj2); // false

cloneDeep() 역시 재귀적으로 value를 복제하는 방식이라고 한다. 오픈소스로 개발되고 검증되었으므로, 충분히 신뢰할 만한 방식이라고 할 수 있다.

참고자료

“모던 자바스크립트 Deep Dive”, 이웅모, 위키북스, 2020.09.25, pp. https://leonkong.cc/posts/js-deep-copy.html

(글에서 틀린 것이 있다면 꼭 지적해 주세요. 감사히 배우겠습니다)