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

이제 플랫폼에 딥 복사를 위한 기본 제공 함수인 구조화된Clone()이 함께 제공됩니다.

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

브라우저 지원

  • 98
  • 98
  • 94
  • 15.4

소스

얕은 사본

자바스크립트에서 값을 복사하는 작업은 deep이 아니라 거의 항상 얕은 값입니다. 즉, 복잡하게 중첩된 값에 대한 변경사항은 원본뿐만 아니라 사본에도 표시됩니다.

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

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 인용:

자바스크립트에서 원시 (원시값, 원시 데이터 유형)는 객체가 아니고 메서드가 없는 데이터입니다. 7가지 원시 데이터 유형(문자열, 숫자, bigint, 불리언, 정의되지 않음, 기호, null)이 있습니다.

MDN - 프리미티브

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

전체 사본

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

과거에는 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 값의 전체 사본을 생성할 수 있는 기능이 필요했습니다. JS 값을 IndexedDB에 저장하려면 JS 값을 디스크에 저장하고 나중에 역직렬화하여 JS 값을 복원할 수 있도록 일종의 직렬화가 필요합니다. 마찬가지로 postMessage()를 통해 WebWorker에 메시지를 전송하려면 한 JS 영역에서 다른 JS 영역으로 JS 값을 전송해야 합니다. 여기에 사용되는 알고리즘을 'Structured Clone'이라고 하며, 최근까지는 개발자가 쉽게 액세스할 수 없었습니다.

이제 상황이 달라졌습니다. 개발자가 JavaScript 값의 깊은 사본을 쉽게 만들 수 있도록 정확하게 이 알고리즘을 실행하는 structuredClone()라는 함수를 노출하도록 HTML 사양이 수정되었습니다.

const myDeepCopy = structuredClone(myOriginal);

간단하죠? 이것이 전체 API입니다. 자세한 내용은 MDN 도움말을 참조하세요.

기능 및 제한사항

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

하지만 여전히 일부 제한사항이 있습니다.

  • 프로토타입: structuredClone()를 클래스 인스턴스와 함께 사용하면 구조화된 클론이 객체의 프로토타입 체인을 삭제하므로 일반 객체를 반환 값으로 얻게 됩니다.
  • 함수: 객체에 함수가 포함된 경우 structuredClone()에서 DataCloneError 예외가 발생합니다.
  • Non-cloneable: 일부 값은 구조화되어 있지 않으며 특히 Error 및 DOM 노드가 있습니다. 이 경우 structuredClone()이 발생합니다.

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

성능

아직 새로운 마이크로 벤치마크 비교는 하지 않았지만 structuredClone()가 노출되기 전에 2018년 초에 비교했습니다. 당시에는 JSON.parse()가 매우 작은 객체의 경우 가장 빠른 옵션이었습니다. 저는 이 부분이 그대로 유지될 것으로 예상합니다. 구조화된 클론에 의존하는 기술은 더 큰 객체에서 상당히 더 빨랐습니다. 새로운 structuredClone()는 다른 API 악용으로 인한 오버헤드 없이 제공되며 JSON.parse()보다 더 강력하므로 전체 사본을 만드는 기본 접근 방식으로 사용하는 것이 좋습니다.

결론

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