React 리렌더 연쇄: 한 번의 클릭이 트리 전체를 리렌더할 때
무슨 일이 일어나는가
React에서 상태 업데이트는 해당 컴포넌트와, 기본적으로 모든 자식을 리렌더합니다. 상태가 트리 상단(컨텍스트, 페이지 수준 스토어)에 있으면 한 번의 키 입력이나 클릭이 수백 개의 컴포넌트를 리렌더할 수 있습니다 — 대부분은 이전과 정확히 같은 것을 표시합니다.
렌더 한 번은 저렴하지만, 한 핸들러에서 수천 번은 아닙니다. 그 작업은 INP의 processing time 단계에 들어갑니다.
알아보는 법
- React DevTools Profiler가 넓은 플레임 그래프를 보여줌: props가 눈에 띄게 바뀌지 않는데도 컴포넌트가 리렌더됨.
- 제어 입력에 타이핑할 때 끊김: 키 입력마다 큰 리스트나 테이블이 리렌더됨.
- INP에서 processing time이 지배적이고, DevTools Performance의 스택이 React 내부로 가득 참.
해결 방법
- 상태를 아래로 옮기세요: 페이지 수준 컴포넌트나 컨텍스트가 아니라, 그것이 필요한 가장 작은 컴포넌트에 두세요.
- 비싼 하위 트리를 React.memo로 감싸고 전달하는 props를 useCallback/useMemo로 안정화하세요 — 렌더마다 새 콜백을 만들면 memo는 소용없습니다.
- 긴급하지 않은 업데이트(입력 중 리스트 필터링)를 useTransition으로 표시해 긴급 렌더(입력 자체)가 먼저 그려지게 하세요.
문제
const Page = () => {
const [query, setQuery] = useState("")
// every keystroke re-renders Page → Header, Sidebar, BigTable, Footer
return <>
<SearchInput value={query} onChange={setQuery} />
<BigTable rows={filterRows(rows, query)} />
</>
}
해결
const BigTable = React.memo(({ rows }) => /* ... */)
const Page = () => {
const [query, setQuery] = useState("")
const [isPending, startTransition] = useTransition()
const rows = useMemo(() => filterRows(allRows, query), [query])
const onChange = (q) =>
startTransition(() => setQuery(q)) // input paints first, table follows
return <>
<SearchInput onChange={onChange} />
<BigTable rows={rows} />
</>
}
사이트 무료로 스캔하기 →