[JS] 얕은복사와 깊은복사(feat.리액트에서 깊은복사를...?)

고정원·2021년 7월 21일
0
post-thumbnail

자바스크립트의 복사 개념

참조형 데이터를 어떻게 잘 복사 할 것인가?

얕은 복사(Shallow copy)

객체가 담겨있는 변수를 다른 변수에 할당하면 데이터 복사가 아닌 참조가 일어나게 되어, 한 변수의 데이터를 변경하면 다른 변수의 데이터도 함께 변경이 된다.

const man1 = {name:"GO"};

const man2 = man1;

man1.name = "Garden";

console.log(man2.name) //Garden
console.log(man1 === man2) //true -> 같은 데이터주소를 바라보고 있다.

man2에 man1이 하나 더 생성된 것이 아니라 해당 데이터의 메모리 주소를 전달받아서 결국 동일한 데이터를 바라보고 있는 것이다.

깊은복사(Depth Copy)

동일한 데이터를 바라보고 있는 것이 아닌, 똑같은 구조의 객체를 하나 더 생성하여 따로 사용하고자 할 때, 깊은 복사를 한다.

const man1 = { name:"GO"};

const man2 = Object.assign({ }, man1);

man1.name = "Garden";

console.log(man2.name) //Go -> 다른 메모리 주소의 데이터니까 man2의 값이 변하지 않는다. 
console.log(man1 === man2) //false -> 형태는 같지만, 각 자 다른 메모리 주소에 저장되어 있는 데이터이다. 

데이터 참조가 아닌 객체의 형태를 그대로 복사하게 함으로써, 한 객체가 변경되도 다른 객체의 데이터에는 영향이 없게 된다.

내가 이해한 얕은복사, 깊은복사
기본형 데이터는 새로운 데이터를 할당하면,
각 변수가 참조하는 데이터영역의 주소가 달라지기 때문에 a, b는 서로 다른 데이터가 된다.
하지만, 참조형 데이터(객체)는 객체의 프로퍼티를 바꾸는 경우, 참조하는 데이터 영역의 주소가 바뀌는 것이 아니기 때문에 여전히 같은 데이터로 간주된다. -> 참조형 데이터를 가변적이라고 부르는 이유, 객체의 프로퍼티 값을 바꿀 수 있기 때문에!

그런데 객체의 프로퍼티가 아니라, 객체 자체를 새로 할당하면, 객체의 형태가 동일하더라도 이제는 바라보는 데이터 영역의 주소가 다르기 때문에 두 객체는 다른 객체로 간주된다. (깊은복사)
-> 리액트에서 state가 변경될 때 리렌더링이 되는 것이 이 원리를 이용한것

깊은 복사를 하는 일반적인 방법

Object.assign()

Object.assign()은 Object 형태의 데이터를 쉽게 병합할 수 있게 해주는 함수이다.

const Obj = {a:1};

const newObj = Object.assign({}, Obj); // 빈 Object에 Obj를 병합하여 반환

console.log(newObj); // {a:1} 
console.log(Obj === newObj);// false -> 형태는 같으나 둘은 다른 데이터를 바라보고 있음
  • 빈 Object에다가 복사를 하려는 Object를 병합한다. 그러면 형태는 Obj이지만 실제로는 빈 Object와 Obj가 병합된 새로운 newObject가 반환된다.

Spread Operation

const Obj1 = {a:1, b:2};
const Obj2 = {c:3};
const Obj3 = {...Obj1, ...Obj2};

console.log(Obj3); // {a:1, b:2, c:3}
  • 깊은 복사하고 싶은 객체앞에...를 붙이면, 배열이나 Object 형태의 괄호를 이용하는 방식이다.
    객체 뿐 아니라, 배열에서도 동일하게 사용할 수 있다.

😎리액트에서 얕은복사,깊은복사 개념을 잘 알아야하는 이유!!

리액트에서 스프레드연산자(Spread Operator)를 이용해 깊은복사를 하면, 객체의 불변성을 유지할 수 있다.
근데 왜 불변성을 유지해야 할까...?
리액트는 state(데이터)가 변경되면 리렌더링을 시켜줘야 하므로, state가 이전과 다른 데이터가 되었음을 감지할 수 있어야 한다.

