깊은복사를 위한 함수 성능측정과 특징 비교

·2023년 8월 18일
28

프론트엔드

목록 보기
3/12
post-thumbnail
  • 썸네일 출처 : 생활코딩 유튜브

javscript 에서 자주 사용되는 깊은복사 함수들의 속도를 측정해보고 각 함수가 가진 특징들을 비교해 보았습니다.

깊은복사는 무엇인가요 ?

객체의 깊은 복사는 원본 객체와 동일한 참조를 공유하지 않는 복사본입니다. 따라서 원본 또는 복사본 중 하나를 변경하더라도 다른 객체가 변경되지 않도록 보장할 수 있으며 의도치 않은 원본 또는 복사본 변경을 방지할 수 있습니다.

깊은복사 함수

🔖 자주 사용되는 깊은 복사함수들을 간단하게 소개합니다.

JSON.parse(JSON.stringify)

JSON.stringify()는 JavaScript 객체를 JSON 형식의 문자열로 변환합니다. JSON.parse()는 JSON 형식의 문자열을 다시 JavaScript 객체로 변환합니다. 객체에 대한 정보를 모두 복사하지만 함수나 심볼과 같은 값들은 JSON으로 직렬화할 수 없어 복사가 불가능합니다.

structuredClone

자바스크립트 내장 함수이기 때문에 별도의 라이브러리 설치 없이 사용이 가능합니다. Node 17 버전 이상, 크롬 98 버전 이상부터 사용이 가능합니다.
함수, 심볼 타입 외에 다양한 타입의 복사를 지원합니다.

cloneDeep

Lodash 라이브러리의 함수로 객체를 재귀적으로 깊은 복사하는 방법을 제공합니다.객체를 복사하면서 객체 내부의 모든 하위 객체까지 재귀적으로 복사합니다. 함수나 심볼과 같은 값도 복사되고 객체 복사 시 순환 참조도 처리할 수 있습니다.

속도 측정 Strat !

이제 이 함수들의 복사 속도를 측정해보겠습니다.

hook import

const _ = require("lodash");
const { performance } = require("perf_hooks");

속도 측정 커스텀 함수 구현

const measureTime = (action) => {
  const startTime = performance.now();
  action();
  const endTime = performance.now();
  return (endTime - startTime).toFixed(3);
};

measureTime 함수를 직접 제작했습니다. JavaScript 일급객체의 특성을 살려 성능을 측정하는 함수를 인자로 받고 속도를 측정합니다.

함수 실행, 결과 확인


const parseStringifyTime = measureTime(() => JSON.parse(JSON.stringify(original)));
const cloneDeepTime = measureTime(() => _.cloneDeep(original2));
const structuredCloneTime = measureTime(() => structuredClone(original3));


console.log("JSON.parse(JSON.stringify):", parseStringifyTime, "ms");
console.log("_.cloneDeep:", cloneDeepTime, "ms");
console.log("structuredClone:", structuredCloneTime, "ms");

데이터

{ profile: { name: "철수", age: 12 }, grade: "A" };

🔎 객체 1개 복사 결과

JSON.parse(JSON.stringify): 0.042 ms
_.cloneDeep: 0.419 ms
structuredClone: 0.548 ms

🏆 1위 : JSON.parse "내가 제일 빨라!"
🏆 2위 : cloneDeep "내가 2등이네." "내가 2등이네."
🏆 3위 : structuredClone "내가 제일 느리다니.."

객체 하나를 복사했을 때 JSON.parse가 가장 빠른 속도를 보였고 로다쉬의 cloneDeep 함수가 그 다음, structuredClone 함수가 가장 느린 속도를 보였습니다.

복사하는 데이터가 많아진다면 어떨까 궁금해졌습니다. 데이터를 약 100개정도 삽입하면 결과가 어떻게 달라지는지 확인해보겠습니다.

데이터 100개 삽입 코드

const largeObj = [];
const number = 100000;
for (let index = 0; index < number; index++) {
  const obj = { profile: { name: "철수", age: 12 }, grade: "A" };
  largeObj.push(obj);
}

