검색창에 타이핑할 때마다 화면이 버벅인 이유 — 무거운 목록과 같은 우선순위
문제 발생
검색어를 입력하면 5천 건짜리 목록이 필터링되는 화면에서, 글자를 칠 때마다 입력이 한 박자씩 늦게 반영됐습니다. 목록 렌더링이 느린 것은 알겠는데 입력창까지 같이 느려지는 것이 문제였습니다.
const [query, setQuery] = useState("");
const results = useMemo(() => filterHuge(items, query), [items, query]);
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<HugeList items={results} />
</>
);원인 분석
React는 하나의 상태 갱신을 하나의 렌더로 처리합니다. setQuery 한 번에 입력창과 목록이 함께 다시 그려지고, 그 렌더가 끝나야 브라우저가 화면을 갱신합니다. 목록 렌더가 200ms 걸리면 입력 반영도 200ms 뒤입니다.
문제의 본질은 성능이 아니라 우선순위입니다. 입력창은 즉시 반응해야 하고 목록은 조금 늦어도 됩니다. 그런데 코드에는 그 차이가 표현돼 있지 않습니다.
디바운스로 미루는 방식이 흔한 우회책이지만, 지연 시간을 사람이 정해야 하고(짧으면 여전히 버벅이고 길면 반응이 굼뜹니다), 빠른 기기와 느린 기기에 같은 값이 적용됩니다.
React는 이 구분을 API로 제공합니다. useDeferredValue 는 값의 "급하지 않은 버전"을 만들고, useTransition 은 특정 갱신을 급하지 않다고 표시합니다. 급한 렌더가 들어오면 진행 중이던 급하지 않은 렌더는 중단되고 다시 시작됩니다.
해결 방안
- 목록에는 미뤄진 값을 넘깁니다. 입력창은
query로 즉시 갱신되고, 목록은deferredQuery를 따라 늦게 따라옵니다.
const [query, setQuery] = useState("");
const deferredQuery = useDeferredValue(query);
const results = useMemo(() => filterHuge(items, deferredQuery), [items, deferredQuery]);
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<HugeList items={results} />
</>
);- 미뤄지는 동안임을 표시합니다. 값이 아직 따라오지 않았다는 것을 비교로 알 수 있습니다.
const isStale = query !== deferredQuery;
<div style={{ opacity: isStale ? 0.6 : 1 }}><HugeList items={results} /></div>- 무거운 자식은
memo로 감쌉니다. 이게 빠지면 미뤄진 값이 같아도 부모가 리렌더될 때 자식이 매번 다시 그려져 효과가 사라집니다.useDeferredValue는memo와 함께 쓸 때 제 값을 합니다. - 이벤트에서 시작하는 갱신이라면
useTransition을 씁니다. 탭 전환처럼 "누른 즉시 반응은 필요하지만 내용 교체는 조금 늦어도 되는" 경우에 맞습니다.
const [isPending, startTransition] = useTransition();
function selectTab(next) {
startTransition(() => setTab(next)); // 이 갱신은 중단 가능
}- 디바운스를 대체하는 것이지 최적화를 대체하지는 않습니다. 렌더 자체가 무거우면 여전히 무겁습니다 — 가상 스크롤이나 결과 개수 제한이 먼저인 경우도 많습니다. 우선순위 분리는 "그럼에도 입력은 즉시 반응하게" 만드는 도구입니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.