일반적으로 데이터 변경에는 두 가지 방법이 있다.
- 데이터의 값을 직접 변경하는 것. (BAD)
- 변경 값을 가진 새로운 사본으로 데이터를 교체하는 것. (GOOD)
var player = {score: 1, name: 'Jeff'};
player.score = 2;
// 이제 player는 {score: 2, name: 'Jeff'}입니다.
var player = {score: 1, name: 'Jeff'};
var newPlayer = {...player, score: 2};
// 이제 player는 변하지 않았지만 newPlayer는 {score: 2, name: 'Jeff'}입니다.
최종 결과는 동일하지만, 직접적인 객체 변경이나 기본 데이터의 변경을 하지 않는다면 (불변성을 유지한다면) 몇가지 이점을 얻을 수 있다!
- 복잡한 특징을 단순하게 만듦
- 변화를 감지함
- 객체는 참조형 데이터이므로 불변성을 띄지 않는다. 그러므로 객체가 직접적으로 수정될 수 있다.
- 따라서 복제가 가능한 객체에서 변화를 감지하는 것은 어렵다. (참조하고 있는 주소값은 변화X)
- 방법 2의 불변 객체 사용 시 참조하고 있는 불변 객체가 이전 객체와 다르다면 객체는 변한 것이므로 변화를 감지하기 쉽다.
- React에서 리렌더링 하는 시기를 결정함
- 불변성의 가장 큰 장점은 React에서 순수 컴포넌트를 만드는 데 도움을 준다는 것이다.
- 변하지 않는 데이터는 변경이 이루어졌는지 쉽게 판단할 수 있으며 이를 바탕으로 컴포넌트가 리렌더링할지를 결정할 수 있다.
👉 불변성 : 한 번 생성된 원본의 데이터는 변경시키지 않고, 데이터 변경 필요 시 새로운 사본을 만드는 디자인 패턴
const arr = [1,2,3,4,5];
arr.push(6);
console.log(arr); // [1,2,3,4,5,6]
// push 메소드는 원본 배열인 arr의 값 자체를 변경시킨다.
const arr = [1,2,3,4,5];
arr.map(el => el * 2); // [2,4,6,8,10] 이 리턴됨.
console.log(arr); // [1,2,3,4,5]
// map 메소드는 원본 배열인 arr의 값을 변경시키지 않는다.
[참고]
배열 메서드가 mutable 한지, immutable 한지 확인할 수 있는 사이트
https://doesitmutate.xyz/
// mutable
const [state, setState] = useState({a: 1, b: 2});
// 초기 state {a: 1, b: 2} 는 참조값으로 0x1234 와 같은 주소값을 가리킴.
state.a = 10; // state 값을 직접 변경해도 state는 여전히 0x1234
setState(state); // 기존 state 와 변경요청 온 state 모두 0x1234 이므로 리렌더링이 발생하지 않음
// mutable
function App() {
// obj 식별자는 MHx0001 주소가 할당되었다고 가정.
const [obj, setObj] = useState({
name: "hyewon",
age: 21
}); // obj: MHx0001 => {name: "wonjang", age: 21}
return (
<div>
<div>{obj.name}</div>
<button onClick={()=> {
obj.name = 'hyetwo'; // side Effect 발생 가능성
console.log(obj);
setObj(obj); // obj는 여전히 MHx0001 의 주소를 가리킴.
}}></button>
</div>
)
}
obj.name = "kim";
) 이 객체와 연결되어 있는 다른 데이터가 변경됨에 따른 예상치 못한 부작용(bug)을 일으킬 수 있다.얕은 복사를 손쉽게 구현할 수 있다.
const [obj, setObj] = useState({
name: "James",
age: 30
})
const increaseAge = () => {
setObj({...obj, age: obj.age + 1});
// setObj({name: "James", age: 30, age: obj.age + 1});
// object의 특성상 key는 중복이 안되며 중복된 경우에는 뒤에 있는 것으로 앞에 있는 것을 덮어 씌운다.
}
그러나 Depth가 깊은 객체라면 다소 가독성이 떨어지고 복잡해보일 수 있다.
const [obj, setObj] = useState({
name: "James",
contact: {
email: "abc@gmail.com",
phone: "010-1234-5678"
}
})
const changeEmailTo = (newEmail) => {
setObj({ ...obj, contact: {...obj.contact, email: newEmail} })
}
이때 사용할 수 있는 것이 immer 라이브러리 이다.
depth가 깊은 중첩 객체인 경우, 불변성 여부를 점검하기 어렵기 때문에
mutable한 문법으로도 불변성을 지킬 수 있는 immer 라이브러리 사용을 권장한다. npm i immer
👉 immer 라이브러리 공식문서 https://immerjs.github.io/immer/
import { produce } from "immer";
const [obj, setObj] = useState({
name: "James",
contact: {
email: "abc@gmail.com",
phone: "010-1234-5678"
}
})
const changeEmailTo = (newEmail) => {
setObj(
produce(draftObj => {
draftObj.contact.email = newEmail;
})
)
}
// 깊은 복사X 수정이 발생한 대상만 복사, 나머지는 유지 -> 메모리 절약 + 불변성 유지
💡 순수 함수란?
- 하나 이상의 인자를 받고, 인자를 변경하지 않고 참조하여 새로운 값을 반환하는 함수
- 즉, 같은 input이 전달되면 항상 동일한 output을 반환하는 함수
- ex) 순수 함수
// 매개변수를 복사한 값을 변경하는 순수함수 const addSixPure = (arr) => { // 펼침 연산자로 새로운 배열에 6 추가 newArr = [...arr, 6]; return newArr; };
- ex) 비순수 함수
const num_arr = [1, 2, 3, 4, 5]; // 매개변수의 값을 직접 변경하는 비순수함수 const addSixImpure = (arr) => { // 매개변수에 직접 6 추가 arr.push(6); return arr; };