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

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

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

브라우저 지원

  • Chrome: 98
  • Edge: 98
  • Firefox: 94
  • Safari: 15.4

소스

JavaScript에서 값을 복사하는 것은 거의 항상 깊이가 아닌 얕음입니다. 즉, 중첩된 값을 변경하면 사본과 원본에 모두 변경사항이 표시됩니다.

객체 확장 연산자 ...를 사용하여 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의 (enumarable) 속성을 반복합니다. 속성 이름과 값을 사용하고 새로 만든 빈 객체에 하나씩 할당합니다. 따라서 결과 객체는 모양은 동일하지만 속성 및 값 목록의 자체 사본이 있습니다. 값도 복사되지만 소위 원시 값은 JavaScript 값에서 원시가 아닌 값과 다르게 처리됩니다. 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()가 있습니다. 만세.