React 19 Concurrent 훅 - useTransition, useOptimistic vs React Query

useTransition

const [isPending, startTransition] = useTransition()

function handleFilter(value: string) {
  startTransition(async () => {
    const data = await fetchData(value)
    setResults(data)
  })
}
  • startTransition으로 감싼 state 업데이트는 **비긴급(non-urgent)**으로 처리
  • 급한 업데이트(타이핑, 클릭 피드백)를 먼저 처리하고, transition 작업은 뒤로 미룸
  • isPending으로 로딩 상태 확인, 기존 UI 유지하면서 백그라운드에서 새 UI 준비

useOptimistic

const [optimisticItems, addOptimistic] = useOptimistic(
  items,
  (current, newItem) => [...current, newItem]
)

async function handleAdd(item: Item) {
  addOptimistic(item) // 즉시 UI 반영
  await saveToServer(item) // 실패하면 자동 rollback
}
  • 서버 응답 전에 UI 먼저 업데이트, 실패 시 자동 복구
  • 좋아요 버튼, 장바구니 추가 같은 인터랙션에 적합

현실: Query가 이미 너무 편함

const { data, isPending } = useQuery({
  queryKey: ['items', filter],
  queryFn: () => fetchItems(filter),
})

useMutation({
  mutationFn: addItem,
  onMutate: async (newItem) => {
    const previous = queryClient.getQueryData(['items'])
    queryClient.setQueryData(['items'], (old) => [...old, newItem])
    return { previous }
  },
  onError: (err, _, context) => {
    queryClient.setQueryData(['items'], context.previous)
  },
})

캐싱, 리페치, devtools, stale-while-revalidate까지 한 방에 해결. 팀에서 이미 쓰고 있으면 “굳이?” 됨.

useTransition이 의미 있는 지점 - 무거운 클라이언트 연산, Next.js Server Actions 조합

<form action={(formData) => {
  startTransition(async () => {
    await createItem(formData)
    router.refresh()
  })
}}>
#495