고양이와 코딩
[React] - state의 업데이트 본문
https://react-ko.dev/learn/queueing-a-series-of-state-updates
-state 변수를 설정하면 다음 렌더링이 큐(대기열, queue)에 들어갑니다. 그러나 경우에 따라 다음 렌더링을 큐에 넣기 전에,
값에 대해 여러 작업을 수행하고 싶을 때도 있습니다.
state 업데이트 일괄처리
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);
}}>+3</button>
</>
)
}
setNumber 내에서 number에 1을 더하는 코드를 세 번 반복했으므로 +3 버튼을 누르면 왠지 세 번 증가할 것 같습니다
하지만 그렇지 않습니다!
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 1); // 0 + 1, 다음 number는 1 !
setNumber(number + 1); // 0 + 1, 다음 number는 1 !
setNumber(number + 1); // 0 + 1, 다음 number는 1 !
}}>+3</button>
</>
)
}
각 렌더링의 state 값은 고정되어 있으므로, 첫 번째 렌더링의 number 값은 setNumber(1)을 몇 번 호출하든 항상 0입니다.
→ React는 state 업데이트를 하기 전에 이벤트 핸들러의 모든 코드가 실행될 때 까지 기다립니다.
리렌더링은 모든 setNumber()의 호출이 완료된 이후에만 일어납니다
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(n => n + 1);
setNumber(n => n + 1);
setNumber(n => n + 1);
}}>+3</button>
</>
)
}
코드를 위와 같이 큐의 이전 state를 기반으로 다음 state를 계산하는 함수를 전달하면,
원하는대로 +3 버튼을 눌렀을때 이벤트가 실행되며 숫자가 3씩 증가합니다.
여기에서 왜 큐를 애기하는지 의문이 들었는데요,
먼저, 큐는 비동기 작업을 순서대로 처리하기 위해 사용되는 자료구조입니다.
setNumber(n => n + 1) : n => n + 1 함수를 큐에 추가
setNumber(n => n + 1) : n => n + 1 함수를 큐에 추가
setNumber(n => n + 1) : n => n + 1 함수를 큐에 추가
따라서 React 컴포넌트가 렌더링 될 때마다 setNumber 함수가 호출되고, 이 함수는 상태 변수의 값을 변경하는 작업을 큐에 추가합니다.
이렇게 큐에 추가된 작업들이 React 엔진에 의해 순차적으로 처리되어 상태 변경이 반영 되는것이라고 합니다!
state를 교체한 후 업데이트하면 ?
<button onClick={() => {
setNumber(number + 5);
setNumber(n => n + 1);
}}>
0 + 5 , 다음 number는 5 가 되고, 아직 한 번의 이벤트가 끝난게 아니므로 0 => 0 + 1이 됩니다. ... 라고 생각했습니다
저 버튼은 한 번 눌렀을때 6이 됩니다
- setNumber(number + 5): number는 0 이므로 setNumber(0 + 5) 입니다.
>> 리액트는 큐에 "5로 바꾸기" 를 추가합니다 - setNumber(n => n + 1): n => n + 1 은 상태 업데이트 함수(업데이터 함수) 입니다.
>> 리액트는 해당 함수를 큐에 추가합니다
여전히 헷갈립니다.
<button onClick={() => {
setNumber(number + 5);
setNumber(n => n + 1);
}}>
이 버튼은 6으로 증가하고
<button onClick={() => {
setNumber(number + 5);
setNumber(number + 1);
}}>
이 버튼은 1이 증가하는 이유가 뭘까요?!!
첫 번째 버튼의 경우
- setNumber(number + 5)는 현재 number 값에 5를 더한 값으로 상태를 업데이트합니다.
- setNumber(n => n + 1)은 이전 상태를 캡쳐(스냅샷)하여 1을 더한 값을 상태로 업데이트합니다. 즉, 위에서 5를 더한 값에서 다시 1을 더합니다.
따라서 두 번째 setNumber 호출은 이미 첫 번째 setNumber 호출에 의해 예약된 상태 업데이트를 덮어쓰게 됩니다.
최종적으로는 6이 됩니다.
두 번째 버튼의 경우
- setNumber(number + 5)는 현재 number 값에 5를 더한 값을 상태로 업데이트합니다.
- setNumber(number + 1)은 현재 number 값에 1을 더한 값을 상태로 업데이트합니다. 여기서 이전 상태를 캡쳐하지 않고 현재 값에 1을 더하기 때문에 이전 상태를 무시합니다.
따라서 두 번째 setNumber 호출은 첫 번째 setNumber 호출에 의해 예약된 상태 업데이트를 무시하고, 현재 number 값에 1을 더한 값으로 상태를 업데이트합니다.
최종적으로는 1이 됩니다.
React 에서 불변성을 지켜야 하는 이유
state의 변이(mutation)에 대해 공부하면서 지금까지 작업해온 숫자, 문자열, 불리언 값들은 `불변` 합니다!
(변이할 수 없거나 읽기 전용입니다)
따라서 다시 렌더링을 촉발하여 값을 변경해야 합니다.
const [num, setNum] = useState(0);
setNum(5);
num이 0에서 5로 변경되었지만, 숫자 0 자체는 변경되지 않았습니다
→ 자바스크립트에서는 number, string, boolea, undefined, null, symbol 과 같은 `원시 타입` 값을 변경할 수 없습니다.
(원시 타입이 아닌것은 전부 참조형입니다. ex. 객체, 배열 함수 ....)
const [position, setPosition] = useState({ x: 0, y: 0 });
하지만 이와 같은 객체 state는 기술적으로 변경할 수 있습니다. 하지만 불변하는 것처럼 취급해야 합니다
→ 객체를 직접 변이하는것이 아니라 교체해야 합니다!
리액트에서 주구장창 전개연산자를 쓰는 이유가 무엇일까요?!
불변성(Immutability) : 데이터의 변경이나 수정을 허용하지 않는 개념.
메모리에서 변수나 객체는 특정한 메모리 주소에 할당되는데요, 이 때 불변성을 가지는 데이터는 한 번 할당된 메모리 공간을 수정하지 않고 그대로 유지합니다. 즉, 변수나 객체의 값이 변경되더라도 해당 메모리 주소에 할당된 값 자체가 변하는 것이 아니라, 변경된 값이 새로운 메모리 공간에 할당되어 저장되는 것입니다.
const originalObject = {
name: 'jin',
age: 25,
hobbies: { first: 'eating', second: 'coding' }
};
const copiedObject = { ...originalObject };
copiedObject.age = 30;
console.log(originalObject); // { name: 'jin', age: 25, hobbies: { first: 'eating', second: 'coding' } }
console.log(copiedObject); // { name: 'jin', age: 30, hobbies: { first: 'eating', second: 'coding' } }
originalObject.hobbies.first = 'sleeping';
console.log(originalObject); // { name: 'jin', age: 25, hobbies: { first: 'sleeping', second: 'coding' } }
console.log(copiedObject); // { name: 'jin', age: 30, hobbies: { first: 'sleeping', second: 'coding' } }
스프레드 연산으로 originalObject를 복사한 뒤 age값을 1로 변경해 보았습니다.
그 후 originalObject를 확인해 보면, 여전히 age가 25임을 확인할 수 있습니다.
이는 깊은 복사가 이루어졌다는 말입니다 (주소 메모리가 서로 다른곳을 가리키고 있기 때문에 변화 x)
그리고 originalObject 와 copiedObject가 같은지를 검사했을때, false가 나오는 것을 확인할 수 있습니다.
하지만 중첩된 객체 hobbies를 변경하면 어떤 결과가 나올까요?
복사본의 hobbies.first를 sleeping으로 변경했는데, originalObject의 first도 sleeping으로 변경되었습니다.
hobbies 객체가 여전히 참조로 유지되기 때문입니다. (같은 주소를 바라본다! === 얕은 복사를 하고있다)
스프레드 연산자를 사용해서 복사를 했을 때, 1depth의 깊이만큼만 깊은 복사를 하고, 그 다음 단계부터는 얕은 복사를 하는 것을 확인할 수 있습니다.
결론적으로 리액트에서 스프레드 연산자를 많이 사용하는 이유 중 하나는,
상태를 업데이트 할 때 새로운 상태를 생성하고 불변성을 유지하기 위해서입니다!
기존 상태를 직접 수정하는것이 아니라, 새로운 상태를 생성하여(주소값이 다른) 전달합니다.
하지만, 스프레드 연산자도 1단계까지만 깊은 복사를 하기 때문에, 중첩된 배열, 객체나 함수를 제대로 깊은 복사를 해서 사용하려면
- lodash의 clone deep과 같은 라이브러리 사용
- JSON.stringify()를 이용해서 객체 => 문자열 , 문자열 => 객체 사용하기
등의 방법이 있습니다.
객체는 실제로 중첩되지 않습니다
중첩 객체를 다루는 방법에 대해서 학습하다가, 객체는 실제로 중첩되지 않는다 (???) 는 말을 봤습니다 ,,,,,
let obj = {
name: 'Niki de Saint Phalle',
artwork: {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
}
};
이 객체는 명확히 중첩되어 있지만, 코드가 실행 될 때 객체의 동작 방식을 고려해보면
let obj1 = {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
};
let obj2 = {
name: 'Niki de Saint Phalle',
artwork: obj1
};
이렇게 서로 다른 두 객체를 보고 있는 것입니다.
심지어 obj1이 obj2의 내부에 있지도 않다고 합니다, obj3도 obj1을 가리킬 수 있습니다.
let obj1 = {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
};
let obj2 = {
name: 'Niki de Saint Phalle',
artwork: obj1
};
let obj3 = {
name: 'Copycat',
artwork: obj1
};
이러한 경우에 obj3.artwork.city를 변경하면, obj2.artwork.city와 obj1.city 전부가 변경됩니다
이는 obj3.artwork, abj2.artwork, obj1 이 동일한 객체이기 때문입니다!
`obj1, obj2, obj3` 은 모두 같은 객체를 가리키며, 메모리에서 같은 주소를 참조하고 있습니다.
→ "중첩된" 객체라고 생각하면 이해하기 어렵습니다... (╥_╥`) , 객체간의 상호 참조라고 이해하는 것이 보다 정확합니다!
'react' 카테고리의 다른 글
[React] - useState와 useRef (0) | 2024.03.06 |
---|---|
[React] - 컴포넌트 순수성 유지 (0) | 2024.02.28 |
[React] - TypeScript : Record 유틸리티 타입 (0) | 2024.02.25 |
코드 스니펫을 사용해보자 ! (snippet-generator) (2) | 2024.01.31 |
[React] - 서버 사이드 렌더링에서의 데이터 로딩 (Redux- thunk) (1) | 2024.01.02 |