구조화된 Clone을 사용하여 자바스크립트 딥 복사

이제 플랫폼에 깊은 복사를 위한 내장 함수인 structuredClone()이 함께 제공됩니다.

가장 오랫동안 JavaScript 값의 전체 사본을 만들려면 해결 방법과 라이브러리를 이용해야 했습니다. 이제 플랫폼이 딥 복사를 위한 내장 함수인 structuredClone()와 함께 제공됩니다.

브라우저 지원

  • Chrome: 98. <ph type="x-smartling-placeholder">
  • Edge: 98. <ph type="x-smartling-placeholder">
  • Firefox: 94 <ph type="x-smartling-placeholder">
  • Safari 15.4. <ph type="x-smartling-placeholder">

소스

얕은 사본

JavaScript에서 값을 복사하는 작업은 deep이 아닌 거의 항상 얕은 복사입니다. 즉, 깊이 중첩된 값에 대한 변경사항은 원본뿐만 아니라 사본에도 표시됩니다.

객체 분산 연산자 ...를 사용하여 자바스크립트에서 부분 복사본을 만드는 한 가지 방법은 다음과 같습니다.

const myOriginal = {
  someProp: "with a string value",
  anotherProp: {
    withAnotherProp: 1,
    andAnotherProp: true
  }
};

const myShallowCopy = {...myOriginal};

부분 사본에서 직접 속성을 추가하거나 변경하면 원본이 아닌 사본에만 영향을 미칩니다.

myShallowCopy.aNewProp = "a new value";
console.log(myOriginal.aNewProp)
// ^ logs `undefined`

그러나 깊이 중첩된 속성을 추가하거나 변경하면 사본과 원본 속성 모두에 영향을 미칩니다.

myShallowCopy.anotherProp.aNewProp = "a new value";
console.log(myOriginal.anotherProp.aNewProp) 
// ^ logs `a new value`

{...myOriginal} 표현식은 분산 연산자를 사용하여 myOriginal의 (열거형 가능) 속성에 대해 반복됩니다. 속성 이름과 값을 사용하고 새로 만든 빈 객체에 이를 하나씩 할당합니다. 따라서 결과 객체의 모양은 동일하지만 속성 및 값 목록의 자체 사본이 있습니다. 값도 복사되지만 이른바 원시 값은 자바스크립트 값에 의해 원시 값이 아닌 값과 다르게 처리됩니다. MDN 인용문:

JavaScript에서 프리미티브 (프리미티브 값, 원시 데이터 유형)는 객체가 아니며 메서드가 없는 데이터입니다. 문자열, 숫자, bigint, 부울, 정의되지 않음, 기호, null의 7가지 원시 데이터 유형이 있습니다.

MDN — 프리미티브

원시 값이 아닌 값은 참조로 처리됩니다. 즉, 값을 복사하는 작업은 실제로는 동일한 기본 객체에 대한 참조를 복사하는 것이므로 얕은 복사 동작이 발생합니다.

전체 사본

얕은 카피의 반대는 깊은 카피입니다. 완전 복사 알고리즘은 또한 객체의 속성을 하나씩 복사하지만, 다른 객체에 대한 참조를 찾으면 자체를 재귀적으로 호출하여 해당 객체의 사본도 생성합니다. 두 코드가 실수로 객체를 공유하고 자신도 모르게 서로의 상태를 조작하지 않도록 하려면 이 작업이 매우 중요할 수 있습니다.

JavaScript에서 값의 깊은 복사본을 만드는 쉽고 좋은 방법은 없었습니다. 많은 사람들이 Lodash의 cloneDeep() 함수와 같은 서드 파티 라이브러리를 사용했습니다. 이 문제에 대한 가장 일반적인 해결책은 아마도 JSON 기반 해킹이었습니다.

const myDeepCopy = JSON.parse(JSON.stringify(myOriginal));

