안녕하세요! 웹파트 OB 김건휘입니다. 이번 시간에는 State 업데이트 큐, 객체 state 업데이트하기, 배열 state 업데이트하기에 대한 내용 중 중요한 내용과 딥다이브 한 내용을 공유하는 시간을 가지도록 하겠습니다.
📌State 업데이트 큐
React는 상태(State) 업데이트를 비동기적으로 처리하며, 이를 효율적으로 관리하기 위해 업데이트 큐(Update Queue)를 사용한다. 상태 변경 요청은 즉시 반영되지 않고, React의 내부에서 업데이트 큐에 추가된 후, 다음 렌더링 사이클에서 처리된다.
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>
</>
)
}
위의 코드는 지난 시간에도 다루었던, "스냅샷으로서 state"에서도 언급되었던 코드이다.
지난 시간에서 배웠듯이 위의 +3 버튼을 클릭하여도, 화면에는 1이 등장하는 것을 알 수 있다.
=> 각 렌더링의 state 값은 고정되어 있으므로, 첫 번째 렌더링의 이벤트 핸들러의 number 값은 setNumber(1)을 몇 번 호출하든 항상 0을 반환하고 있다.
하지만 여기에서 문제가 하나 있다. 위의 코드로 예시를 계속해서 들어보겠다.
버튼이 클릭되면, setNumber가 state 업데이트 큐에 들어가서 3번 렌더링이 된다고 생각해보자. 어짜피 1번을 렌더링 시키던 3번을 렌더링 시키던 동일한 화면(숫자 1)이 보여지게될텐데 얼마나 비효율적인 방식이 아닌가?
그래서 React에서는 이벤트 핸들러 내에서 발생한 여러 상태 업데이트를 한 번의 렌더링으로 묶어 처리하는 것을 배치 처리(Batching) 기능을 제공하고 있다.
React는 배치 처리를 통해 여러 상태 업데이트를 한 번의 렌더링으로 묶어 성능을 최적화한다.
🧐 state 처리는 어디서 진행하는걸까?
카운터라는 컴포넌트에서 state를 처리하는 것이 아니라, state를 관리하는 별도의 메모리 공간이 있고 그 메모리 공간 안에서 setNumber를 수집했다가 한꺼번에 일괄처리하고, 다음번 렌더링 때 그 때 반영되어있는 state 값을 업데이트 해준다!
📌React는 이벤트 핸들러가 실행을 마친 후 state 업데이트를 처리한다.
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
console.log('Before setCount:', count); // 기존 상태 출력
setCount(count + 1);
console.log('After setCount:', count); // 여전히 기존 상태 출력
}
return (
<>
<h1>{count}</h1>
<button onClick={handleClick}>Increment</button>
</>
);
}
버튼을 클릭하면 콘솔 출력은 다음과 같다.
Before setCount: 0
After setCount: 0
그러나, 화면에는 count가 1로 업데이트된 것을 확인 할 수 있다.
🧐왜 After setCount에서도 count가 0인가?
- setCount(count + 1)는 상태 업데이트 요청을 큐에 추가하지만, 이벤트 핸들러가 실행되는 동안 상태 값은 변경되지 않는다.
- 이벤트 핸들러가 실행을 종료한 후, React가 상태 업데이트를 처리하고 컴포넌트를 재렌더링하기 때문이다.
let x = 0;
function incrementX() {
console.log('Before:', x); // 0
x = x + 1;
console.log('After:', x); // 1
}
incrementX();
위의 코드는 반대로 동기적으로 실행하는 일반적인 변수의 업데이트를 진행하는 코드이다.
Before: 0
After: 1
state 변경과는 다르게 즉시 After:1이 찍히는 것을 확인할 수 있다.
📌객체 state 업데이트 하기, 배열 state 업데이트 하기
React에서는 상태(State)를 업데이트할 때 불변성(Immutability)을 유지해야한다는 이야기를 많이 들어봤을 것이다. 이는 상태를 직접 변경하는 대신, 상태의 복사본을 만들어 변경하고 이를 상태 업데이트 함수(setState 또는 useState)에 전달해야 한다는 것을 의미한다.
🧐왜 불변성을 유지해야 할까?
- React의 상태 변경 감지 메커니즘: React는 상태의 참조값(Reference)이 변경되었는지를 비교하여 상태 변경을 감지한다. 객체를 직접 변경하면 참조값이 동일하게 유지되므로 React가 변경을 감지하지 못할 수 있다.
- Virtual DOM 최적화: React는 상태가 변경된 경우 최소한의 변경만 실제 DOM에 반영하려고 한다. 불변성을 유지하면 변경된 부분만 정확히 감지할 수 있다.
import { useState } from 'react';
function MyComponent() {
const [user, setUser] = useState({
name: 'John Doe',
age: 25,
address: {
city: 'New York',
zip: '10001'
}
});
return <div>{user.name}</div>;
}
✅상태 업데이트
function updateUserName() {
setUser(prevUser => ({
...prevUser, // 기존 객체 복사
name: 'Jane Doe' // 변경할 값만 수정
}));
}
React의 상태를 업데이트할 때는 기존 객체를 복사한 뒤 수정된 값만 반영해야한다.
스프레드 연산자는 필수!로 사용하자.
✅깊은 중첩 객체 업데이트
function updateCity() {
setUser(prevUser => ({
...prevUser, // 기존 객체 복사
address: {
...prevUser.address, // 중첩된 객체 복사
city: 'Los Angeles' // 변경할 값
}
}));
}
address.city를 변경하려면, 각 레벨을 복사해서 변경해야한다.
✅Immer 라이브러리 사용
import produce from 'immer';
function updateCity() {
setUser(prevUser =>
produce(prevUser, draft => {
draft.address.city = 'Los Angeles'; // 직관적으로 업데이트
})
);
}
Immer 라이브러리를 사용하여 깊은 중첩 객체를 직관적이고 간단하게 업데이트할 수 있다.
사실 나도 이번에 처음 알게된 라이브러리이다. 생각해보니 Immer 라이브러리를 사용할정도로 깊은 중첩이 생기는 객체나 배열을 경험해볼정도로 큰 프로젝트를 진행하지 않았기 때문에 어찌보면 당연한 이야기인 것 같다.
실제로도 라이브러리를 무작정 도입하는 것이 아니라, 필요성을 느낄 때만 라이브러리를 도입해야하고 도입시 고려해야되는 사항들을 충분히 알아보고 도입해야한다. 개인적으로 라이브러리 도입을 최소화 하는것을 좋아한다.(성능 문제 야기 + 복잡성 증가)
✅배열을 복사하더라도 배열 내부 에 기존 항목을 직접 변경해서는 안됩니다
const [items, setItems] = useState([{ id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' }]);
function updateItem(id, newName) {
const updatedItems = [...items]; // 배열 복사
const item = updatedItems.find(item => item.id === id); // 특정 항목 찾기
item.name = newName; // ❌ 항목 직접 수정 (불변성 위반)
setItems(updatedItems);
}
여기서 item.name = newName으로 배열 내부 항목을 직접 수정하면, 내부 객체(item)는 직접 수정하게 되었으므로 불변성을 위반하고 있다. => React 상태 관리 원칙에 어긋나며, 유지보수성과 예측 가능성을 해친다.
✅불변성을 유지하며 상태를 업데이트하도록 수정
function updateItem(id, newName) {
const updatedItems = items.map(item =>
item.id === id
? { ...item, name: newName } // 새로운 객체 생성
: item // 기존 객체 유지
);
setItems(updatedItems);
}
map함수를 활용하여 배열의 참조값만 바꾸는 것이 아니라, 내부 객체의 불변성도 유지하게 하는 것이 중요하다. => map을 사용하여 특정 항목만 새 객체로 대체
🔥중첩 객체는 참조값으로 공유된다
const original = { id: 1, nested: { value: 42 } };
const shallowCopy = { ...original }; // 얕은 복사
console.log(original.nested === shallowCopy.nested); // true (참조값 공유)
- original.nested와 shallowCopy.nested는 같은 객체를 참조하고 있으므로, true를 반환한다.
- 이 때문에 복사본의 중첩 객체를 변경하면 원본 객체도 영향을 받게된다.
shallowCopy.nested.value = 99;
console.log(original.nested.value); // 99
console.log(shallowCopy.nested.value); // 99
중첩 객체는 얕은 복사 시 참조값으로 공유된다고 이해하면 편하다!
🧐어떻게 중첩 객체의 참조값 공유를 피할 수 있을까?
const original = { id: 1, nested: { value: 42 } };
// 얕은 복사 + 중첩 객체 복사
const deepCopy = {
...original,
nested: { ...original.nested },
};
deepCopy.nested.value = 99;
console.log(original.nested.value); // 42 (독립적)
console.log(deepCopy.nested.value); // 99
다음과 같이 스프레드 연산자를 사용하여 중첩 객체도 복사를 해준다!(깊은 복사)
🧐궁금한 점
왜 자바스크립트에서는 참조형 데이터 타입을 복사할 때 깊은 복사 대신 얕은 복사가 기본 동작인걸까?
애초부터 깊은 복사가 기본 동작이었다면, 이런 특정 상황에서 스프레드 연산자를 사용하지 않아도 됐을텐데... 또한, 보다 직관적인 코드를 짤 수 있었을텐데라는 궁금증이 생긴다.
'나야, 리액트 스터디' 카테고리의 다른 글
[week4] ... state 왜 파도파도 계속 나와요? (batch, immer 등 리액트 state 변경에 대해 알아보자) (9) | 2024.11.18 |
---|---|
[week4] 객체 state 업데이트 하기 (2) | 2024.11.18 |
[week 4] 객체 state 업데이트하기, 배열 state 업데이트하기 (3) | 2024.11.17 |
[week4] 객체 state 업데이트 하기, 배열 state 업데이트하기 (3) | 2024.11.17 |
[week4] 상호작용성 더하기 - State 업데이트 큐, 객체 state 업데이트하기, 배열 state 업데이트하기 (4) | 2024.11.17 |