🔎 객체 100개 복사 결과

JSON.parse(JSON.stringify): 0.157 ms
_.cloneDeep: 2.987 ms
structuredClone: 0.908 ms

🏆 1위 : JSON.parse "역시 이번에도 내가 제일 빨랐다!"
🏆 2위 : structuredClone "2위 자리 탈환!" (3위-> 2위)
🏆 3위 : cloneDeep "데이터가 많아지니까 내가 밀리는구나..!" (2위-> 3위)

100개 결과를 얻었습니다. 그럼 10000개는 어떨까? 하는 궁금증이 생기지 않나요 ?

데이터 10000개를 한 번에 내려 받아 복사할 일이 흔치 않겠지만, 데이터 10000개 삽입하고 다시 복사를 돌려봅시다.

🔎 객체 1000개 복사 결과

JSON.parse(JSON.stringify): 1.099 ms
_.cloneDeep: 8.761 ms
structuredClone: 3.051 ms

위의 결과와 비슷합니다.

그렇다면 객체로 이루어진 배열이 아닌 프로퍼티가 많은 단일 객체라면 어떨까요?

🔎 프로퍼티가 1000개인 단일 객체 복사 결과

데이터 출력 예시

{
 prop1: {},
 prop2: {},
 prop3: {}, ... //
}

JSON.parse(JSON.stringify): 0.435 ms
_.cloneDeep: 0.940 ms
structuredClone: 6.964 ms

프로퍼티가 1000개일때도 JSON이 여전히 가장 빠릅니다.
그럼 단순히 속도가 빠르기 때문에 JSON 함수를 사용해야 할까요 ? 그것은 아닙니다. JSON 에는 여러 허점이 존재합니다.

class B {
  a = 1;
  b = "qwerty";
  c = true;
  d = null;
  e = undefined;
  f = [1, 2, 3];
  g = { f1: "asdfg" };
  h = new Date();
  i = new Set([1, 2, 3]);
  j = /abc/;
}
const o1 = new B();
const o2 = JSON.parse(JSON.stringify(o1));
const o3 = _.structuredClone(o1);
const o4 = cloneDeep(o1);

💻 JSON.parse(JSON.stringify()) 깊은복사 결과값

{
  a: 1,
  b: 'qwerty',
  c: true,
  d: null,
  f: [ 1, 2, 3 ],
  g: { f1: 'asdfg' },
  h: '2023-08-17T13:48:25.637Z',
  i: {},
  j: {}
}

💻 structuredClone,cloneDeep 깊은복사 결과값

B {
  a: 1,
  b: 'qwerty',
  c: true,
  d: null,
  e: undefined,
  f: [ 1, 2, 3 ],
  g: { f1: 'asdfg' },
  h: 2023-08-17T13:48:47.465Z,
  i: Set(3) { 1, 2, 3 },
  j: /abc/
}

우리가 왜 JSON 을 사용하면 안되는 지 이유를 알 것 같지 않나요 ?
JSON 함수는 undefined 가 무시되고 날짜 객체를 문자열로 변환하여 반환합니다. new Set 과 정규식은 아예 빈 객체를 반환하는 것을 볼 수 있습니다.

structuredClone와 cloneDeep 함수는 JSON 함수가 지키지 못하는 객체들을 지켜줄 수 있습니다. 또 다른 이점은 순환 참조를 지원한다는 점입니다. JSON은 순환 참조를 나타낼 수 없으므로 이러한 객체를 받으면 예외를 던집니다.

순환참조 예시코드

const obj1 = {
  name: "Object 1",
};

const obj2 = {
  name: "Object 2",
};

obj1.circularRef = obj2;
obj2.circularRef = obj1;
// 순환 참조가 무엇인지 모르는 분들을 위해 예시 코드를 작성했습니다. 아주 쉽죠 ? 

