불변성이란 단어는 React를 관통하는 핵심키워드라 볼 수 있습니다.
불변성은 어떤 값을 직접 변경하지 않고 새로운 값으로 다시 만들어내는 것입니다.
필요한 값을 변형해서 사용하려면 어떤 값의 사본을 만들어 사용해야 합니다.
JS에서 원시타입은 괜찮지만 객체 타입의 불변성을 지키는 것은 고려해야 하는 부분이 있습니다.
아래와 같은 코드가 있다고 가정할때 user와 copyUser 변수는 같은 참조 (Memory Address) 값을 가집니다.
(객체 타입은 참조 값을 주고받습니다.)
그렇기 떄문에 user가 가지고 있는 객체의 값이 변경된다면 copyUser도 같은 객체를 가지기 때문에 값이 공유
됩니다.
하지만 이런 작동 방식에는 값을 예측할 수 없거나 버그를 유발하기 떄문에 붋변성을 통해서 해결해야 합니다.
const user = { name: 'Choi', age: 25 }
const copyUser = user; // 배열의 복사가 아니라 같은 참조 값을 가짐
user.age += 1;
/*
user = { name: 'Choi', age: 26 }
copyUser = { name: 'Choi', age: 26 }
*/
user === copyUser // true
*call by value, call by refernece를 알고 있다면 이해하기 수월합니다.
새로운 객체 ...라는 spred operator를 사용해 복사를 합니다. otherUser는 새로운 객체를 할당 받았기 때문에 user 와
otherUser가 가리키는 객체의 참조 값은 다르게 됩니다.
따라서 user의 값 변경은 otherUser의 객체에 영향을 주지 않습니다. (불변성이 잘 지켜졌다고 할 수 있습니다.)
예시로 어떠한 인쇄물을 프린트하면 똑같은 것이 총 두 장 생기는데 한 장의 인쇄물에 낙서를 해도 다른 인쇄물에 영향을 주지 않는 것과 같습니다.
const user = { name: 'Choi', age: 25 }
const otherUser = { ...user };
user.name = 'Lee';
/*
user = { name: 'Lee', age: 25 }
otherUser = { name: 'Choi', age: 25 }
*/
user === otherUser // false 서로 다른 참조 값을 가지고 있음
하지만 객체의 깊이가 깊어지면 여전히 문제가 생깁니다 .spred operator는 얕은 복사(shallow copy)를 하게 되는데 완전히 다른 객체를 만들고 싶다면 복사(deep copy)를 해야 합니다.
const user = { name: 'Choi', age: 25, friends: ['Park', 'Kim']}
const otherUser = { ...user };
user.name = 'Lee';
user.friends.push('Kang');
/*
user = { name: 'Lee', age: 25, friends: ['Park', 'Kim', 'Kang'] }
otherUser = { name: 'Choi', age: 25, friends: ['Park', 'Kim', 'Kang'] }
*/
user === otherUser // false
user.friends === otherUser.friends // true
React 애기로 넘어와서 React Component 가 리렌더링 되는 조건은 Props, State의 값이 바뀌거나 부모 컴포넌트가 리렌더링될 때가 있습니다.
여기서 React Component의 Props나 State의 값이 바뀔 때 바뀐지 안 바뀐지 어떻게 확인할 까요 ?
이전 값과 현재 값을 전체적으로 비교한다고 가정하면
상대 객체가 다음과 같을 때 모든 요소를 하나하나 비교하는 방법은 비효율 적일 것입니다.
const useState = {
name: 'Kim',
age: '23',
loginToken: 'asfweasdd',
friends: ['Lee','Park'],
skills: {
frontEnd: ['React', 'Vue','Jquery','HTML','CSS'],
backEnd: ['Node.js'],
common: ['TS'],
}
}
따라서, React에서는 값을 비교할 때는 얕은 비교를 실행해 성능 최적화를 만들어내게 됩니다.
그렇기 떄문에 불변성을 지켜주는 일이 중요한 것입니다.
불변성을 지키지 않는 Counter 컴포넌트는
import React, { useState } from "react";
export default function Counter() {
const [state, setState] = useState({ count: 0 });
console.log("mounted or updated");
return (
<div>
<p>{state.count}</p>
<button
onClick={() => {
state.count += 1;
setState(state);
}}
>
+1
</button>
<button
onClick={() =>
setState((prevState) => ({ count: prevState.count - 1 }))
}
>
-1
</button>
</div>
);
}
React Component에서 setState와 같은 setter함수는 해당 컴포넌트의 리렌더링을 작동시킵니다.
여기서 +1 버튼의 onClick 이벤트를 발생하면 state.count에 변화가 일어나고 setState 함수가 실행됩니다.
이경우 Counter 함수가 리렌더링 되지 않습니다.
왜냐하면 state.counter +=1을 통해서 값을 변경시켜주었습니다. 하지만 state가 가르키고 있는 객체의 참조 값은
여전히 같습니다. 그리고 setState에 넘겨주는 state의 참조 값 또한 같습니다.
setState가 실행되면 가지고 있는 state와 전달 받은 state의 참조 값을 비교합니다. 참조 값이 같기 때문에 리렌더링이
실행되지 않습니다. 비교는 ObjectJs() 함수를 사용합니다.
불변성이 지켜지는 Counter Component
import React, { useState } from "react";
export default function Counter() {
const [state, setState] = useState({ count: 0 });
console.log("mounted or updated");
return (
<div>
<p>{state.count}</p>
<button
onClick={() => {
setState((prevState) => ({ count: prevState.count + 1 }));
}}
>
+1
</button>
<button
onClick={() =>
setState((prevState) => ({ count: prevState.count - 1 }))
}
>
-1
</button>
</div>
);
}
+1 버튼 클릭스 setState가 실행되며 콜백함수에 의해 새로운 객체가 리턴됩니다. 이렇게 리턴된 객체의 참조 값은
state가 가리키는 참조 값과 다르므로 Counter Component는 정상적으로 리렌더링 됩니다.
참고 자료
'프론트 엔드 > React' 카테고리의 다른 글
[React] 컴포넌트 성능 개선 React.memo() (0) | 2021.10.09 |
---|---|
[React] useCallback과 useMemo를 제대로 사용하는 법 (0) | 2021.10.09 |
[React] useEffect와 useLayoutEffect의 차이는 무엇일까? (0) | 2021.10.09 |