내용 보기

작성자

아롱이 (IP : 172.17.0.1)

날짜

2020-07-28 09:04

제목

[JavaScript] 깊은 복사와 얕은 복사에 대한 심도있는 이야기



읽기 전에

이 글은 여러분이 자바스크립트, 조금 더 명확하게는 ECMA-262 에 명시된 다음의 내용을 모두 알고 있다는 가정하에 작성되었다. 그리고, 이 글은 자바스크립트 명세서를 읽고 이해할 수 있을 정도의 수준은 되어야 제대로 이해할 수 있으니 난이도가 조금 있는 셈이다.

  • Object.assign
  • JSON.stringify
  • Iterable

설명을 시작하기에 앞서

Array.prototype.slice

이 방식은 아마도 제일 널리 알려진 방식일텐데, 주로 배열을 깔끔하게 복사할 때 사용할 수 있다. Array.prototype.slice 는 start 부터 end 까지 대상 배열에서 꺼내와 새로운 배열을 만들어 값을 집어 넣는다. start 와 end 가 주어지지 않으면 전체 배열을 복사한다.

const arr = [1, 2, 3];
const copied = arr.slice();
checker(arr, copied); // truecopied.push(4);
checker(arr, copied); // false
const arr = [1, 2, [3, 4]];
const copied = arr.slice();
checker(arr, copied); // truecopied[2].push(5);
checker(arr, copied); // true
~~~~~~~
// This shouldn't return true anymore

Spread Operator

이터러블을 모르면 이제 여기서부터 이해하기 어려울 수 있다.

Image for post
모든 자바스크립트 객체는 반복이라는 행위를 수행하기 위해선 빨간색 네모에 해당하는 녀석을 갖고 있어야한다
const b = [1, 2, 3];
const a = [ ...b ];
if (!(Typeof(b) is Iterable)) {
throw TypeError
}
var a = [];
for (var i = 0; i < b.length; i += 1) {
a.push(b[i]);
}
const arr = [1, 2, [3, 4]];
const copied = [ ...arr ];
checker(arr, copied); // truecopied[2].push(5);
checker(arr, copied); // true
~~~~~~~
// It should've been false

Object.assign

가끔 Object.assign 으로 객체를 복사하는 개발자들을 봤는데, 시도는 좋았지만 역시나 결과는 “복사할 수 없다” 이다.

const arr = [1, 2, [3, 4]];
const copied = Object.assign([], arr);
checker(arr, copied); // truecopied[2].push(5);
checker(arr, copied); // true
~~~~~~~
// It should've been false

JSON.parse & JSON.stringify

이제 이 글의 진짜 주제에 대해 본격적으로 다뤄보기로 한다.

const arr = [1, 2, [3, 4]];
const copied = JSON.parse(JSON.stringify(arr));
checker(arr, copied); // truecopied[2].push(5);
checker(arr, copied); // false
const obj = { d: new Date() };
const copied = JSON.parse(JSON.stringify(obj));
checker(obj, copied); // false

대체, 왜?

Image for post
Image for post
왼쪽이 원본 / 오른쪽이 복사본
JSON.stringify(BigInt(1));
~~~~~~~~~~~~~~
// Uncaught TypeError
// Do not know how to serialize a BigInt
const a = [];
a[0] = a;
JSON.stringify(a);
// Uncaught TypeError
// Converting circular structure to JSON
const arr = [function(){}, () =>{}];
const copied = JSON.parse(JSON.stringify(arr));
checker(arr, copied); // false
Image for post
Image for post
왼쪽이 원본 / 오른쪽이 복사본
Image for post
The image is from ECMA-404 documentation
Image for post
Image source: ECMA-262
  1. 객체를 넘길 경우 — 해당 { key: value } 를 모두 삭제한다. 왜냐하면 JSON.stringify 에는 두 번째 인자로 함수를 넘길 수 있는데, 이 함수의 반환값이 fasly 인 경우엔 해당 { key: value} 를 결과 값에 포함시키지 않는다. 마찬가지로, 객체를 넘겼는데 그 안에 함수나 undefined 과 같은 값이 value 로 들어있다면 입력 받은 값의 평가 과정에서 해당 값이 falsy 로 간주되어 결과에 포함되지 않는다.
  2. 단일 값으로 넘길 경우 — undefined 로 처리한다
  3. 그 외에 기타 경우 — 에러 발생 (대표적으로 BigInt )
JSON.stringify(function() {})     // undefined
JSON.stringify([function() {}]) // [null]
JSON.stringify({f: function() {}) // {}
JSON.stringify({f: () => {}) // {}
JSON.stringify({d: undefined}) // {}

Lodash 와 Ramda 를 이용한 비교

하늘이 무너져도 솟아날 구멍이 있다고 하던가. 아예 방법이 없는 것은 아니다. 대표적으로 lodash 와 ramda 가 완전한 깊은 복사를 구현해놓았는데, 그 둘을 비교해보자.

Image for post
Image for post
왼쪽이 loadsh / 오른쪽이 ramda
The cloning method in Lodash
Image for post
The cloning method in Ramda
Image for post
Image for post
https://github.com/tc39/ecma262/issues/1319

결론

자바스크립트에서 주로 알려져있는 깊은 복사를 하는 방식은 사실 모두 깊은 복사를 수행할 수 없다. 내부적으로 그렇게 모델링 되어있지 않기때문. 그렇기 때문에 일부 라이브러리들은 사용자의 요구를 채워주기 위해 다소 낮은 퍼포먼스를 띠더라도 정확하게 모든 요소를 복사하는 메소드를 구현해 놓았다.

추가)

BigInt 는 JSON.stringify 를 원래 통과해야 정상이지만, JSON.parse 로 다시 원상복귀할 때 어떻게 값을 원복시킬 것인지에 대한 코드 처리가 없기때문에 JSON.stringify 역시 현재는 이용할 수 없다.

출처1

https://medium.com/watcha/%EA%B9%8A%EC%9D%80-%EB%B3%B5%EC%82%AC%EC%99%80-%EC%96%95%EC%9D%80-%EB%B3%B5%EC%82%AC%EC%97%90-%EB%8C%80%ED%95%9C-%EC%8B%AC%EB%8F%84%EC%9E%88%EB%8A%94-%EC%9D%B4%EC%95%BC%EA%B8%B0-2f7d797e008a

출처2