사이드 프로젝트로 블로그를 만들던중에 태그 자동완성 기능이 있었는데, 태그 자동완성을 붙이다보니 타이핑할 때마다 쿼리가 너무 많이 날라가 문제가 생겼다.
컴포넌트인 TagInput은 컨트롤드 인풋이고, 입력이 바뀔 때마다 useQuery가 실행된다.
// 태그입력 자동완성 쿼리
const { data: tagData, isLoading } = useQuery<
QueryResponse<{ body: string }[]>
>({
queryKey: ["tag", input],
queryFn: async () => {
const q = input.trim();
if (q.length < 2) return { data: [] };
const res = await fetch(
`/api/tag/search?q=${encodeURIComponent(q)}`
);
return res.json();
},
enabled: input.trim().length >= 2,
});
문제점
//useDeferredValue 훅 사용
const deferredInput = useDeferredValue(input);
const { data: tagData, isLoading } = useQuery<
QueryResponse<{ body: string }[]>
>({
queryKey: ["tag", deferredInput],
queryFn: async () => {
const q = deferredInput.trim();
if (q.length < 2) return { data: [] };
const res = await fetch(
`/api/tag/search?q=${encodeURIComponent(q)}`
);
return res.json();
},
enabled: deferredInput.trim().length >= 2,
});
달라진 점
결과적으로 “입력 UX”와 “네트워크 요청”이 분리 되었다.
위에 코드처럼 useDeferredValue를 사용하여 해결하였는데, 어떻게 해결이 된것일까?
useDeferredValue는 값을 즉시 바꾸는 훅이 아니고, 이미 변경된 값을 조금 늦게 소비할 수 있게 해주는 훅에 가깝다.
사용할땐 값을 래핑해서 사용한다.
const deferredInput = useDeferredValue(input);
이 코드는 input을 지연시키는 게 아니라, input의 느린 버전(slower copy)을 하나 더 만들어 준다.
그래서 동작 방식은 이렇다. input은 타이핑과 동시에 바로 업데이트되고 deferredInput은 React가 판단한 여유 있는 시점에 따라온다. 이 덕분에 입력 UX는 그대로 유지하면서 무거운 렌더링이나 네트워크 요청만 늦출 수 있다.
자동완성 예제에서는 input은 즉시 화면에 반영하고, 쿼리는 deferredInput 기준으로만 실행하도록 분리하면서 타이핑 속도와 API 호출을 같은 타이밍으로 처리하지 않게 만들었다.
즉, useDeferredValue는 “이 값은 지금 당장 쓰지 않아도 된다”고 React에게 알려주는 역할을 한다.
useDeferredValue로 문제는 어느 정도 해결됐지만, React 18 관련 글들을 찾아보다 보니 비슷한 역할을 하는 훅이 하나 더 눈에 들어왔다.
바로 useTransition이다.
useTransition 사용법
const [isPending, startTransition] = useTransition();
그리고 상태를 감싸서 사용한다.
startTransition(() => {
setState(nextState);
});
이 훅의 핵심은 이 상태 업데이트는 우선순위가 낮으니 당장 처리 하지마 이다
즉, useDeferredValue처럼 값을 하나 더 만들어서 쓰는 게 아니라,
업데이트 자체를 React에게 “천천히 처리해도 된다”고 알려주는 방식이다
const [input, setInput] = useState("");
const [keyword, setKeyword] = useState("");
const [isPending, startTransition] = useTransition();
<input
value={input}
onChange={(e) => {
const value = e.target.value;
// 입력값은 즉시 반영
setInput(value);
// 자동완성 기준 값은 낮은 우선순위로 업데이트
startTransition(() => {
setKeyword(value);
});
}}
/>
쿼리는 키워드로 실행
useQuery({
queryKey: ["tag", keyword],
queryFn: () => fetchTag(keyword),
enabled: keyword.trim().length >= 2,
});
정리하자면
| 구분 | useDeferredValue | useTransition |
|---|---|---|
| 접근 방식 | 값 소비를 지연 | 상태 업데이트를 지연 |
| 코드 위치 | render 단계 | 이벤트 핸들러 |
| 제어권 | React에게 맡김 | 개발자가 명시 |
| 상태 구조 변경 | 거의 없음 | 분리 필요 |
결론 둘 다 “지연”은 맞지만,
useDeferredValueuseTransition