[Typescript] 참조 타입(reference type)의 객체 비교 및 shallow copy & deep copy에 대한 고촬

in-ch·2023년 12월 10일
1

typescript

목록 보기
3/4
post-thumbnail

서론


사실상 정확히 말하면 typescrip와 100% 관련된 내용은 아니나 이전 포스팅과 관련된 내용이므로 이어서 정리해보려고 한다.

참고로 비슷한 문제를 겪었던 적이 있었는데, React.memo()에서 depth가 싶은 객체를 props를 받은 컴포넌트를 메모리제이션을 했을 때 제대로 동작하지 않는 문제가 발생했다.

원인은 Javascript의 한계로 깊은 객체의 경우 제대로 객체 비교를 수행하지 못해서 발생한 문제였다. (얕은 객체의 경우 잘 비교한다.)

예를 들어 다음과 같은 예제가 있다.

import React, { useState } from 'react';

const DeepComparisonComponent = React.memo(({ data }) => {
  console.log('Rendering DeepComparisonComponent');
  return (
    <div>
      {data.map(item => (
        <span key={item.id}>{item.value}</span>
      ))}
    </div>
  );
});

const App = () => {
  const [items, setItems] = useState([
    { id: 1, value: 'Item 1' },
    { id: 2, value: 'Item 2' },
  ]);

  const updateItem = () => {
    // 깊은 복사를 통해 새로운 객체를 생성
    const newItems = [...items];
    // 첫 번째 아이템의 값을 변경
    newItems[0].value = 'Updated Item 1';
    // 상태 업데이트
    setItems(newItems);
  };

  return (
    <div>
      <button onClick={updateItem}>Update Item</button>
      <DeepComparisonComponent data={items} />
    </div>
  );
};

export default App;

이 예제에서는 DeepComparisonComponentReact.memo로 래핑되어 있지만, 첫 번째 아이템의 값을 변경할 때 리렌더링이 발생하지 않는다. 이는 React.memo가 얕은 비교를 수행하고 있기 때문이다. 만약 깊은 비교가 필요하다면, React.memo의 두 번째 매개변수로 커스텀 비교 함수를 제공하여 해결할 수 있다.

import React, { useState, useMemo } from 'react';

const DeepComparisonComponent = React.memo(
  ({ data }) => {
    console.log('Rendering DeepComparisonComponent');
    return (
      <div>
        {data.map((item) => (
          <span key={item.id}>{item.value}</span>
        ))}
      </div>
    );
  },
  (prevProps, nextProps) => {
    // 깊은 비교를 수행하여 리렌더링 여부 결정
    return JSON.stringify(prevProps.data) === JSON.stringify(nextProps.data);
  }
);

const App = () => {
  const [items, setItems] = useState([
    { id: 1, value: 'Item 1' },
    { id: 2, value: 'Item 2' },
  ]);

  const updateItem = () => {
    // 깊은 복사를 통해 새로운 객체를 생성
    const newItems = [...items];
    // 첫 번째 아이템의 값을 변경
    newItems[0].value = 'Updated Item 1';
    // 상태 업데이트
    setItems(newItems);
  };

  return (
    <div>
      <button onClick={updateItem}>Update Item</button>
      <DeepComparisonComponent data={items} />
    </div>
  );
};

export default App;

이 예제에서는 React.memo의 두 번째 매개변수로 함수를 전달하고, 이 함수에서 이전 프롭스(prevProps)와 새로운 프롭스(nextProps)의 data 속성을 문자열로 변환하여 비교하고 있다. 이러한 방식으로 깊은 비교를 수행하고, 데이터가 변경되었을 때에만 리렌더링이 발생하도록 할 수 있다.

shallow copy & deep copy


  • Javascript에서 객체와 배열은 참조 타입(reference type)으로 처리.
    • 이는 변수가 객체나 배열을 가리키는 포인터 값으로 처리되어 변수에 대한 복사는 실제 객체나 배열의 복사가 아닌 해당 객체나 배열에 대한 참조가 복사되는 것을 의미
    • 따라서 객체나 배열을 복사하려면, 해당 객체나 배열의 내용을 새로운 객체나 배열에 복사해야 한다. 이 때 사용하는 복사 방식에는 얕은 복사(shallow copy)깊은 복사(deep copy)가 있다.

얕은 복사 (Shallow copy)

얕은 복사는 객체나 배열의 복사본을 만드는 것이지만, 내부에 있는 객체나 배열은 참조 값으로 그대로 유지

즉, 새로운 객체나 배열은 원본 객체나 배열의 주소를 참조하고 있으므로, 하나의 값을 변경하면 다른 쪽도 영향을 받게 된다.
const originalObj = { a: 1, b: { c: 2 } };
const shallowCopyObj = Object.assign({}, originalObj);

originalObj.a = 3;
originalObj.b.c = 4;