예제를 보자.

function App(){
  const [inputs, setInputs] = useState({
    username = "",
    email = "",
  });
  const { username, email } = inputs;
  
  const onChange = (e) => {
    const { name, value } = e.target; 
    
    setInputs({
      ...inputs, //📍 스프레드연산자
      [name]:value,
    });
  }; 
  • inputs라는 state에는 key(username, email)는 있지만, value(string)는 비어있다.
  • setInputs()에서 스프레드연산자를 사용해서 기존 inputs(state)를 똑같이 복사를 해서 새로운 state가 생겼다. 형태는 같겠지만, 데이터가 다름 → 스프레드연산자를 통한 깊은복사!!
  • 이제 사용자가 input창에 값을 입력하면, 그 값이 value에 할당된다.
  • 그렇게 기존의 inputs(value가 비어있던)와는 다른 새로운 inputs(입력받은 value가 할당된)가 만들어진다.

📍 Spread Operator는 변수영역에서 참조하는 데이터영역의 주소를 원본 데이터가 참조하는 데이터 영역의 주소와 다르게 한다. 이 코드에서도 스프레드연산자로 원본 데이터와는 다른 데이터가 생기게 했으므로 불변성을 확보했고(깊은복사)! 리액트는 state가 변경되었다는 것을 감지하고 리렌더링을 할 수 있었다.

📍 immer라는 라이브러리를 활용해서도 불변성을 확보할 수 있다!

😭주의할점!

깊은복사는 현재의 Depth만 복사하지, 그 이상 더 깊은 복사는 하지 않는다.

예를들면,
depth가 2인 객체를 스프레드연산자를 이용해서 Obj2에 할당했다.

const Obj1 = { a: 
              	{ b : 1 }
             };
const Obj2 = { ...obj1 };

console.log(Obj2) // { a: { b : 1 } };
console.log(Obj1 === Obj2) //false
  • Obj1와 Obj2의 형태는 동일하고, 각 자 다른 메모리 주소를 바라보고 있다.

하지만, 깊은 복사가 된 것은 제일 바깥의 depth뿐이다...!!

const Obj1 = {a :{b:1}};
const Obj2 = { ...Obj1};

console.log(Obj2) // { a: { b : 1 } };
console.log(Obj1 === Obj2) //false

console.log(Obj1.a === Obj2.a) // true...🙄😨😵
  • 두 번째 depth 이상의 요소들은 참조 값을 전달하는 즉, 얕은복사가 된 것이다.
    Object.assign()을 사용해도 동일하다.

그럼 어떻게 완벽하게 모든 depth를 깊은 복사할 수 있을까?

완벽하게 깊은 복사를 하는 방법

재귀적으로 깊은 복사 수행

깊은 복사를 하려는 형태에 맞게 재귀 함수를 만들어서 복사를 해야한다. 문제는...사용하는 Object의 Depth가 깊어질수록 Time Complexity(시간 복잡도)도 늘어나게 된다.

Lodash의 cloneDeep 함수 사용

자바스크립트 고차함수 집합 및 함수형 라이브러리이다.
Lodash의 cloneDeep 함수를 사용하면, 완벽하게 깊은 복사를 할 수 있다.

JSON.parse()와 JSON.stringify()함수 사용

JSON.stringify 함수를 이용해서, Object 전체를 문자열로 변환 후, 다시JSON.parse 함수를 이용해서 문자열을 Object 형태로 변환한다.
그러면 문자열로 변환하는 순간 참조 값이 끊기기 때문에 새로운 Object로 만들어 사용할 수 있다.
하지만 JSON 함수는 엄청나게 리소스를 잡아먹는 함수인 만큼, 성능이 좋지 않은 부분을 고려해야 한다.

[참고]
https://heo-dev-0229.tistory.com/47

profile
해결문제에 대해 즐겁게 대화 할 수 있는 프론트엔드 개발자

0개의 댓글