서론
- React를 사용하게 되면 state를 업데이트 해야하는 상황이 온다.
- state를 업데이트할 때 사용하는 useState의 setState에 대해 알아보자.
- setState를 사용할때 함수를 넣는 경우와 값을 직접 넣는 경우를 본 적이 있을 것이다.
- 각각의 경우가 어떠한 방식으로 실행되고 차이점은 무엇일까?
- setState가 무엇인지 그리고 어떠한 방식으로 실행되는지 알아보자
setState
- React를 쓰면서 가장 많이 쓰는 메소드이다.
- useState 훅에서 호출하는 메소드 이다.
- state의 업데이트를 위해 사용된다.
- state를 업데이트하면 리랜더링이 일어난다.
- 비동기적으로 동작한다.
사용법
1. updater 함수를 이용한 방식 (권장)
- 형태
- 이전 상태값을 안전하게 참조할 수 있어 연속적인 업데이트에 적합하다.
const [state, setState] = useState(0);
setState((prev) => prev + 1);
2. state 값을 이용한 방식
- 형태
- 단순한 상태 업데이트에 사용할 수 있지만, 연속적인 업데이트시 주의가 필요하다.
const [state, setState] = useState(0);
setState(state + 1);
차이점
- 가장 큰 차이점은 state변경 시 이전 값의 보장 유무이다.
- 이게 무슨 말이냐? 예제를 보자
- 위와 똑같이 state를 1씩 증가시키는 함수가 있다고 가정하자
- setState는 똑같이 1씩 증가시킨다. 그렇다면 setState를 두번 실행시키는 increase1, increase2에서 값이 동일하게 2씩 늘어날까?
const [state, setState] = useState(0);
const increase1 = () => {
setState((prev) => prev + 1);
setState((prev) => prev + 1);
};
// result +2 per click
const increase2 = () => {
setState(state + 1);
setState(state + 1);
};
// result +1 per click
결과
- increase1 = +2 증가하고, increase2 = +1 증가한다.
Why?
- 바로 setState는 비동기적으로 동작하기 때문이다.
- setState는 호출 즉시 동작하지 않는다.
- React에서는 효율적으로 처리하기 위해서 state를 변경하겠다고 대기열에 추가한 뒤, 여러 변경 사항들과 묶어서 한번에 업데이트 한다.
- 그렇기 때문에 increase2의 경우 setState를 2번 호출하지만 한꺼번에 묶어서 처리하기 때문에 +1씩 증가하는 것이다.
- 함수를 이용한 방식은 setState가 state를 변경하는 시점에 이전 값을 참조하기 때문에 +2씩 증가한다.
- 이러한 이유로 연속적인 상태 업데이트가 필요한 경우 updater 함수를 사용하는 것이 안전하다.
React 18의 Automatic Batching
- React 18부터는 자동 배칭(Automatic Batching)이 도입되었다.
- 단, createRoot를 사용할 때만 적용된다.
- 이전에는 React 이벤트 핸들러 내부에서만 배칭이 발생했다.
- 이제는 Promise, setTimeout, 이벤트 핸들러 등 모든 곳에서 배칭이 발생한다.
- 배칭은 여러 상태 업데이트를 하나의 리렌더링으로 묶어서 처리함으로써 성능을 최적화한다.
- 배칭을 피해야 하는 경우 flushSync를 사용할 수 있다.
- 단, flushSync는 성능에 부정적인 영향을 미칠 수 있으므로 꼭 필요한 경우에만 사용해야 한다.
- 예제를 보자
// React 18 이전
setTimeout(() => {
setCount((c) => c + 1); // 리렌더링 발생
setFlag((f) => !f); // 리렌더링 발생
}, 1000);
// React 18
setTimeout(() => {
setCount((c) => c + 1); // 배칭
setFlag((f) => !f); // 배칭
}, 1000); // 한번의 리렌더링
// 배칭 피하기 (꼭 필요한 경우에만 사용)
import { flushSync } from "react-dom";
const handleClick = () => {
flushSync(() => {
setCount((c) => c + 1); // 즉시 리렌더링
});
flushSync(() => {
setFlag((f) => !f); // 즉시 리렌더링
});
};
객체나 배열 상태 다루기
- state가 객체나 배열일 때는 더욱 주의해야 한다.
- React에서 불변성(Immutability)을 지키는 것은 매우 중요하다.
- 불변성을 지켜야 React가 상태 변화를 감지하고 효율적으로 리렌더링할 수 있다.
- 얕은 복사로 인해 의도치 않은 결과가 발생할 수 있다.
- 중첩된 객체나 배열을 다룰 때는 더욱 주의가 필요하다.
- 복잡한 객체의 경우 Immer 같은 도구를 사용하면 불변성을 쉽게 유지할 수 있다.
- 예제를 보자
const [user, setUser] = useState({
name: "John",
age: 20,
address: {
city: "Seoul",
street: "Gangnam",
},
hobbies: ["reading", "gaming"],
});
// ❌ 잘못된 방법
const updateAddress = () => {
user.address.city = "Busan"; // 직접 수정
setUser(user); // 참조가 같아서 리렌더링 안됨
};
const addHobby = () => {
user.hobbies.push("coding"); // 직접 수정
setUser(user); // 참조가 같아서 리렌더링 안됨
};
// ✅ 올바른 방법
const updateAddress = () => {
setUser((prev) => ({
...prev,
address: {
...prev.address,
city: "Busan",
},
}));
};
const addHobby = () => {
setUser((prev) => ({
...prev,
hobbies: [...prev.hobbies, "coding"],
}));
};
// ✅ Immer를 사용한 방법
import produce from "immer";
const updateAddressWithImmer = () => {
setUser(
produce((draft) => {
draft.address.city = "Busan";
})
);
};
여러 상태 동시에 업데이트하기
- 여러 상태를 한번에 업데이트 해야할 때가 있다.
- 이럴 때는 useReducer를 고려해보는 것이 좋다.
- useReducer는 다음과 같은 경우에 특히 유용하다:
- 여러 상태가 서로 연관되어 있을 때
- 다음 상태가 이전 상태에 의존적일 때
- 상태 업데이트 로직이 복잡할 때
- 예제를 보자
// 여러 상태를 개별적으로 업데이트
const updateUserInfo = () => {
setName("John");
setAge(25);
setEmail("john@example.com");
};
// useReducer를 사용한 방법
const reducer = (state, action) => {
switch (action.type) {
case "UPDATE_USER":
return {
...state,
name: action.payload.name,
age: action.payload.age,
email: action.payload.email,
};
default:
return state;
}
};
const updateUserInfo = () => {
dispatch({
type: "UPDATE_USER",
payload: {
name: "John",
age: 25,
email: "john@example.com",
},
});
};
성능 최적화
- state 업데이트는 리렌더링을 발생시키므로 성능에 영향을 준다.
- 성능 최적화는 실제 성능 측정 후에 진행하는 것이 좋다.
- React DevTools의 Profiler를 사용해 성능 병목지점을 찾아보자.
- useCallback과 useMemo는 신중하게 사용해야 한다:
- 이러한 최적화가 항상 성능 향상을 보장하지는 않는다.
- 때로는 불필요한 복잡성만 추가할 수 있다.
- 실제 성능 측정 후 필요한 경우에만 적용하자.
// 🤔 최적화가 필요한지 고민이 필요한 경우
const Component = ({ items }) => {
const [filter, setFilter] = useState("");
// 매 렌더링마다 새로운 함수 생성
const handleFilter = (value) => {
setFilter(value);
};
// 매 렌더링마다 필터링 로직이 재실행됨
const filteredItems = items.filter((item) => item.name.includes(filter));
return (
<div>
<input onChange={(e) => handleFilter(e.target.value)} />
<ItemList items={filteredItems} />
</div>
);
};
// ✅ 성능 측정 후 최적화가 필요한 경우
const Component = ({ items }) => {
const [filter, setFilter] = useState("");
// 성능 측정 결과 최적화가 필요한 경우에만 적용
const handleFilter = useCallback((value) => {
setFilter(value);
}, []); // 의존성 없음
// 필터링 비용이 큰 경우에만 메모이제이션 적용
const filteredItems = useMemo(
() => items.filter((item) => item.name.includes(filter)),
[items, filter] // items나 filter가 변경될 때만 재계산
);
return (
<div>
<input onChange={(e) => handleFilter(e.target.value)} />
<ItemList items={filteredItems} />
</div>
);
};
마무리
- setState는 React에서 상태를 관리하는 핵심 메커니즘이다.
- 상태 업데이트시 updater 함수 사용을 권장한다.
- 불변성을 지키는 것이 매우 중요하다.
- 성능 최적화는 실제 측정 후 적용하자.
- React 18의 Automatic Batching을 활용하면 더 나은 성능을 얻을 수 있다.
참조
피드백은 언제나 환영입니다.