console.log(shallowCopyObj.a); // 1
console.log(shallowCopyObj.b.c); // 4

여기서 shallowCopyObj를 출력해보면 a는 그대로 1로 출력되지만 b.c는 originalCopyObj 의 영향을 받아 4로 바뀐 것을 확인해 볼 수 있다.

깊은 복사 (Deep copy)

깊은 복사는 객체나 배열의 내부에 있는 객체나 배열도 모두 복사하여 새로운 객체나 배열을 만드는 방법이다.

이 때, 내부에 있는 객체나 배열은 원본 객체나 배열과는 다른 메모리 공간에 저장되므로, 복사본을 변경하더라도 원본 객체나 배열에는 영향을 주지 않는다.

const originalObj = { a: 1, b: { c: 2 } };
const deepCopyObj = JSON.parse(JSON.stringify(originalObj));

originalObj.a = 3;
originalObj.b.c = 4;

console.log(deepCopyObj.a); // 1
console.log(deepCopyObj.b.c); // 2

여기서 shallowCopyObj를 출력해보면 originalCopyObj의 영향을 받지 않아. 1, 2로 그대로 출력되는 것을 확인해 볼 수 있다.

정리

깊이가 얕은 배열이나 객체를 복사할 때는 얕은 복사를 사용하는 것이 일반적으로 더 빠르고 간단.

그러나 객체나 배열의 깊이가 깊을 때는 깊은 복사를 사용하는 것이 안전하다.

복사 방법 정리


얕은 복사 방법

  • 확산 연산자(...) 사용
const array = [1, 2, 3];
const copyWithEquals = array;
const spreadCopy = [...array];

console.log(array === copyWithEquals); // true
console.log(array === spreadCopy); // false
  • slice() 사용
const array = [1, 2, 3];
const copyWithEquals = array;
const sliceCopy = array.slice();

console.log(array === copyWithEquals); // true
console.log(array === sliceCopy); // false
  • assign() 사용
const array = [1, 2, 3];
const copyWithEquals = array;
const assignCopy = [];
Object.assign(assignCopy, array);

console.log(array === copyWithEquals); // true
console.log(array === assignCopy); // false
  • Array.from() 사용
const array = [1, 2, 3];
const copyWithEquals = array;
const fromCopy = Array.from(array);

console.log(array === copyWithEquals); // true
console.log(array === fromCopy); // false

깊은 복사법 정리

배열을 포함한 javascript 객체가 깊게 중첩된 경우 얕은 복사가 아니라 깊은 복사가 필요하다.

  • 재귀 함수를 통한 딥 카피
const deepCopyFunction = (inObject) => {
  let outObject, value, key;

  if (typeof inObject !== "object" || inObject === null) {
    return inObject; // Return the value if inObject is not an object
  }

  // Create an array or object to hold the values
  outObject = Array.isArray(inObject) ? [] : {};

  for (key in inObject) {
    value = inObject[key];

    // Recursively (deep) copy for nested objects, including arrays
    outObject[key] = deepCopyFunction(value);
  }

  return outObject;
};

let originalArray = [37, 3700, { hello: "world" }];
console.log("Original array:", ...originalArray); // 37 3700 Object { hello: "world" }

let shallowCopiedArray = originalArray.slice();
let deepCopiedArray = deepCopyFunction(originalArray);

originalArray[1] = 0; // Will affect the original only
console.log(`originalArray[1] = 0 // Will affect the original only`);
originalArray[2].hello = "moon"; // Will affect the original and the shallow copy
console.log(
  `originalArray[2].hello = "moon" // Will affect the original array and the shallow copy`
);

console.log("Original array:", ...originalArray); // 37 0 Object { hello: "moon" }
console.log("Shallow copy:", ...shallowCopiedArray); // 37 3700 Object { hello: "moon" }
console.log("Deep copy:", ...deepCopiedArray); // 37 3700 Object { hello: "world" }
  • JSON.parse/stringfy를 이용한 딥 카피
const sampleObject = {
  string: "string",
  number: 123,
  boolean: false,
  null: null,
  notANumber: NaN, // NaN values will be lost (the value will be forced to 'null')
  date: new Date("1999-12-31T23:59:59"), // Date will get stringified
  undefined: undefined, // Undefined values will be completely lost, including the key containing the undefined value
  infinity: Infinity, // Infinity will be lost (the value will be forced to 'null')
  regExp: /.*/, // RegExp will be lost (the value will be forced to an empty object {})
};

console.log(sampleObject);
console.log(typeof sampleObject.date); // object

const faultyClone = JSON.parse(JSON.stringify(sampleObject));

console.log(typeof faultyClone.date); // string

결론


객체를 사용할 때 둘의 차이점을 명확히 인식하고 사용하자 !!

profile
인치

0개의 댓글