최근 PR에
useState
의setter
함수를 사용한 곳에서 아래와 같이 비슷한 코드 리뷰가 달렸다.
useEffect에 이어 useState도 이 기회에 제대로 정리하고자 한다.
컴포넌트는 상호 작용의 결과로 화면의 내용을 변경해야 하는 경우가 많이 생긴다.
(폼에 입력하면 입력 필드가 업데이트되어야 하고, 버튼을 클릭하면 어떤 제품이 담겨야 하고, ... 등등)
이러한 일은 현재 값을 "기억"해야 발생할 수 있는데, 기억하기 위한 컴포넌트의 메모리를 state
라고 한다.
- 렌더링 사이에 데이터를 "유지"하기 위한
state 변수
- 변수를 업데이트하고, React가 컴포넌트를 렌더링하도록 "유발"하는
state setter 함수
const [count, setCount] = useState(0);
setCount(5); // 직접 값 전달하기
setCount(prev => prev + 1); // 이전 상태 기반으로 갱신하기
state 변수
는 컴포넌트마다 지역적이다.
즉, 동일한 컴포넌트를 두 번 렌더링해도, 각 복사본은 고유한 state를 가진다.
setter 함수
는 상태를 업데이트하는 함수다.
비동기적으로 작동하기 때문에, 즉시 상태가 바뀌지 않고 변경된 값은 다음 렌더링에서 반영된다.
- 최적의 성능을 위해: 여러 상태 변경을 모아서 한번에 처리 (Batching)
- DOM 렌더링 최소화: 불필요한 렌더링을 줄이기 위해
const [count, setCount] = useState(0);
setCount(1);
console.log(count); // ❌ 여전히 0
페이지를 구성하는데는 수많은 state가 존재한다. 만약 하나하나의 state 변화에 리랜더링을 발생시킨다면 성능 저하가 발생할 것이다.
이를 해결하기 위해서, React는 setState가 연속 호출되면 배치(batch) 처리를 통해 한번에 랜더링하게 하는 것이다.
많은 setState를 연속으로 사용해도 배치 처리로 인해 랜더링은 한번만 되는 것이다.
상태 변경 직후의 값을 알기 위해선, 2가지 방법이 있다.
- useEffect의 의존성 배열 사용하기
- useRef로 최신 값 임시 저장하기
const [count, setCount] = useState(0);
useEffect(() => {
console.log('count가 변경된 후:', count);
}, [count]);
const countRef = useRef(0);
const handleClick = () => {
countRef.current += 1;
console.log('즉시 접근 가능:', countRef.current);
};
2개 이상의 state 변수를 항상 동시에 업데이트한다면, 다음과 같이 하나의 state로 묶는것이 좋다.
const [x, setX] = useState(0);
const [y, setY] = useState(0);
const [position, setPosition] = useState({ x: 0, y: 0 });
여러 state가 서로 모순되고 불일치될 수 있다면, 실수를 만들 수 있다.
const [isSending, setIsSending] = useState(false);
const [isSent, setIsSent] = useState(false);
const [status, setStatus] = useState('typing');
const isSending = status === 'sending';
const isSent = status === 'sent';
컴포넌트의 props나 기존 state로부터 계산 가능한 값이라면, 굳이 따로 state로 만들지 말고 직접 계산해서 써야 한다.
예를 들어 Props를 state에 미러링하는 경우, messageColor가 바뀌어도 color는 그대로다.
즉, props는 계속 변하는데 state는 한 번만 초기화된다.
-> props와 state가 불일치하는 버그 발생
props를 state로 사용하고 싶은 경우는 useEffect를 적절하게 사용해야 한다.
function Message({ messageColor }) {
const [color, setColor] = useState(messageColor);
// props를 직접 사용하기
function Message({ messageColor }) {
const color = messageColor;
여러 상태 변수 간 동일한 데이터가 중복될 경우 동기화를 유지하기 어렵다.
업데이트하기 쉽지 않아, 가능하면 state를 평탄한 방식으로 구성하는 것이 좋다.
const PhotoTalkGallery = () => {
const [selectedImages, setSelectedImages] = useState<string[]>([]);
// 바로 여기
const toggleAllImages = () => {
const allSelected = selectedImages.length === images.length;
return setSelectedImages(allSelected ? [] : [...images]);
};
return (
<div>
<input
onChange={toggleAllImages}
aria-label={`이미지 전체 선택`}
/>
</div>
);
};
export default PhotoTalkGallery;
setter 함수
에 return문을 사용하면,
기능상 문제는 없지만, 의미가 없고 오해를 유발한다
setter 함수는 단순히 상태 업데이트만 요청하고, 새로운 값이나 결과를 반환하지 않는다
return문을 작성하면, 무언가 반환할 것처럼 보여서 의도를 흐리게 하고 리팩토링 시 혼란을 줄 수 있다.
const toggleAllImages = () => {
const allSelected = images.every((image) => selectedImages.includes(image));
const newSelected = allSelected ? [] : [...images];
setSelectedImages(newSelected);
};
코드 리뷰가 아주 친절하네요