하지만 그럼에도 불구하고 structuredClone 함수는 만능이 아닙니다. 심볼 타입을 지원하지 않고, Error와 Function 또한 복제할 수 없습니다. 시도해보면 DATA_CLONE_ERR exception 에러를 만나게 될 것입니다.

추가적으로 cloneDeep 함수는 Symbol 을 삽입해도 structuredClone 처럼 ref 에러를 뱉지 않습니다. 다만 깊은복사가 되지 않고 값이 그대로 반환됩니다.

이와 같이 각 함수마다 제공하는 기능과 특성이 다르기 때문에 먼저 해당 함수들의 기능과 특징을 정확하게 파악해야합니다. 사실, 성능 측정은 그 다음 단계입니다. JSON 함수는 객체를 복사하기에 편리하고 또 꽤나 빠른 기능입니다. 하지만 set, date, 정규식 객체 등을 제대로 복사하지 못하는 리스크를 가져가는 것은 개인적으로 조금 위험하다는 생각이 듭니다. 미미한 성능차이보다 더 중요한 것은 코드의 안정성입니다.

최종 속도 비교

💻 객체로 이루어진 배열

데이터 삽입코드

const largeObj = [];
const number = 10000;

for (let index = 0; index < number; index++) {
  const obj = { profile: { name: "철수", age: 12 }, grade: "A" };
  largeObj.push(obj);
}

// 데이터 출력 예시
//[{},{},{}... ]

결과

깊은 복사 함수number 1101001000100000
JSON.parse(JSON.stringify)0.040 ms0.051 ms0.156 ms1.099 ms110.561 ms
cloneDeep0.452 ms0.808 ms3.387 ms8.761 ms174.174 ms
structuredClone0.563 ms0.571 ms0.751 ms3.051 ms169.150 ms

💻 프로퍼티가 많은 단일 객체

데이터 삽입코드

const largeObj = {};

