주소 복사와 값 복사
데이터는 메모리에 할당된다. 그러므로 데이터는 값(value)과 메모리주소(reference)를 갖는다. reference 는 참조라는 뜻이지만 주소라고 부를 수 있는 이유는, 그 메모리주소를 참조하면 메모리에 할당된 데이터를 가져올 수 있기 때문이다.
그러므로 데이터를 복사하는 방식은 주소를 복사(얕은 복사)하는 방법과 값을 복사(깊은 복사)하는 두개의 방식으로 나뉜다.
자바스크립트의 데이터타입 중 원시형에 해당하는 number, string, boolean, null, undefined 는 불변성을 보장받는다. 그러므로 복사 방식에 상관없이 사본은 새로운 메모리를 할당받는다. 추측건대 자주 사용되는 타입이면서 차지하는 메모리가 크지 않기 때문인 것 같다. 그러나 이 다섯 타입을 제외한 모든 객체들은 참조형 데이터이며, 복사하는 방식에 따라 사본도 원본과 같은 메모리를 가리킬지, 아니면 같은 값을 가지면서 새로운 메모리를 할당받을지 결정된다.
https://gist.github.com/yeonwooz/2c47a20eea5fe26e17cfe677b4062dfe
만약 프로그래머가 whiteShirt 라는 객체를 만들어두고, 새로운 쇼핑아이템들을 일일이 새로 만드는 대신에 이런 식으로 얕은 복사를 한다면, 두개의 변수가 같은 주소값을 가리키므로 어떤 변수를 통해 프로퍼티를 수정하든 두 변수 모두에 반영된다. 프로그래머 입장에서는 의도하지 않은 원본훼손이 일어난 것으로 볼 수 있다.
뎁스가 1짜리인 오브젝트에서는 const pants = {...whiteShirt} 이렇게 전개구문을 사용하거나 Object.assign을 사용하면 깊은 복사가 가능한데, 만약 뎁스가 깊어진다면 이 방식을 사용해도 하위 뎁스에서는 얕은 복사가 발생한다.
깊은 복사는 원본의 각 속성값을 그대로 복사하되 전부 새로운 메모리에 할당한 사본을 만드는 것이다. 따라서 원본의 모든 속성과 사본의 모든 속성은 서로 독립적이다. 깊은복사를 할 수 있는 방법은 직접 함수를 구현하는 방법도 있지만, 이미 제공되는 함수를 이용할 수도 있다.
깊은 복사 방법으로 가장 유명한 것은 Lodash 의 cloneDeep 이다.
2. JSON.stringify() 함수를 통한 직렬화
속도가 상대적으로 느리고, 데이터 타입에 따라서는 유실가능성이 존재한다. 가령 객체의 필드 중 값이 undefined인 필드는 무시된다. 또 만약에 특정 필드의 값이 NaN 이면 null 로 자동변환된다.
stringify 한 문자열은 JSON.parse 로 다시 역직렬화해서 사용해야 한다.
3. structuredClone() 함수를 통한 직렬화
프로토타입 체인을 제외한 모든 데이터가 완벽히 복제된다고 한다. 이 함수는 다음과 같이 사용한다.
https://gist.github.com/yeonwooz/02f84d1802d1294d4d71e320d3f464ca
4. 프로토타입 체인까지 포함하여 깊은복사를 하고 싶다면, MDN은 다음의 방식을 제안하고 있다.
constructor 종류에 따라서 케이스 대응을 해준 후에 재귀적으로 클론하는 방법이다.
https://gist.github.com/yeonwooz/c8834d77c37f571025b3a45bfb8f6135
JSON.stringify나 structuredClone 은 엄밀히 말해서 깊은복사 메소드가 아니라 직렬화 메소드이다.
직렬화란, 객체 포맷의 데이터를 깊이가 없는 바이트포맷으로 변환하는 것이다. 직렬화는 원본을 훼손하지 않고 데이터송신을 위한 새로운 데이터를 만드는데, 값은 원본에서 복사해오므로 깊은복사가 이루어진다. 따라서 직렬화를 이용하면 깊은 복사가 가능한 것이다.
Photo by Natalia Yakovleva on Unsplash
TMI1.
자바스크립트 이론개념이 머릿속에 정립되어있지 않음을 많이 느끼는 몇달이었고, 최근 코어자바스크립트와 모던자바스크립트를 같이 읽고 있다. 기본이 흔들리지 않도록 해야겠다.