useDeferredValue 와 useTransition

용인사는 개발자·2026년 1월 15일
post-thumbnail

자동완성 입력에서 생긴 문제

사이드 프로젝트로 블로그를 만들던중에 태그 자동완성 기능이 있었는데, 태그 자동완성을 붙이다보니 타이핑할 때마다 쿼리가 너무 많이 날라가 문제가 생겼다.

1. 기존 문제

컴포넌트인 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,
});

문제점

  • input은 매 키 입력마다 즉시 변경
  • useQuery는 queryKey가 바뀌면 바로 실행
  • 결과적으로 타이핑 속도 = API 호출 속도, 비효율적

2. useDeferredValue 사용하여 해결

//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,
});

달라진 점

  • input은 그대로 즉시 업데이트
  • 쿼리는 조금 늦은 값 기준으로만 실행
  • 중간 입력 단계들이 자연스럽게 스킵됨

결과적으로 “입력 UX”와 “네트워크 요청”이 분리 되었다.

useDeferredValue?

위에 코드처럼 useDeferredValue를 사용하여 해결하였는데, 어떻게 해결이 된것일까?

useDeferredValue는 값을 즉시 바꾸는 훅이 아니고, 이미 변경된 값을 조금 늦게 소비할 수 있게 해주는 훅에 가깝다.
사용할땐 값을 래핑해서 사용한다.

const deferredInput = useDeferredValue(input);

이 코드는 input을 지연시키는 게 아니라, input의 느린 버전(slower copy)을 하나 더 만들어 준다.

그래서 동작 방식은 이렇다. input은 타이핑과 동시에 바로 업데이트되고 deferredInput은 React가 판단한 여유 있는 시점에 따라온다. 이 덕분에 입력 UX는 그대로 유지하면서 무거운 렌더링이나 네트워크 요청만 늦출 수 있다.

자동완성 예제에서는 input은 즉시 화면에 반영하고, 쿼리는 deferredInput 기준으로만 실행하도록 분리하면서 타이핑 속도와 API 호출을 같은 타이밍으로 처리하지 않게 만들었다.

즉, useDeferredValue“이 값은 지금 당장 쓰지 않아도 된다”고 React에게 알려주는 역할을 한다.

useTransition?

useDeferredValue로 문제는 어느 정도 해결됐지만, React 18 관련 글들을 찾아보다 보니 비슷한 역할을 하는 훅이 하나 더 눈에 들어왔다.
바로 useTransition이다.

1.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,
});

정리하자면

  • input은 타이핑 즉시 반응
  • 자동완성 기준 값은 천천히 따라옴
  • 쿼리는 keyword가 바뀔 때만 실행

useDeferredValue vs useTransition 비교

구분useDeferredValueuseTransition
접근 방식값 소비를 지연상태 업데이트를 지연
코드 위치render 단계이벤트 핸들러
제어권React에게 맡김개발자가 명시
상태 구조 변경거의 없음분리 필요

결론 둘 다 “지연”은 맞지만,

  • useDeferredValue
    값은 이미 바뀌었고, 그 값을 쓰는 렌더링만 늦춘다.
  • useTransition
    상태를 바꾸는 업데이트 자체를 낮은 우선순위로 늦춘다.
profile
용인에 거주하는 개발자입니다. 사이드프로젝트의 진행사항, 오류해결, 배운것들을 적습니다

0개의 댓글