Logo

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

자바스크립트로 코딩을 하시다가 객체가 의도하신 대로 복제되지 않아서 고생하신 적이 한 번 쯤은 있으실텐데요.

예를 들어, 다음과 같은 객체가 있다고 가정해보겠습니다.

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

여러분은 위 객체를 어떻게 복제하여 새로운 변수에 복제본을 할당하실 건가요? 원본에 영향이 없도록 안전하게 복제하실 자신이 있으신가요?

const clone = /* original의 복제본 */;

이번 포스팅에서는 자바스크립트에서 객체를 복제하는 다양한 방법에 대해서 실습을 통해서 한 번 정리해보도록 할께요. 우선 객체를 복제하다가 쉽게 범할 수 있는 실수에 대해서 살펴보고, 얇은 복제와 깊은 복제에 대한 개념을 잡아보겠습니다. 마지막으로 Lodash 라이브러리와 웹 표준 API인 structuredClone() 함수를 활용해서 객체 복제를 해보겠습니다.

참조 할당

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

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)와 깊은 복제(Deep Clone)라는 이해하는 것이 중요합니다. 얇은 복제로 충분한 상황에서 깊은 복제를 하게 되면 성능 문제로 이어질 수 있고, 깊은 복제를 해야 하는 상황에서 얇은 복제를 하게 되면 데이터 문제를 일으킬 수 있기 때문입니다.

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

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

말로만 설명드리는 것보다 맨 처음 사용했던 예제 객체를 통해서 설명드리는 것이 더 이해가 쉬우실 것 같은데요.

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

original 객체의 obj 속성에는 x 속성과 y 속성으로 이루어진 객체가 할당되어 있습니다.

얇은 복제에서는 원본 객체의 속성에 객체가 할당되어 있다면 그 속성에 대한 참조를 그대로 복제본 객체의 속성이 가리키게 합니다. 즉, 이름이 다른 두 개의 변수가 동일한 객체를 참조있는 형국이 되죠.

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

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

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

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

깊은 복제의 단점은 곧 얇은 복제의 장점이 되는데요. 얇은 복제를 하게 되면 메모리를 효율적으로 쓸 수 있고, 데이터를 한 곳에서 바꿔도 쉽게 여러 곳으로 전파할 수 있다는 장점이 있습니다. 그래서 의도적으로 얇은 복제를 하는 경우도 있지만, 상대적으로 데이터 버그 발생의 위험 때문에 각별한 주의가 필요합니다.

Object.assign() 함수

자바스크립트에서 얇은 복제를 위해서 예전부터 Object.assign() 함수를 많이 사용했었습니다.

Object.assign() 함수는 첫 번쩨 인자로 넘어온 객체에 두 번째 인자의 속성들을 추가하여 반환하기 때문에, 첫 번째 인자로 빈 객체를 넘기고, 두 번째 인자로 원본 객체를 넘기면 얇게 복제된 객체를 얻을 수 있습니다.

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

하지만 객체나 배열로 된 속성을 변경해보면 서로 영향을 주는 것을 볼 수 있습니다. (배열도 자바스크립트에서는 객체로 취급되죠?)

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

…(전개 연산자)

ES6 부터는 얇은 복제를 할 때 ...(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

JSON.parse(JSON.stringify())

자, 그럼 객체 트리의 최말단 노드까지 복제, 즉 깊은 복제를 해야할 때는 어떻게 해야할까요?

예전부터 널리 사용되던 편법은 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이라는 오류가 발생한다는 문제도 있습니다.

자바스크립트에서 JSON 데이터를 다룰 때 사용되는 JSON 내장 객체에 대해서는 별도 포스팅에서 자세히 다루고 있으니 참고 바랍니다.

직접 구현

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

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

const clone5 = clone(original);

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

_.cloneDeep() 활용

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

const _ = require("lodash");

const clone6 = _.cloneDeep(original);

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

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

structuredClone() 등장

여기서 반가운 소식! 🙌

바로 깊은 복제를 하기 좀 더 편하도록 웹 표준 API로 structuredClone() 함수가 비교적 최근에 추가되었다는 것입니다. structuredClone() 함수에 인자로 객체를 넘기면 깊게 복제된 새로운 객체가 반환됩니다. 정말 쉬어졌죠? 😁

한 가지 아쉬운 부분은 JSON.parse(JSON.stringify(obj))을 사용할 때와 마찬가지로 함수로 된 속성은 복제가 되지 않습니다. 만약에 원본 객체에 함수로 된 속성이 포함되어 있다면 DataCloneError 오류가 발생하므로 주의가 필요합니다.

delete original.func; // ⚠️ 함수 속성은 오류를 발생시키므로 제거
const clone7 = structuredClone(original);

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

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

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

자바스크립트의 structuredClone() 함수는 2022년 3월부터 익스플로러를 제외한 모든 브라우저에서 지원하고 있습니다. 참고로 서버 측 자바스크립트 런타임인 Node.js에서도 v17부터 지원을 시작하였습니다. 물론 Bun과 같은 차세대 런타임에서는 structuredClone() 함수가 처음부터 지원하고 있습니다.

전체 코드

본 포스팅에서 작성한 코드는 아래에서 확인하고 직접 실행해보실 수 있습니다.

마치면서

이상으로 자바스크립트에서 객체를 복제하는 여러가지 방법에 대해서 총정리를 해보았습니다. 객체 복제가 얼마나 까다로울 수 있는지 놀라지 않으셨나요?

항상 객체를 복제하실 때는 얇은 복제가 필요한지 깊은 복제가 필요한 상황인지에 대해서 생각해보세요. 그리고 그 두 가지 복제 방법의 Trade Off를 잘 고려하신다면 객체 복제 관련해서 실수나 시행착오를 줄이실 수 있으실 것입니다.