참조형 데이터를 어떻게 잘 복사 할 것인가?
객체가 담겨있는 변수를 다른 변수에 할당하면 데이터 복사가 아닌 참조가 일어나게 되어, 한 변수의 데이터를 변경하면 다른 변수의 데이터도 함께 변경이 된다.
const man1 = {name:"GO"};
const man2 = man1;
man1.name = "Garden";
console.log(man2.name) //Garden
console.log(man1 === man2) //true -> 같은 데이터주소를 바라보고 있다.
man2에 man1이 하나 더 생성된 것이 아니라 해당 데이터의 메모리 주소를 전달받아서 결국 동일한 데이터를 바라보고 있는 것이다.
동일한 데이터를 바라보고 있는 것이 아닌, 똑같은 구조의 객체를 하나 더 생성하여 따로 사용하고자 할 때, 깊은 복사를 한다.
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 형태의 데이터를 쉽게 병합할 수 있게 해주는 함수이다.
const Obj = {a:1};
const newObj = Object.assign({}, Obj); // 빈 Object에 Obj를 병합하여 반환
console.log(newObj); // {a:1}
console.log(Obj === newObj);// false -> 형태는 같으나 둘은 다른 데이터를 바라보고 있음
newObject
가 반환된다.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
하지만, 깊은 복사가 된 것은 제일 바깥의 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...🙄😨😵
Object.assign()
을 사용해도 동일하다.그럼 어떻게 완벽하게 모든 depth를 깊은 복사할 수 있을까?
깊은 복사를 하려는 형태에 맞게 재귀 함수를 만들어서 복사를 해야한다. 문제는...사용하는 Object의 Depth가 깊어질수록 Time Complexity(시간 복잡도)도 늘어나게 된다.
자바스크립트 고차함수 집합 및 함수형 라이브러리이다.
Lodash의 cloneDeep 함수를 사용하면, 완벽하게 깊은 복사를 할 수 있다.
JSON.stringify 함수를 이용해서, Object 전체를 문자열로 변환 후, 다시JSON.parse 함수를 이용해서 문자열을 Object 형태로 변환한다.
그러면 문자열로 변환하는 순간 참조 값이 끊기기 때문에 새로운 Object로 만들어 사용할 수 있다.
하지만 JSON 함수는 엄청나게 리소스를 잡아먹는 함수인 만큼, 성능이 좋지 않은 부분을 고려해야 한다.