객체를 복사하려고 한다면 깊은 복사
와 얕은 복사
라는 개념에 대해 한 번쯤 고민하게 됩니다. 직관적으로 단순하게 생각했을 때에는 깊은 복사의 동작을 기대하기 마련이지만, 대개 실제로는 얕은 복사가 수행되기 때문일 것입니다.
JavaScript
의 경우, 언어 자체의 구조적인 이유로 모든 복사는 기본적으로 얕은 복사를 구현하게끔 되어 있습니다. 아키텍처의 관점에서 깊은 복사가 정말로 필요하느냐 아니냐는 차치하고, 어쨌든 깊은 복사를 하고자 한다면 제한된 조건 하에서 일종의 우회 구현하거나 lodash
같은 라이브러리를 사용하는 경우가 일반적이었습니다.
하지만 Web API
로 StructuredClone
이 2022년 즈음부터 지원되기 시작하면서, 선택지가 하나 더 늘어나게 되었습니다.
깊은 복사와의 비교를 위해 잠시 얕은 복사를 먼저 언급하겠습니다. 얕은 복사는 원시primitive 자료형이라면 값을 그대로 복사하지만, 객체라면 참조reference를 복사하므로, 공유 참조가 되면서 원본과 복사된 객체 중 하나가 수정되면 다른 쪽에도 영향이 가게 됩니다. 사이드 이펙트, 즉 개발자의 실수로 의도하지 않은 변경이 일어날 여지가 존재하는 셈입니다.
JavaScript
에서 원시 자료형이란 '객체가 아니며 메소드가 없는 데이터'로 정의됩니다.string
,number
,bigint
,boolean
,undefined
,symbol
,null
이 해당합니다.
흔히 사용되는 방법으로, 전개 연산자로 얕은 복사본을 만드는 예시입니다. 아래 코드와 같이, 중첩된 프로퍼티를 추가/수정하면 원본과 사본 모두 변경되는 모습을 볼 수 있습니다.
const original = {
someProp: 'value',
anotherProp: {
withAnotherProp: true,
}
};
const shallowCopy = { ...original };
shallowCopy.newProp = 'new value';
shallowCopy.anotherProp.newProp = 'new value';
console.log(original.newProp); // undefined
console.log(original.anotherProp.newProp); // 'new value'
마찬가지로 자주 사용되는 Object.assign()
의 경우에도 동일합니다. MDN 문서에서도 아래와 같이 설명하고 있습니다.
Object.assign()
은 프로퍼티의 값을 복사하므로, 깊은 복사를 수행하려면 다른 방법을 사용해야 합니다. 만약 값이 객체에 대한 참조라면 참조 값만 복사합니다.
const original = {
someProp: 'value',
anotherProp: {
withAnotherProp: true
}
};
const shallowCopy = Object.assign({}, original);
shallowCopy.anotherProp.withAnotherProp = false;
console.log(original.anotherProp.withAnotherProp); // false
얕은 복사와 달리, 복사본이 원본의 참조와 동일한 참조를 공유하지 않는 별개의 독립적인 복사본을 생성합니다. 객체의 참조가 아니라, 참조가 되는 모든 객체 자체에 대한 완전히 새로운 사본을 만드는 것입니다.
아래의 선택지 이외에도 재귀 함수를 직접 구현하는 방법으로로 구현할 수 있겠지만, 어느 정도 원리를 이해하고 있다면 굳이 바퀴를 다시 발명할 필요는 없을 것입니다.
어째서
JavaScript
에는 깊은 복사를 구현하는 메소드가 없었는지는 아래 TC39의 이슈들을 통해서 알아볼 수 있습니다.
일종의 트릭으로, 직렬화가 가능하다면 JSON.stringify()
로 객체를 JSON string으로 변환한 다음 다시 JSON.parse()
로 변환하여 새로운 객체를 생성하는 방법입니다.
const deepCopy = JSON.parse(JSON.stringify(original));
JSON의 문법 구조가 JavaScript
에 비하면 훨씬 단순하기 때문에, 높은 효율로 파싱이 가능하다는 점에서 최적화의 여지가 있어 종종 언급되는 트릭이기도 했지만 (특히 V8 엔진), 막상 벤치마크 결과 등을 참고해보면 성능적으로 그렇게 대단한 이득이 있는 것은 아닙니다.
또한 함수는 물론 Symbol
, DOM
, Map
, Set
, Date
, ArrayBuffer
, RegExp
등은 직렬화 과정에서 실패하거나 손실된다는 한계도 있습니다.
그래서 흔히 사용되는 lodash
라이브러리의 cloneDeep을 사용하는 경우가 많았습니다. 상대적으로 성능상 트레이드오프가 있긴 하지만, JSON 트릭보다 좀 더 의도에 부합하게(정확하게) 재귀적으로 깊은 복사를 수행할 수 있기 때문입니다.
const deepCopy = _.cloneDeep(original);
이러한 필요 때문이었는지, 네이티브로 깊은 복사를 수행할 수 있는 방법이 추가되었습니다. MDN에 따르면 구조화된 복제 알고리즘을 사용하여 깊은 복사를 수행할 수 있습니다. JSON 트릭에 비하면 당연히 제한 사항도 더 적고, 성능도 좋습니다.
const original = { name: 'MDN' };
original.itself = original;
const clone = structuredClone(original);
console.assert(clone !== original);
console.assert(clone.name === 'MDN');
console.assert(clone.itself === clone);
호환성은 아래와 같습니다. 주요 브라우저들은 다소 제한적으로 지원하고 있고, Node.js
의 경우에는 현재 글 작성 시점 기준으로는 LTS 버전에 포함돼 있지 않아 사용이 불가할 수 있습니다. 타입스크립트에서도 4.7 버전부터 지원합니다. 또한 폴리필도 존재합니다 : core-js
, structured-clone
하지만 structuredClone()
역시 여전히 일부 제한 사항은 있습니다.