실제로 이 방법은 매우 인기 있는 해결 방법으로 V8이 JSON.parse() 적극적으로 최적화했으며, 특히 위의 패턴을 최대한 빠르게 최적화했습니다. 속도가 빠르지만 몇 가지 단점과 트립 와이어가 있습니다.

  • 재귀 데이터 구조: 재귀 데이터 구조를 제공하면 JSON.stringify()이 발생합니다. 이는 연결 목록이나 트리를 사용할 때 상당히 쉽게 발생할 수 있습니다.
  • 기본 제공 유형: 값에 Map, Set, Date, RegExp, ArrayBuffer 등 다른 JS 기본 제공 항목이 포함된 경우 JSON.stringify()이 발생합니다.
  • 함수: JSON.stringify()는 조용히 함수를 삭제합니다.

구조화된 클론

플랫폼에는 이미 몇 가지 위치에서 JavaScript 값의 전체 사본을 만들 수 있는 기능이 필요했습니다. IndexedDB에 JS 값을 저장하려면 디스크에 저장하고 나중에 JS 값을 복원하기 위해 역직렬화할 수 있도록 일종의 직렬화가 필요합니다. 마찬가지로 postMessage()를 통해 WebWorker에 메시지를 전송하려면 한 JS 영역에서 다른 JS 영역으로 JS 값을 전송해야 합니다. 여기에 사용되는 알고리즘을 '구조화된 클론'이라고 하며, 최근까지는 개발자가 쉽게 액세스할 수 없었습니다.

하지만 이제는 상황이 바뀌었어! HTML 사양은 개발자가 JavaScript 값의 전체 사본을 쉽게 만들 수 있는 수단으로 이 알고리즘을 정확하게 실행하는 structuredClone()라는 함수를 노출하도록 수정되었습니다.

const myDeepCopy = structuredClone(myOriginal);

완료되었습니다. 이것이 전체 API입니다. 자세한 내용은 MDN 문서를 참고하세요.

기능 및 제한사항

구조화된 클론은 JSON.stringify() 기법의 많은 단점을 해결합니다. 구조화된 클론은 주기적 데이터 구조를 처리할 수 있고 많은 기본 제공 데이터 유형을 지원할 수 있으며 일반적으로 더 강력하고 빠릅니다.

하지만 다음과 같은 제한사항이 있어서 경계할 수 있습니다.

  • 프로토타입: structuredClone()를 클래스 인스턴스와 함께 사용하면 반환으로 일반 객체를 얻게 됩니다. 값을 전달합니다.
  • 함수: 객체에 함수가 포함되어 있는 경우 structuredClone()에서 DataCloneError 예외가 발생합니다.
  • 클론 불가능 항목: 일부 값은 구조화되지 않은 클론이 불가능합니다. 특히 Error 및 DOM 노드와 같은 값을 사용하세요. 그것은 structuredClone()이 발생합니다.

이러한 한계가 사용 사례에 방해가 되는 경우 Lodash와 같은 라이브러리는 사용 사례에 적합하거나 적합하지 않을 수 있는 다른 딥 클론 알고리즘의 커스텀 구현을 제공합니다.

성능

새로운 마이크로 벤치마크 비교는 하지 않았지만 structuredClone()이 노출되기 전인 2018년 초에 비교를 수행했습니다. 당시에는 JSON.parse()이 아주 작은 물체에 적합한 가장 빠른 옵션이었습니다. 저는 그대로 유지될 것으로 기대합니다. 구조화된 클론에 의존하는 기법은 더 큰 객체에 대해 (상당히) 더 빨랐습니다. 새 structuredClone()는 다른 API를 악용하는 오버헤드 없이 제공되고 JSON.parse()보다 더 강력하다는 점을 고려할 때 깊은 사본을 만들기 위한 기본 접근 방식으로 사용하는 것이 좋습니다.

결론

변경할 수 없는 데이터 구조를 사용하거나 함수가 원본에 영향을 주지 않고 객체를 조작할 수 있도록 하기 위해 JS에서 값의 전체 복사본을 만들어야 하는 경우 더 이상 해결 방법이나 라이브러리를 찾지 않아도 됩니다. 이제 JS 생태계에 structuredClone()가 있습니다. 만세.