Logo

자바스크립트 객체 복제 방법

자바스크립트에서 의외로 객체의 복제가 쉽지 않을 수 있습니다. 예를 들어 다음과 같은 객체가 있다고 해보겠습니다.

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

자바스크립트에서 객체(object)란 속성(property)의 집합으로 생각할 수 있습니다. 속성은 키(key)와 값(value)를 가집니다. 자바스크립트에서 키는 항상 문자열인데 반해, 값은 위와 같이 모든 데이터형이 될 수 있다는 특징을 가지고 있습니다.

참조 할당

객체를 복제할 때 초보자들이 가장 많이 하는 실수는 다음과 같이 = 연산자를 통해 새로운 변수에 복사할 객체를 할당하는 것입니다.

const clone1 = original;

original.num = 2000;
console.log(clone1.num); // 2000

clone1.bool = false;
console.log(original.bool); // false

console.log(original === clone1); // true

위 코드는 동일한 객체를 가리키는 변수를 하나 더 만드는 것 뿐 입니다. 즉, original이 가리키던 객체를 clone1도 가리키게 된 것 이죠. 다시 말해, 이 하나의 객체는 2개의 변수에 의해서 공유되고 있습니다. 더 쉽게 설명하면, 해당 객체에 접근하는 통로가 2개가 된 것입니다. 따라서 어느 변수를 통해 값을 바꾸던 나머지 변수에 영향을 주게 됩니다.

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

얕은 복제 (Shallow Clone)

Object.assign(obj)

객체를 복제할 때 다음으로 많이 하는 방법은 객체의 얕은 복사입니다.

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

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

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

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

Object.assign(target, ...sources) 메서드를 사용하면 첫번쩨 인자로 두번째 인자의 속성들을 복사할 수 있습니다. 따라서 위와 같이 언뜻 보면 원본과 복사본이 서로 영향을 주지 않고 변경이 가능한 것 처럼 보입니다.

하지만 단순 속성이 아닌 객체나 배열 속성을 변경해보면 아래와 같이 여전히 서로 영향을 준다는 것을 알 수 있습니다.

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

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

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

이런 현상이 발생하는 이유는 객체를 하나의 트리 구조로 봤을 때 최상위 레벨의 속성만 복사를 하는 Object.assign(target, ...sources) 메서드의 동작 방식에 있습니다. 객체 트리의 최말단 노드까지 복사되지 않기 때문에 이러한 복제 방식을 얕은 복제(Shallow Clone)라고 일컽습니다.

…(Spread Operator)

물론 의도적으로 깊은 복제 대신에 얕은 복제를 하는 경우도 있을 수 있습니다. 얕은 복제는 아래와 같이 ...(Spread Operator)를 활용해서 더 간결하게 처리할 수 있습니다.

const clone3 = { ...original };

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

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

console.log(clone3.arr === original.arr); // true

깊은 복제 (Deep Clone)

JSON.parse(JSON.stringify(obj))

자, 그럼 객체 트리의 최말단 노드까지 복제하고 싶을 때는 어떻게 해야할까요? 널리 사용되는 방법은 JSON 내장 객체를 사용하는 것 입니다. 아래와 같이 JSONparse() 메서드와 stringify() 메서드를 연달아 호출하면 동일한 객체 트리를 가지는 새로운 객체가 복제됩니다.

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

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

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

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

엄밀히 말하면 이 방법에도 약간의 주의해야될 부분이 있는데요. 첫 번째는 json에는 함수 데이터 타입이 없기 때문에 함수 속성들은 누락된다는 점입니다.

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

이 밖에도 객체 트리 내에 순환 참조가 있는 경우, stringify() 메서드에서 TypeError: Converting circular structure to JSON이라는 오류가 발생한다는 문제도 있습니다.

직접 구현

이렇게 생각보다 자바스크립트 객체를 완벽하게 복제하는게 쉽지 않다는 것을 알게 되었습니다. 결국은 깊은 복제를 하려면 재귀적으로 객체 트리를 따라서 말단 노드까지 모두 복사를 해주는 함수가 필요합니다. 직접 코드를 짜보면 대략 다음과 비슷하게 나올 것 입니다.

function clone(source) {
  var target = {};
  for (let i in source) {
    if (source[i] != null && typeof source[i] === "object") {
      target[i] = clone(source[i]); // resursion
    } else {
      target[i] = source[i];
    }
  }
  return target;
}

const clone5 = clone(original);

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

_.cloneDeep(obj) 활용

사실 위에 작성한 코드가 수많은 경우의 수의 입력에 대해서 100% 작동할지 자신이 없습니다. 그래서 저는 보통 속편하게 lodash라는 외부 라이브러리의 cloneDeep(obj)이라는 메서드를 사용합니다. 아무래도 오픈 소스 커뮤니티의 고수들이 오랜 시간에 거쳐 다듬어온 코드이기 때문에 충분히 검증이 되었으라라 봅니다.

const _ = require("lodash");

const clone6 = _.cloneDeep(original);

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

이 밖에도 검색해보시면 객체 복제를 해주는 다양한 자바스크립트 라이브러리들이 있다는 것을 확인하실 수 있으실 겁니다.

이상으로 의외로 까다로운 자바스크립트 객체 복제 방법에 대해서 알아보았습니다. 예제 코드는 아래에 올려두었니 필요하시다면 직접 실행해보시길 바랍니다.