이 글은 아래 원문을 번역한 글로 의역이 있을 수 있습니다. 정확한 의미를 파악하고 싶으신 분은 원문을 참고해주시기 바랍니다.
원문: https://web.dev/structured-clone/
저작권 정보: https://creativecommons.org/licenses/by/4.0/
오랫동안, 자바스크립트의 값을 깊게 복사하려면 별도의 해결 방법에 의지하거나 라이브러리를 사용해야만 했습니다. 이제 깊은 복사를 위한 내장기능으로 structuredClone()이 제공됩니다.
지원 브라우저: Chrome X / Firefox 94 / Edge X / Safari preview 출처
글 작성 당시, 모든 브라우저에서 일일 배포(nightly releases)로 이 API를 구현했으며, Firefox는 Firefox 94에서 이 API를 안정(stable)되게 제공했습니다. 또한 Node 17과 Deno 1.14도 이 API를 구현했습니다. 바로 이 기능을 사용할 수 있습니다. 후회하지 않을 거예요.
얕은 복사
자바스크립트에서 값을 복사하면 대체로 깊은 복사가 아니라 얕은 복사입니다. 즉, 깊이 중첩되어 있는 값의 변경은 원본뿐만 아니라 사본에도 적용됩니다.
자바스크립트에서 얕은 복사를 하는 방법 중 하나는 전개 연산자(spread operator, ...)를 사용하는 것입니다.
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}` 식은 전개 연산자(spread operator)를 사용하여 myOriginal의 (셀 수 있는, enumerable) 속성에 대해 반복됩니다. 속성 이름과 값을 사용하여, 새롭게 만들어진 빈 객체에 하나씩 할당합니다. 따라서 결과 객체는 모양은 동일하지만, 속성과 값의 복사본이 있습니다. 값은 복사되지만, 소위 원시 값이라 불리는 값과 원시 값이 아닌 값은 다르게 처리됩니다.
자바스크립트에서 원시(primitive, 원시 값, 원시 타입)은 객체가 아니고 메서드를 갖지 않는 데이터를 말합니다. 원시 데이터 타입은 7종류로 string, number, bigint, boolean, undefined, symbol, 그리고 null이 있습니다.
MDN - Primitive
원시 값이 아닌 값은 참조로 처리되는데, 즉 값을 복사하는 작업은 근본적으로 동일한 객체에 대한 참조를 복사하는 것으로 결국 얕은 복사입니다.
깊은 복사
얕은 복사의 반대말은 깊은 복사입니다. 깊은 복사 알고리즘 또한 객체의 속성을 하나하나 복사하지만, 다른 객체의 참조를 찾으면 재귀적으로 호출하여 해당 객체의 복사본을 만듭니다. 이는 두 코드 조각이 의도치 않게 객체를 공유하거나 모르는 사이에 서로의 상태를 조작하지 않도록 하는데 매우 중요할 수 있습니다.
이전에는 자바스크립트에서 깊은 복사를 하는 쉽고 좋은 방법이 없었습니다. 많은 사람들이 Lodash의 cloneDeep() 함수와 같은 외부 라이브러리에 의존했습니다. 가장 일반적인 해결책은 JSON 기반이었습니다.
const myDeepCopy = JSON.parse(JSON.stringify(myOriginal));
사실, 이것은 매우 인기 있는 해결방법이었고, V8에서 JSON.parse()를 공격적으로 최적화해서 이 패턴을 가능한 빠르게 만들었습니다. 빠른 속도에도 불구하고, 몇 가지 단점과 에러 발생 요인이 있습니다.
- 재귀 데이터 구조: JSON.stringify()는 재귀 데이터 구조가 있으면 에러를 던집니다. 링크드 리스트나 트리를 다룰 때 꽤 쉽게 발생할 수 있습니다.
- 내장 타입: JSON.stringify()는 값이 Map, Set, Date, RegExp 또는 ArrayBuffer와 같은 다른 자바스크립트 내장 타입을 제대로 변환하지 못합니다.
- 함수: JSON.stringify()는 함수를 조용히 폐기합니다.
구조화된 복제(Structured cloning)
이미 내부적으로 여러 곳에서 자바스크립트의 값을 깊은 복사할 수 있는 기능이 필요했었습니다. 예를 들어, 자바스크립트의 값을 IndexedDB에 저장하려면 특정 형식으로 직렬화가 필요합니다. 직렬화를 하면 디스크에 저장할 수 있고, 나중에 역직렬화 하여 자바스크립트 값으로 복원할 수 있습니다. 마찬가지로, postMessage()를 통해 WebWorker로 메시지를 전송하려면 자바스크립트 값을 한 영역에서 다른 영역으로 옮겨야 합니다. 이를 위해 사용되는 알고리즘을 'Structured Clone(구조화된 복제)'이라고 하는데, 최근까지 개발자들이 쉽게 접근할 수 없었습니다.
이제 바뀌었습니다! HTML 규격은 개발자들이 자바스크립트 값을 쉽게 깊은 복사할 수 있도록 structuredClone()이라는 함수를 노출시키는 쪽으로 수정되었습니다.
const myDeepCopy = structuredClone(myOriginal);
끝입니다! 이게 이 API의 전부입니다. 만약 자세한 내용을 더 알아보고 싶으면 MDN 문서를 참조하세요.
기능 및 한계
구조화된 복제(structured cloning)는 (전부는 아니지만) 많은 JSON.stringify()의 단점을 보완합니다. 구조화된 복제(structured cloning)는 순환 데이터 구조를 처리할 수 있고, 많은 내장 데이터 타입을 지원하며, 대게 더 강력하고 빠릅니다.
그러나, 여전히 몇 가지 한계가 있어 허를 찔릴 수 있습니다.
- 프로토타입: structuredClone()으로 Class 인스턴스를 복제하면, 구조화된 복제는 객체의 프로토타입 체인을 무시하므로 일반 객체를 반환합니다.
- 함수: 객체에 함수가 포함되어 있으면 조용히 폐기됩니다.
- 복제 불가: 에러 객체나 DOM 노드는 구조화된 복제(structured clone)가 불가능한 경우가 많습니다. 이런 경우 structuredClone() 시도 시, 에러를 던집니다.
이런 한계들 중에 당신의 사용 용도에 맞지 않는 것이 있다면, Lodash와 같은 라이브러리들은 여전히 당신의 사용 용도에 맞거나 혹은 맞지 않는 다른 깊은 복사 알고리즘을 제공합니다. 이를 활용하면 됩니다.
성능
새로 성능 비교를 해본 것은 아니지만, 2018년 초, structuredClone()이 노출되기 이전에 비교한 적이 있습니다. 그 당시에, JSON.parse()는 작은 객체를 복사하기 위한 가장 빠른 선택지였습니다. 저는 계속 그럴 거라고 기대합니다. 구조화된 복제(structured cloning) 기술은 객체가 크면 클수록 (훨씬) 빨랐습니다. 새로 제공된 structuredClone()이 다른 API를 남용하는 오버헤드가 없고, JSON.parse() 보다 강력하다는 점을 고려하면, 깊은 복사를 할 때는 이 내장 API, structuredClone()을 사용하는 것을 추천합니다.
결론
자바스크립트에서 값의 깊은 복사가 필요할 때(불변 데이터를 만들 때 혹은 원본에 영향을 미치지 않고 객체를 조작하는 함수를 만들 때) 더 이상 해결 방법이나 라이브러리를 찾을 필요가 없습니다. 이제 자바스크립트 생태계는 structuredClone()을 가졌습니다.
'JavaScript' 카테고리의 다른 글
An in-depth perspective on webpack's bundling process (한글) (2) (0) | 2022.07.13 |
---|---|
An in-depth perspective on webpack's bundling process (한글) (1) (0) | 2022.04.06 |
You Can Label a JavaScript `if` Statement (한글) (1) | 2022.01.26 |
Building A Dynamic Header With Intersection Observer (한글) (0) | 2021.09.22 |
Implementing Private Fields for JavaScript (한글) (0) | 2021.08.18 |