blog

React 함수형 상태 업데이트와 Dispatch<SetStateAction<T>> 타입

Front-end / React#React#useState#TypeScript#stale closure

1. setCount(count + 1) vs setCount(prev => prev + 1)

새 상태가 이전 상태로부터 계산된다면 함수형 업데이트를 쓰는 편이 안전하다.

const [isSidebarOpen, setIsSidebarOpen] = useState(false);

// 값으로 업데이트
const toggleDirectly = () => setIsSidebarOpen(!isSidebarOpen);

// 함수형 업데이트
const toggle = () => setIsSidebarOpen((prev) => !prev);

언제 차이가 나는가

버튼을 두 번 클릭하는 것처럼 업데이트가 서로 다른 렌더링에서 일어나는 경우에는 둘 다 정상 동작한다. 클릭마다 다시 렌더링되고, 핸들러도 최신 isSidebarOpen을 가진 채로 새로 만들어지기 때문이다.

차이가 나는 것은 한 렌더링 안에서 같은 값을 여러 번 업데이트하거나, 오래된 클로저가 상태를 읽는 경우다.

// 한 이벤트 안에서 두 번 호출
const handleClick = () => {
  setCount(count + 1);
  setCount(count + 1); // 둘 다 같은 count를 읽어서 결과는 +1
};

const handleClickFn = () => {
  setCount((prev) => prev + 1);
  setCount((prev) => prev + 1); // 대기 중인 결과에 이어서 적용되어 +2
};
// 비동기 작업 후의 업데이트
const handleSave = async () => {
  await save();
  setCount(count + 1); // 클릭 시점의 count 사용 (그 사이 바뀐 값 무시)
  setCount((prev) => prev + 1); // 적용 시점의 최신 값 사용
};

setInterval, useEffect 안의 콜백, 메모이제이션된 핸들러(useCallback 의존성 누락)처럼 함수가 예전 렌더링의 값을 기억하는 경우에도 함수형 업데이트가 이 문제를 피하게 해 준다. 덤으로 상태를 의존성 배열에서 뺄 수 있어 effect나 콜백이 덜 다시 만들어진다.

2. Dispatch<SetStateAction> 타입

useState(false)의 반환 타입은 [boolean, Dispatch<SetStateAction<boolean>>]이다. 보통은 추론에 맡기면 되고, setter를 props로 넘길 때 이 타입을 직접 쓰게 된다.

// @types/react 정의
type SetStateAction<S> = S | ((prevState: S) => S);
type Dispatch<A> = (value: A) => void;
  • SetStateAction<boolean>: 새 값(boolean) 또는 이전 값을 받아 새 값을 반환하는 함수
  • Dispatch<A>: A를 인자로 받고 아무것도 반환하지 않는 함수

즉, Dispatch<SetStateAction<boolean>>은 "true/false를 받거나 (prev) => !prev 같은 함수를 받는 setter"라는 뜻이다.

import type { Dispatch, SetStateAction } from 'react';

interface SidebarProps {
  isOpen: boolean;
  setIsOpen: Dispatch<SetStateAction<boolean>>;
}

다만 setter를 그대로 자식에게 넘기면 자식이 상태를 마음대로 바꿀 수 있다. onToggle: () => void처럼 의도가 드러나는 콜백을 넘기는 편이 컴포넌트 경계가 더 분명해진다.