React 18 useDeferredValue 심화: 검색 자동완성에서 디바운싱을 제거하는 이유

React 18 useDeferredValue 심화: 검색 자동완성에서 디바운싱을 제거하는 이유

React Query v5 마이그레이션: useMutation의 변화점 완벽 가이드

더 이상 mutate 함수에 콜백을 넣지 마세요.
v5가 가져온 변화의 이유와 올바른 사용법을 알아봅니다.


React 생태계에서 비동기 상태 관리의 표준으로 자리 잡은 TanStack Query(React Query)가 v5로 업데이트되면서 많은 변화가 있었습니다.
그중에서도 개발자들을 가장 당혹스럽게 만든 것은 바로 useMutation 훅의 사용법 변경일 것입니다.
혹시 습관적으로 mutate(variables, { onSuccess: ... }) 코드를 작성하고 계신가요?
v5에서는 이 패턴이 더 이상 동작하지 않거나, 경고를 발생시킬 수 있습니다.
왜 라이브러리 메인테이너들은 멀쩡하던 기능을 제거했을까요?
오늘 글에서는 useMutation의 변화된 점을 집중적으로 파헤치고, 어떻게 코드를 수정해야 하는지 명확한 가이드를 제시합니다.

⚠️ 핵심 요약: 무엇이 바뀌었나요?

이제 mutate() 함수를 호출할 때 콜백 함수(onSuccess, onError, onSettled)를 전달할 수 없습니다.
모든 사이드 이펙트 처리는 useMutation 훅을 선언하는 시점으로 이동해야 합니다.

🚀 1. mutate 트리거에서 콜백이 사라지다

React Query v4까지는 mutate 함수를 실행할 때, 실행 시점에 따른 콜백을 정의하는 것이 매우 흔한 패턴이었습니다.
UI에서 버튼을 클릭했을 때 특정 토스트 메시지를 띄우거나, 페이지를 이동시키는 로직을 mutate 함수 안에 직접 넣곤 했습니다.
하지만 v5부터는 이 기능이 공식적으로 제거되었습니다.

🔍 Code Comparison: Before vs After

❌ React Query v4 (Deprecated)
const { mutate } = useMutation({ mutationFn: updateTodo });

// 버튼 클릭 핸들러 내부
const handleSubmit = () => {
  mutate(id, {
    onSuccess: () => {
      console.log("성공!"); // v5에서는 실행되지 않음
    }
  });
};
✅ React Query v5 (Recommended)
const { mutate } = useMutation({
  mutationFn: updateTodo,
  onSuccess: () => {
    console.log("성공!"); // 훅 선언 시점에 정의
  }
});

// 버튼 클릭 핸들러 내부
const handleSubmit = () => {
  mutate(id); // 인자만 전달
};

위 예시처럼, 로직의 위치가 호출 시점(Trigger)에서 선언 시점(Definition)으로 강제 이동되었습니다.
이제 mutate는 오직 변수를 전달하는 역할에만 집중하게 됩니다.


🤔 2. 왜 이렇게 바뀌었을까?

많은 개발자가 편리하게 사용하던 기능을 왜 없앴을까요?
이유는 크게 두 가지, 예측 불가능한 동작 방지관심사의 분리 때문입니다.

  • 1. 조건부 실행(Race Condition) 문제 해결
    커스텀 훅으로 뮤테이션을 분리했을 때, 컴포넌트가 언마운트되면 mutate의 콜백이 실행되지 않는 경우가 있었습니다.
    반면 훅 자체에 정의된 onSuccess는 컴포넌트 생명주기와 무관하게 안정적으로 동작합니다.
    v5는 이러한 일관성 없는 동작을 원천 차단하고자 했습니다.
  • 2. UI와 데이터 로직의 분리
    useMutation 선언부는 데이터 상태 변화에 따른 '전역적인' 사이드 이펙트(예: 캐시 무효화)를 담당해야 합니다.
    반면 개별 컴포넌트에서의 UI 피드백은 다른 방식으로 처리하는 것이 구조적으로 더 깔끔합니다.

🛠️ 3. 그렇다면 개별 UI 처리는 어떻게?

"그럼 폼 제출 후 페이지 이동은 어디서 하나요?"라는 질문이 생길 수 있습니다.
여전히 방법은 존재합니다.
mutateAsync를 사용하면 Promise를 반환받아 비동기 흐름을 제어할 수 있습니다.

💡 mutateAsync를 활용한 패턴

const { mutateAsync } = useMutation({ mutationFn: updateTodo });

const handleSubmit = async () => {
  try {
    await mutateAsync(id);
    alert("저장되었습니다!"); // UI 관련 로직
    router.push('/list');
  } catch (error) {
    console.error(error);
  }
};

mutateAsync를 사용하면 await 키워드를 통해 뮤테이션이 완료될 때까지 기다릴 수 있습니다.
이후 try-catch 블록을 사용하여 성공/실패에 따른 UI 로직(모달 닫기, 페이지 이동 등)을 컴포넌트 내부에서 처리할 수 있습니다.
이 방식은 데이터 캐싱 로직(훅 내부)과 UI 흐름 제어(핸들러 내부)를 명확히 분리해줍니다.

구분 useMutation 옵션 (훅 내부) mutateAsync (핸들러 내부)
주요 목적 데이터 동기화, 캐시 무효화 (invalidateQueries) UI 피드백, 라우팅, 폼 리셋
실행 보장 거의 확실함 (선언적) 컴포넌트 마운트 상태에 의존

📝 요약 및 마무리

React Query v5의 변화는 처음에는 불편해 보일 수 있지만, 장기적으로는 더 견고하고 유지보수하기 쉬운 코드를 만들기 위한 과정입니다.
mutate의 콜백 제거는 개발자들에게 "데이터 로직"과 "UI 로직"을 어디에 두어야 할지 다시 한번 고민하게 만듭니다.

🔥 오늘 바로 적용할 액션 아이템
  1. 프로젝트 전체에서 mutate(variables, { ... }) 패턴 검색하기.
  2. 데이터 관련 로직(쿼리 무효화 등)은 useMutation 선언부로 이동시키기.
  3. 화면 이동이나 알림 같은 UI 로직은 mutateAsyncawait로 리팩토링하기.

댓글 쓰기