const size = 100000;
for (let i = 0; i < size; i++) {
  largeObj[`prop${i}`] = i;`

//데이터 출력 예시
//{
// prop1: {},
// prop2: {},
// prop3: {}, ...
//}

결과

깊은 복사 함수size 1101001000100000
JSON.parse(JSON.stringify)0.038 ms0.044 ms0.131 ms0.435 ms83.993 ms
cloneDeep0.398 ms0.420 ms0.568 ms0.940 ms68.315 ms
structuredClone0.537 ms0.547 ms0.655 ms6.964 ms92.978 ms

보다 정확한 측정을 위해 최소 5-10번의 속도 측정을 했습니다.

측정을 하면서 느낀 것은 역시나 데이터의 종류, 크기, 환경에 따라 실행 결과가 조금씩 결과 값이 달라질 수 있다는 것이었습니다. 특히나 흥미로운것은 객체로 이루어진 긴 배열은 cloneDeep 함수가 상대적으로 느렸지만 프로퍼티가 많은 단일 객체의 경우는 의외의 선전을 보였습니다.

저는 함수 성능을 직접 측정 하는 것을 좋아합니다. 단순히 "이 함수는 빠르고,이 함수는 느려!" 와 같은 말처럼 1차원적으로 표현 할 수 없는 부분이 분명 존재하기 때문입니다.

"어떤 환경 인지" "어느 타입의 데이터를 삽입 했는지 "몇개의 데이터를 삽입했는지" 등을 비교해가며 분석을 해야 합니다.

기능 정리

깊은 복사 함수symbolnew Date정규식순환참조undefinednew Set
JSON.parse(JSON.stringify)
cloneDeep
structuredClone

테스트를 돌려가며 표로 작성해보았습니다. 혹시나 수정할 정보가 있으면 테스트 코드와 함께 댓글을 남겨주세요.

마무리하며

여러 상황에 대비해 코드를 작성해 속도를 측정하고 각 함수가 가지고 있는 특성들을 비교해 보고 싶었습니다. 다만 실무에서는 우리가 예상치 못한 변수를 만나게 될 가능성이 훨씬 높습니다. 함수가 가지고 있는 각 기능들을 정확히 이해한다면 현재 처한 상황에 맞게 어떤 함수를 선택 할지 현명히 판단 할 수 있게 될 것입니다.

😎 직접 코드를 복사해 속도를 확인해보세요!


const _ = require("lodash");
var clone = require("clone");
const { performance } = require("perf_hooks");

const largeObj = {};

// const size = 100000;
// for (let i = 0; i < size; i++) {
//   largeObj[`prop${i}`] = i;
// }

// const largeObj = [];
// const number = 100000;

// for (let index = 0; index < number; index++) {
//   const obj = { profile: { name: "철수", age: 12 }, grade: "A" };
//   largeObj.push(obj);
// }

const measureTime = (action) => {
  const startTime = performance.now();
  action();

  const endTime = performance.now();
  return (endTime - startTime).toFixed(3);
};

const parseStringifyTime = measureTime(() =>
  JSON.parse(JSON.stringify(largeObj))
);
const cloneDeepTime = measureTime(() => _.cloneDeep(largeObj));
const structuredCloneTime = measureTime(() => structuredClone(largeObj));


console.log("JSON.parse(JSON.stringify):", parseStringifyTime, "ms");
console.log("_.cloneDeep:", cloneDeepTime, "ms");
console.log("structuredClone:", structuredCloneTime, "ms");


저는 여러분들이 로직을 복사해 실행시켜 결과 값을 두 눈으로 직접 확인하는 것을 권장합니다. 만약 이해가 가지 않는 로직들이 있으면 지워도 보고, 데이터를 직접 삽입도 해보면서 능동적으로 여러가지 시도를 해보셨으면 좋겠습니다. 블로그 글을 읽기만 했을 때 보다 훨씬 더 빠른 이해가 될 것입니다.

📖 레퍼런스

해당 글은 @hoonsbory 님의 도움을 받아 작성됐습니다.

https://marian-caikovski.medium.com/the-best-way-to-copy-objects-9434cf2fef75
https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm#supported_types
https://dev.to/builderio/deep-cloning-objects-in-javascript-the-modern-way-17kf
https://medium.com/@tiagobertolo/which-is-the-best-method-for-deep-cloning-in-javascript-are-they-all-bad-101f32d620c5
https://medium.com/@tiagobertolo/which-is-the-best-method-for-deep-cloning-in-javascript-are-they-all-bad-101f32d620c5

profile
My Island

8개의 댓글

comment-user-thumbnail
2023년 8월 18일

고생많았어요! 같이 정리하면서 정말 도움 많이 됐습니다!

답글 달기
comment-user-thumbnail
2023년 8월 18일

우와 속도를 직접 측정하신 것이 유익하네요, 빠르다고 해서 무조건적으로 좋은 것도 아니고요. 여러 방면으로 글을 써주셔서 도움이 되었습니다 감사합니다!

답글 달기
comment-user-thumbnail
2023년 8월 21일

정말 유익했습니다. 감사합니다.

1개의 답글
comment-user-thumbnail
2023년 8월 21일

글에서 정성 냄새나요.... 글 저장해두고 헷갈릴 때마다 보러오겠습니다!!

답글 달기
comment-user-thumbnail
2023년 8월 25일

오.. 단순하게 "문자열 관련 파싱 연산이니 느리겠지" 정도로 생각했는데, 역시 세상에 당연한 건 없네요.
좋은 통찰이 되었어요. 정리 감사합니다~

답글 달기
comment-user-thumbnail
2023년 8월 25일

그런데 아무래도 문자열 관련 파싱은 key-value쌍에서 총 문자열 길이의 영향을 받을 거라 생각해서 실험해봤는데, 객체가 긴 문자열을 포함하면 문자열의 길이에 따라 성능이 큰 차이가 있는 것 같아요~
올려주신 예시에서 value에 긴 문자열을 넣어보니 JSON메소드를 활용한 경우에 성능이 수십배까지 차이가 나는 걸 볼 수 있네요.

적절한 상황에 맞게 메소드를 선택해야한다는 것도 함께 생각하면 좋을 것 같아요!

1개의 답글